0%

Bilibili直播弹幕抓取(3):弹幕传输协议分析

前言

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

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

抓包分析

弹幕消息

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

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

其中选中的文本部分显然是一个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 基本使用
  • 抓包分析能力(二进制敏感度?)

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