前言

有了前两篇的铺垫,其实到这里再去分析基本就没有什么难点了,无非是苦力活罢了。

所以这篇文章主要是总结如何更好的分(偷)析(懒)。

抓包分析 弹幕消息

这里挑选了一个比较典型的包,它包括了弹幕内容并且有多条弹幕。

可以很明显看出,它是由两条弹幕构成的:

其中选中的文本部分显然是一个JSON,前面的非 ASCII 码没猜错应该就是头部了,我们看一下第一条弹幕的头部。

100 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,暂时猜测不出来它的意思,先放放。

也就是说目前头部分析出来的结果是:

12| 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

它包含了之前我抓包分析的内容,还有一些我没有分析到的内容,总之看这份代码基本上就可以偷懒了(逃

比如之前提到的消息种类判定代码:

1234567891011121314151617181920212223242526272829303132333435function 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 弹幕抓取系列到这里就结束了,这个过程中虽然绕了很多弯路浪费了大把时间,不过我还是学到了不少知识:

WebSocketFiddlerScript 编写WireShark 基本使用抓包分析能力(二进制敏感度?)

另外感觉计网光过了一遍课本真的不够,用起来总是觉得力不从心,等什么时候闲下来就去做计网实验吧(挖坑)。

(苏州铁艺楼梯)