前言
有了前两篇的铺垫,其实到这里再去分析基本就没有什么难点了,无非是苦力活罢了。
所以这篇文章主要是总结如何更好的分(偷)析(懒)。
抓包分析
弹幕消息
这里挑选了一个比较典型的包,它包括了弹幕内容并且有多条弹幕。
可以很明显看出,它是由两条弹幕构成的:
其中选中的文本部分显然是一个JSON,前面的非 ASCII 码没猜错应该就是头部了,我们看一下第一条弹幕的头部。
1
|
00 00 01 0F 00 10 00 00 00 00 00 05 00 00 00 00
|
首先头部的长度是 16,也就是 0x10,所以大胆猜测其中的 00 10 字段就是头部长度,结合后面第二条弹幕头部的相同字段的值,基本确定就是头部长度。
那么可以猜测前面 00 00 01 0F 也就是 0x10F (别忘了 TCP 是大端序),应该是这条弹幕帧(头部+JSON文本数据)的长度。
观察到偏移 0X10F 处开始正好是第二条弹幕的内容,而且第二条弹幕头部中这个字段的值是 0x125,两者相加是 0x234 即整个包的长度,所以确定猜测是正确的。
此外还有一个非零值是 0x5,暂时猜测不出来它的意思,先放放。
也就是说目前头部分析出来的结果是:
1 2 |
| 00 00 01 0F | 00 10 | 00 00 00 00 00 05 00 00 00 00 | 帧长度 | 头部长度 | 未知 | |
然后我们再把注意力放到后面的 JSON 数据上,这里切换到 Fiddler 的 JSON 视图:
由于头部部分不是 UTF-8 编码干扰了解码,所以 Fiddler 只解析出来了第一条弹幕的文本部分,不过已经足够了。
其中有一个很重要的字段是 cmd,显然是 command 的缩写,它的值是 DANMU_MSG,所以显然是“弹幕消息”的意思。实际上通过抓包还可以看到有 WISH_BOTTLE, SEND_GIFT 等值,因此可以确定 cmd 用来指明弹幕的种类。
同时可以看到 info 中直接就包含了弹幕的内容(“看不懂”)、弹幕发送者的 id (“266649406”)、弹幕发送者的昵称(“简短回忆”)和头衔(“靖菌”)等等内容。
到这里弹幕的内容我们就可以抓取到了,但是还有一些细节性的东西。
人气值
持续抓包一段时间后,我们可以看到有非常多这样长度固定并且一来一回的包。
分别查看内容如下:
可以看到上行包长度固定是 16,下行包固定长度是 20,它们头部字段的含义跟之前的分析吻合,同时在下行的包中可以发现其中四字节的字段 0x73E8 就是人气值。
另外刚才弹幕包中头部为 0x5 的字段这里变成了 0x2 和 0x3,因此猜测应该是消息种类。
这里头部最后一个字段出现了一个 0x1,暂时不知道什么意思,先放着。
握手
现在我们回过头来分析最开始的握手包。
首先是请求包。
可以看到跟之前的分析都是吻合的,而且这里消息种类是 0x7 代表客户端请求连接,同时后面的文本信息包含了用户 id(这里我是游客,所以是0)、房间 id、客户端类型和客户端版本等信息。
然后是返回的包。
这个包很简单,只要头部,并且其中消息种类 0x8 表示服务器接受连接。
总结
可以看出,抓取弹幕的核心是要正确解析头部中的消息种类,目前出现过的种类有:
- 0x2 客户端请求人气值
- 0x3 服务端返回人气值
- 0x5 弹幕消息、礼物等等
- 0x7 客户端请求连接
- 0x8 服务端允许连接
以及头部的结构:
1
|
| 帧长度(4) | 头部长度(2) | 未知1(2) | 消息种类(4) | 未知(4) |
|
虽然还有很多未知的字段,不过按照目前掌握的内容已经足以正常抓取弹幕了。
站在巨人的肩膀上
decorator.js
抓包总是有限制的,当然我们也可以选择分析前端的代码,不过我当时看到 .min
就放弃了:
不过 Chrome 格式化之后搜索 DANMU_MSG 后还是有点收获的:
可以看到之前 cmd 的其它取值。
但是分析这个文件是真的难受,我看了 30min 就放弃了,不知道有没有好的分析办法。
Bilibili HTML5 Live
后来我偶然发现了这个脚本:Bilibili HTML5 Live
它包含了之前我抓包分析的内容,还有一些我没有分析到的内容,总之看这份代码基本上就可以偷懒了(逃
比如之前提到的消息种类判定代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
function onMessage(evt) { var data = evt.data; var dataView = new DataView(data, 0); var packetLen = dataView.getUint32(packetOffset); if (dataView.byteLength >= packetLen) { var headerLen = dataView.getInt16(headerOffset); var ver = dataView.getInt16(verOffset); var op = dataView.getUint32(opOffset); var seq = dataView.getUint32(seqOffset); switch (op) { case 8: this.heartBeat(); heartbeatInterval = setInterval(this.heartBeat.bind(this), 30 * 1000); break; case 3: if (this._listener) this._listener('online', dataView.getInt32(16)); break; case 5: var packetView = dataView; var msg = data; var msgBody; for (var offset = 0; offset < msg.byteLength; offset += packetLen) { packetLen = packetView.getUint32(offset); headerLen = packetView.getInt16(offset + headerOffset); msgBody = textDecoder.decode(msg.slice(offset + headerLen, offset + packetLen)); if (!msgBody) { textDecoder = getDecoder(false); msgBody = textDecoder.decode(msg.slice(offset + headerLen, offset + packetLen)); } if (this._listener) this._listener('msg', msgBody); } break; } } } |
此外可以看到之前的“未知1”字段是版本号,“未知2”字段可能是序列编号?这里存疑,不过不影响抓取弹幕。
小结
Bilibili 弹幕抓取系列到这里就结束了,这个过程中虽然绕了很多弯路浪费了大把时间,不过我还是学到了不少知识:
- WebSocket
- FiddlerScript 编写
- WireShark 基本使用
- 抓包分析能力(二进制敏感度?)
另外感觉计网光过了一遍课本真的不够,用起来总是觉得力不从心,等什么时候闲下来就去做计网实验吧(挖坑)。