# 说在前面

逻辑抄自 B 站弹幕姬 - Github

# 封包组成 (协议版本 1)

packet_lenheader_lenversionopcodemagic_numberpayload
含义总包长度头部长度协议版本操作码幻数负载
长度 (byte)42244...
备注包括头固定 16固定 1固定 1大多数情况下为 json

# 操作码(OPCode)

操作码名称释义
CLIENT_HEARTBEAT客户端发送心跳包2
POPULARITY_VALUE人气值变动(当客户端发送心跳包时返回)3
CMD弹幕5
AUTHENTICATION鉴权7
SERVER_HEARTBEAT服务端心跳8

# 封包(Pack)

封包均为大端顺序。
代码示例 (C#)

1
2
3
4
5
6
7
8
9
10
11
12
public static byte[] Pack(OpCode opCode, byte[] payload)
{
using MemoryStream stream = new();
BinaryWriter bw = new(stream);
bw.Write(IPAddress.HostToNetworkOrder(payload.Length + HEADER_LEN));
bw.Write(IPAddress.HostToNetworkOrder(HEADER_LEN));
bw.Write(IPAddress.HostToNetworkOrder(PROTOCOL_VERSION));
bw.Write(IPAddress.HostToNetworkOrder((int)opCode));
bw.Write(IPAddress.HostToNetworkOrder(MAGIC_NUMBER));
bw.Write(payload);
return stream.ToArray();
}

IPAddress.HostToNetworkOrder() 用于转换大小端

# 解包(Unpack)

代码示例 (C#)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static RawDanmaku UnPack(byte[] data)
{
using MemoryStream stream = new(data);
BinaryReader reader = new(stream);
RawDanmaku danmaku = new();
danmaku.Data = new byte[data.Length - HEADER_LEN];
danmaku.PacketLen = IPAddress.NetworkToHostOrder(reader.ReadInt32());
danmaku.HeaderLen = (byte)IPAddress.NetworkToHostOrder(reader.ReadInt16());
danmaku.Version = (byte)IPAddress.NetworkToHostOrder(reader.ReadInt16());
danmaku.OpCode = (OpCode)IPAddress.NetworkToHostOrder(reader.ReadInt32());
danmaku.MagicNumber = IPAddress.NetworkToHostOrder(reader.ReadInt32());
Buffer.BlockCopy(data, HEADER_LEN, danmaku.Data, 0, danmaku.Data.Length);
return danmaku;
}

其中, RawDanmaku 为自定义类

1
2
3
4
5
6
7
8
9
public class RawDanmaku
{
public int PacketLen { get; set; }
public byte HeaderLen { get; set; }
public byte Version { get; set; }
public OpCode OpCode { get; set; }
public int MagicNumber { get; set; }
public byte[] Data { get; set; }
}

# 拉取连接配置

  1. 拉取 json https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id={需要拉取的房间号}
  2. host_server_list 中任取一个,拼接字符串 wss://{host_server_list}/sub ,使用 WebSocket 客户端连接,注意监听 DataReceive 事件而不是 MessageReceive 事件
  3. 之后进行鉴权操作,见下一节

# 鉴权

当连接建立(Open)之后,就可以发送鉴权了

1
2
3
4
5
6
7
8
9
{
"uid": 0,// 此处本应写自己的uid,未登录写0;
"roomid": {RoomID},// 监听的房间ID
"protover": 0,// 协议版本
"platform": "web",// 客户端类型
"clientver": "2.6.25",// 客户端版本
"key": {token},// 上一步获取的配置中的token
"type": 2
}

代码示例 (C#)

1
2
3
4
5
6
7
8
9
10
11
12
object Auth = new
{
uid = 0,
roomid = RoomID,
protover = 0,
platform = "web",
clientver = "2.6.25",
key = Config.token,
type = 2
};
var data = Helper.Pack(Enum.OpCode.AUTHENTICATION, Auth);
SocketConnect.Send(data, 0, data.Length);

将这个 json 通过封包,就可向上发送了。如果鉴权通过,服务器会返回来一个 SERVER_HEARTBEAT

# 心跳

当鉴权完成之后,B 站会维持本连接 70 秒,发送心跳包可刷新断开时间。心跳包 data 列为空,单纯将 OpCode 设置为 CLIENT_HEARTBEAT 就可以了,发送间隔没什么要求,20-40s 都可

# 接收弹幕

筛选 OPCode 为 CMD 的即可,其中大多都是使用 deflate 压缩的,也有没压缩的时候,建议用 try 统一处理。部分消息会使用套娃结构,本节来介绍处理方法。

# deflate 解压

Deflategzip 的实现算法。
代码示例 (C#)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static byte[] TryUncompress(byte[] data)
{
MemoryStream stream = new(data);
stream.Position = 2;
using var deflate = new DeflateStream(stream, CompressionMode.Decompress, false);
byte[] buffer = new byte[65535];
byte[] uncompressData;
try
{
int count = deflate.Read(buffer, 0, buffer.Length);
uncompressData = new byte[count];
Buffer.BlockCopy(buffer, 0, uncompressData, 0, count);
}
catch
{
uncompressData = data;
}
return uncompressData;
}

# 爱生活远离套娃

协议头负载
16
弹幕Json内容
协议头负载
16
弹幕Json内容
协议头负载
16
弹幕Json内容
...

看见上面这个馅饼,要做的事很简单了,就是一层一层的解。
协议头部有包长度,使用 packet_len - header_len 从负载中截取就是要获取的弹幕 Json 内容,下一层就使用 payload.length - packet_len + header_len 截取,之后以此类推。直到 payload.length == packet_len - header_len
每一层进行一次解析即可。

# 弹幕常见类型

名称含义
INTERACT_WORDxx 人进入了直播间
DANMU_MSG弹幕消息 内部参数大部分可读
SEND_GIFT赠送礼物 单位 / 1000 为¥
WATCHED_CHANGE看过人数变化
SUPER_CHAT_MESSAGESC
ENTRY_EFFECT特效进场
GUARD_BUY购买 / 续费舰长、提督、总督
ONLINE_RANK_TOP高能榜变化