# 说在前面
逻辑抄自 B 站弹幕姬 - Github
# 封包组成 (协议版本 1)
| packet_len | header_len | version | opcode | magic_number | payload |
|---|
| 含义 | 总包长度 | 头部长度 | 协议版本 | 操作码 | 幻数 | 负载 |
| 长度 (byte) | 4 | 2 | 2 | 4 | 4 | ... |
| 备注 | 包括头 | 固定 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; } }
|
# 拉取连接配置
- 拉取 json
https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id={需要拉取的房间号} - 从
host_server_list 中任取一个,拼接字符串 wss://{host_server_list}/sub ,使用 WebSocket 客户端连接,注意监听 DataReceive 事件而不是 MessageReceive 事件 - 之后进行鉴权操作,见下一节
# 鉴权
当连接建立(Open)之后,就可以发送鉴权了
1 2 3 4 5 6 7 8 9
| { "uid": 0, "roomid": {RoomID}, "protover": 0, "platform": "web", "clientver": "2.6.25", "key": {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 解压
Deflate 是 gzip 的实现算法。
代码示例 (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; }
|
# 爱生活远离套娃
看见上面这个馅饼,要做的事很简单了,就是一层一层的解。
协议头部有包长度,使用 packet_len - header_len 从负载中截取就是要获取的弹幕 Json 内容,下一层就使用 payload.length - packet_len + header_len 截取,之后以此类推。直到 payload.length == packet_len - header_len 。
每一层进行一次解析即可。
# 弹幕常见类型
| 名称 | 含义 |
|---|
| INTERACT_WORD | xx 人进入了直播间 |
| DANMU_MSG | 弹幕消息 内部参数大部分可读 |
| SEND_GIFT | 赠送礼物 单位 / 1000 为¥ |
| WATCHED_CHANGE | 看过人数变化 |
| SUPER_CHAT_MESSAGE | SC |
| ENTRY_EFFECT | 特效进场 |
| GUARD_BUY | 购买 / 续费舰长、提督、总督 |
| ONLINE_RANK_TOP | 高能榜变化 |