2022-01-06 19:41:42 +08:00
|
|
|
|
# B 站直播弹幕 websocket 协议分析
|
|
|
|
|
|
|
|
|
|
## 包格式
|
|
|
|
|
|
2022-01-08 12:47:28 +08:00
|
|
|
|
```txt
|
2022-01-08 12:50:03 +08:00
|
|
|
|
____________________________________________________________________________
|
|
|
|
|
| | | | | |
|
|
|
|
|
| package_size | header_size | protocol_version | operation | sequence_id |
|
|
|
|
|
|______________|_____________|__________________|____________|_____________|
|
|
|
|
|
| |
|
|
|
|
|
| data |
|
|
|
|
|
| |
|
|
|
|
|
|__________________________________________________________________________|
|
2022-01-08 12:47:28 +08:00
|
|
|
|
```
|
|
|
|
|
|
2022-01-06 19:41:42 +08:00
|
|
|
|
包为 byte 数据,由头部和数据组成,字节序均为大端模式
|
|
|
|
|
|
|
|
|
|
头部长度为 16
|
|
|
|
|
|
|
|
|
|
| 偏移量 | 长度 | 含义 |
|
|
|
|
|
| ------ | ---- | ------------------------- |
|
2022-01-08 12:50:03 +08:00
|
|
|
|
| 0 | 4 | 包总大小 package_size |
|
2022-01-06 19:41:42 +08:00
|
|
|
|
| 4 | 2 | 头部大小 header_size |
|
|
|
|
|
| 6 | 2 | 协议版本 protocol_version |
|
|
|
|
|
| 8 | 4 | 操作码 operation |
|
|
|
|
|
| 12 | 4 | 包序列 sequence_id |
|
|
|
|
|
|
|
|
|
|
操作码含义
|
|
|
|
|
|
|
|
|
|
| 操作码 | 含义 |
|
|
|
|
|
| ------ | --------------------------------------------------- |
|
|
|
|
|
| 2 | 心跳 HEARTBEAT (标记该包为心跳包) |
|
|
|
|
|
| 3 | 心跳回应 HEARTBEAT_REPLY (有时候带着人气值数据返回) |
|
|
|
|
|
| 5 | 消息 NOTIFY (B 站的弹幕或业务消息都属于这个操作码) |
|
|
|
|
|
| 7 | 认证 AUTH (标记该包为认证包) |
|
|
|
|
|
| 8 | 认证回复 AUTH_REPLY |
|
|
|
|
|
|
|
|
|
|
协议版本
|
|
|
|
|
|
|
|
|
|
| 协议版本 | 含义 |
|
|
|
|
|
| -------- | ----------------------- |
|
|
|
|
|
| 0 | NORMAL 未压缩的正常消息 |
|
|
|
|
|
| 1 | HEARTBEAT 心跳 |
|
|
|
|
|
| 2 | DEFLATE zlib 压缩包 |
|
|
|
|
|
| 3 | BROTLI brotil 压缩包 |
|
|
|
|
|
|
|
|
|
|
## 连接建立流程
|
|
|
|
|
|
|
|
|
|
1. 根据直播间 id 得到 websocket 连接地址
|
|
|
|
|
|
|
|
|
|
B 站直播间 id 分短 id 和真正的 id,如 510,605,1314,7777 等都属于短 id,需要请求
|
|
|
|
|
|
|
|
|
|
`GET https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=房间号`
|
|
|
|
|
|
|
|
|
|
得到真正的房间号,再次请求
|
|
|
|
|
|
|
|
|
|
`GET https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id=真正的房间号&platform=pc&player=web`
|
|
|
|
|
|
|
|
|
|
系统会分配 3 个 websocket 连接地址 (负载均衡考虑) 和 连接 token,按照 `wss://{host}:{wss_port}/sub` (TLS WS 连接) 或 `ws://{host}:{ws_port}/sub` (WS 连接格式) 的格式拼接,即可得到 websocket 连接地址
|
|
|
|
|
|
|
|
|
|
2. 建立连接后,发送认证包
|
|
|
|
|
|
|
|
|
|
建立连接后需要立即发送认证包,否则服务器会断开连接
|
|
|
|
|
|
|
|
|
|
认证包数据载荷为 json 格式的字符串,内容如下,和头部拼接时候需要 使用 utf-8 编码转化为 bytes
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"uid": 0, // 用户id,0 代表游客
|
|
|
|
|
"roomid": 123, //你要收取弹幕的房间号
|
|
|
|
|
"protover": 3, //后面通信要使用的协议版本,主要影响处理粘包时的解码选择,选3代表后面粘包使用 brotil 压缩,
|
|
|
|
|
"platform": "web", // 平台选择web端
|
|
|
|
|
"type": 2, //不明确,填2
|
|
|
|
|
"clientver": "1.4.3", //客户端版本,填1.4.3
|
|
|
|
|
"key": "your token" //填写上一步系统给的token值
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
头部操作码选择 7 (AUTH 认证) ,版本为 0 (NORMAL 未压缩的消息), sequence_id 填 1,包大小和头部大小请自行计算填写
|
|
|
|
|
|
|
|
|
|
3. 定时发送心跳包
|
|
|
|
|
|
|
|
|
|
B 站服务器设置的心跳间隔约为 70s 左右,所以连接建立后每隔 30s 发送一次心跳维持连接基本稳妥。心跳包内容随意填写无影响,头部操作码选择 2 (HEARTBEAT 心跳), 版本选择 0 (NORMAL 未压缩的消息)
|
|
|
|
|
|
|
|
|
|
## 收到包的消息处理
|
|
|
|
|
|
|
|
|
|
服务器推送的包只会有 3 种操作码,分别为 3(心跳回应),5(消息),8(认证回复),所以我们只需要对这三种包进行处理
|
|
|
|
|
|
|
|
|
|
- ### 心跳回应
|
|
|
|
|
|
2022-01-07 11:35:39 +08:00
|
|
|
|
心跳回应数据都是未压缩的,所以可以跳过协议判断,直接 `data[16+4:]`截取有效载荷再使用 utf-8 编码转化为字符串即可,16 是头部长度,+4 是因为数据段前面还有 4 位无效数据?(含义不明,不知道为什么有),直接去掉
|
2022-01-06 19:41:42 +08:00
|
|
|
|
|
|
|
|
|
- ### 认证回复
|
|
|
|
|
|
|
|
|
|
认证回复为发送认证包后收到的回复,数据包无压缩,直接 `data[16:]`截取有效载荷再使用 utf-8 编码转化为字符串即可
|
|
|
|
|
|
|
|
|
|
- ### 消息
|
|
|
|
|
|
2022-01-07 11:35:39 +08:00
|
|
|
|
首先说明,B 站对 消息 这一数据包出于服务器效率考虑会存在粘包的情况(目前的情况是一定会有粘包的存在),所以在编写处理代码时无可避免要处理这一情况,并且在认证(AUTH)包发送阶段选择的 协议版本 的值会在这一阶段产生影响,如果在认证时选择 0 (NORMAL 未压缩的消息) 或者 1 (HEARTBEAT),则默认服务器在粘包使用 zlib 压缩多个包,若选择 3(Brotil 压缩),则会使用 Brotil 压缩多个包。首先说明服务端的粘包逻辑,这有利于编写解包代码
|
2022-01-06 19:41:42 +08:00
|
|
|
|
|
|
|
|
|
- 服务器正常打包每一个数据包
|
|
|
|
|
|
|
|
|
|
```txt
|
|
|
|
|
----------
|
|
|
|
|
| header |
|
|
|
|
|
|--------|
|
|
|
|
|
| |
|
|
|
|
|
| data |
|
|
|
|
|
| |
|
|
|
|
|
----------
|
|
|
|
|
----------
|
|
|
|
|
| header |
|
|
|
|
|
|--------|
|
|
|
|
|
| |
|
|
|
|
|
| data |
|
|
|
|
|
| |
|
|
|
|
|
----------
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
2022-01-07 11:35:39 +08:00
|
|
|
|
- 服务器发现包太多,或者单个消息包太大,都会使用认证期选择的压缩协议压缩数据包,并再套上一个包头,协议版本字段写明使用的压缩协议
|
2022-01-06 19:41:42 +08:00
|
|
|
|
|
|
|
|
|
```txt
|
|
|
|
|
----------------
|
|
|
|
|
| header |
|
|
|
|
|
----------------
|
|
|
|
|
| ---------- |
|
|
|
|
|
| | header | |
|
|
|
|
|
| |--------| |
|
|
|
|
|
| | | |
|
|
|
|
|
| | data | |
|
|
|
|
|
| | | |
|
|
|
|
|
| ---------- |
|
|
|
|
|
| ---------- |
|
|
|
|
|
| | header | |
|
|
|
|
|
| |--------| |
|
|
|
|
|
| | | |
|
|
|
|
|
| | data | |
|
|
|
|
|
| | | |
|
|
|
|
|
| ---------- |
|
|
|
|
|
----------------
|
|
|
|
|
```
|
|
|
|
|
|
2022-01-07 11:35:39 +08:00
|
|
|
|
所以判断是否粘贴包的逻辑为
|
2022-01-06 19:41:42 +08:00
|
|
|
|
|
|
|
|
|
```txt
|
2022-01-07 11:35:39 +08:00
|
|
|
|
--------->说明数据载荷为多个包粘贴或单个包太大---->丢弃头部,使用对应压缩协议解压数据段,得到无缝拼接的包数据
|
|
|
|
|
/ 然后对数据进行拆包提取数据
|
|
|
|
|
/
|
|
|
|
|
zlib/brotil压缩
|
|
|
|
|
/
|
|
|
|
|
收到数据----->判断头部协议版本
|
|
|
|
|
\
|
|
|
|
|
正常消息
|
|
|
|
|
\
|
|
|
|
|
-------->说明数据载荷为单个数据---->直接提取数据
|
2022-01-06 19:41:42 +08:00
|
|
|
|
|
|
|
|
|
```
|
2022-01-07 11:35:39 +08:00
|
|
|
|
|
|
|
|
|
拆包逻辑的主要思想是:首先读取头部信息,计算数据长度是否大于头部声明的数据包长度。如果数据长度大于头部声明的数据长度,则按声明的数据长度截取数据,取出第一个包,然后相同逻辑判断后面的数据,代码实现请参考源代码 blive/core.py `BWS_MsgPackage`类的`unpack`方法实现
|