从零开始仿写一个B站客户端之抓包接口

3,769 阅读5分钟

章节

从零开始仿写一个B站客户端之-编译ijkplayer

从零开始仿写一个B站客户端之-抓包B站接口

从零开始仿写一个B站客户端之-使用ijkplayer打造一个通用的播放器

从零开始仿写一个B站客户端之-整体架构设计和网络请求封装

既然要仿写一个客户端,那么数据从哪儿来呢?刚开始打算用springboot自己写后端,然后去学吧,第一个hello world项目就运行不起了(;′⌒`),这不是明示我劝退吗。所以果断抓包B站接口,不需要花费时间写后端代码,还还不缺数据来源。

抓包工具使用的是fiddler,对手机上的B站客户端进行抓包。需要注意的是必须确保安装fiddler的电脑和手机在同一个局域网环境下 。

电脑上的配置

  1. 配置fiddler代理端口号
  2. 配置https,也可以不配,目前抓包出来的B站接口都是http的,以防万一还是配上吧。

安装好fiddler之后,在Tools->Options->Connections中这样配:

Fiddler listens on port是手机连接fiddler时的代理端口号,.Allow remote computers to connect是允许远程(手机端)发送请求。

配置Https:

点击ok之后,在手机上访问电脑ip+8888,电脑ip查看方式是在cmd中输入ipconfig:

手机上的配置

我这里的ip是192.168.0.134,手机端用自带的浏览器访问: http://192.168.0.134:8888下载证书并安装。点击最下面那个蓝色链接安装证书:

emmm...小米手机不能直接安装,需要从更多设置 ->系统安全->加密与凭据->从sd卡中安装证书

安装证书完成之后,需要修改wifi的网络,手动设置代理,代理服务器主机名为电脑的IP地址,代理端口为在fiddler里设置的端口号,保存后,fiddler将能够收到手机上的请求信息:

准备就绪,可以在手机上打开B站客户端开始抓包了:

接口分析

以直播up主的粉丝榜接口为例,抓到的接口是这个样子的:

http://api.live.bilibili.com/rankdb/v2/RoomRank/mobileMedalRank?actionKey=appkey&appkey=1d8b6e7d45233436&build=5400000&channel=bilibiil140&device=android&mobi_app=android&page=1&platform=android&roomid=2910685&ruid=33588706&ts=1556159005&sign=fdc0eb4340508ad9a62d1a27146a4183

这太长了,可以剔除一些无用信息,变为下面这样:

http://api.live.bilibili.com/rankdb/v2/RoomRank/mobileMedalRank?page=1&roomid=2910685&ruid=33588706

其中page表示第一页,roomid表示房间号,ruid表示up主的uid。

有些接口是可以剔除的,但是有些接口是必须要一家人整整齐齐的,比如获取直播up主的uid信息就需要全部参数:

http://api.live.bilibili.com/xlive/app-room/v1/index/getInfoByRoom?actionKey=appkey&appkey=1d8b6e7d45233436&build=5400000&channel=bilibiil140&device=android&mobi_app=android&platform=android&room_id=2910685&ts=1556157467&sign=811b018c9e54efad87e4ec16a76cd111

前面的参数几乎都是固定的,除了最后的roomidts以及sign,如果有一个参数不正确,服务器就会返回下面的错误:

{
	"code": -3,
	"message": "API校验密匙错误",
	"ttl": 1
}

API校验密匙就是最后一个参数sign,它是通过前面的参数排序之后,加上SecretKey 做md5生成的,其中SecretKey存放在了so库中,具体操作参考了@Misery_Dx的:

仿B站Android客户端系列(启动篇)

获取sign的代码:

fun getSign(map: Map<String, Any>): String {
        //拼接参数(按顺序) + SecretKey
        val orignSign = getUrlParamsByMap(map) + SECRET_KEY
        //进行MD5加密
        var sign = ""
        try {
            sign = MD5Util.getMD5(orignSign).trim()
            Log.i(TAG, "加密后的sign: $sign")
        } catch (e: NoSuchAlgorithmException) {
            Log.e(TAG, "sign encryption failed: ${e.printStackTrace()}")
        }
        return sign
    }

/**
     * 将map转换成url参数
     * @param map
     * @return
     */
    fun getUrlParamsByMap(map: Map<String, Any>): String {
        var params =  StringBuffer()
        val it = map.iterator()
        while (it.hasNext()) {
            val str = it.next()
            params.append(str.key)
            params.append("=")
            params.append(str.value)
            if (it.hasNext()) {
                params.append("&")
            }
        }
        return params.toString()
    }

能获取到sign,大部分的问题就解决了。

直播弹幕的获取参考@lovelyyoshino直播弹幕 WebSocket 协议,嗯。。。没搞定,不知道是我发送的封包数据有问题还是B站直播弹幕协议变了,如果有大佬能搞定,希望能不吝赐教,感谢感谢~。

这是发送数据封包的方法:

/**
     * @param cmd 命令
     * @param data 数据包
     */
    private fun sendCmd(cmd: Int, data: ByteArray, webSocket: WebSocket){
        var buffer = ByteBuffer.allocate(16 + data.size)
        buffer.order(ByteOrder.BIG_ENDIAN)  //字节序为大端模式
        buffer.putInt(16 + data.size)
        buffer.putShort(16)  //头部长度
        buffer.putShort(1)  //协议版本,目前是1
        buffer.putInt(cmd)  //操作码(封包类型)
        buffer.putInt(1)  //sequence,可以取常数1
        buffer.put(data)
        webSocket.send(ByteString.of(buffer))
    }

这是使用okhttp3自带的websocket实现的加入房间:

private var webSocket: okhttp3.WebSocket? =null
    fun joinRoom(roomId:Int){
        var client = OkHttpClient.Builder().build()
        var request = Request
            .Builder()
            .url(GlobalProperties.LIVE_DANMAKU_URL)
            .build()
        webSocket = client.newWebSocket(request,object : WebSocketListener(){
            override fun onOpen(webSocket: okhttp3.WebSocket, response: Response) {
                super.onOpen(webSocket, response)
                var inRoomMessage = JSONObject()
                inRoomMessage.put("clientver","1.6.3")
                inRoomMessage.put("platform","web")
                inRoomMessage.put("protover",2)
                inRoomMessage.put("roomid",roomId)  //必填
                inRoomMessage.put("type",2)
                var bytes = inRoomMessage.toString().toByteArray(Charsets.UTF_8)
                sendCmd(7, bytes, webSocket)
                Log.d(TAG,"websocket连接成功,发送进房消息$inRoomMessage")
            }

            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                super.onMessage(webSocket, bytes)
                Log.d(TAG,"websocket接收消息$bytes")
            }
            override fun onClosed(webSocket: okhttp3.WebSocket, code: Int, reason: String) {
                super.onClosed(webSocket, code, reason)
                Log.d(TAG,"websocket断开连接")
                exitRoom()
            }

            override fun onFailure(webSocket: okhttp3.WebSocket, t: Throwable, response: Response?) {
                super.onFailure(webSocket, t, response)
                Log.d(TAG,"websocket连接失败: $response , throw: $t")
                exitRoom()
            }
        })
    }

第一次连接成功之后,发送进房消息,然后连接就立即断开了,应该是我发送的数据不对,才导致服务端主动断开连接的。

目前暂时是使用直播历史评论抓取,而不是实时的:

http://api.live.bilibili.com/xlive/app-room/v1/dM/gethistory?room_id=2910685

直播弹幕的获取暂时就放后面再说了~

目前我抓取的接口都放在GlobalProperties这个类里面,不想自己抓包的同学可以去这里看:

项目地址:仿BiliBili客户端