using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Text.RegularExpressions; using BarrageGrab.Modles.BiliProtoEntity; using BarrageGrab.Proxy.ProxyEventArgs; using BrotliSharpLib; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace BarrageGrab { /// /// 哔哩哔哩弹幕抓取器 /// 协议格式: 自定义16字节Big-Endian帧头 + JSON body (body可能为zlib/brotli压缩) /// public class BiliBarrageGrab { // B站直播弹幕WS域名 // broadcast-msg.chat.bilibili.com (主域名) // *-live-comet.chat.bilibili.com (新版comet服务器, 如 zj-cn-live-comet.chat.bilibili.com) // live-trace.com (备用) // ws.live.bilibili.com (历史域名) private readonly Regex biliWsReg = new Regex(@"chat\.bilibili\.com|live-trace\.com|ws\.live\.bilibili\.com", RegexOptions.IgnoreCase); private const string BILI_LIVE_HOST = "live.bilibili.com"; /// 弹幕消息 public event EventHandler> OnChatMessage; /// 点赞消息 public event EventHandler> OnLikeMessage; /// 礼物消息 public event EventHandler> OnGiftMessage; /// 进入直播间/关注消息 public event EventHandler> OnInteractMessage; /// 房间统计消息 public event EventHandler> OnRoomStatsMessage; /// 分享消息 (INTERACT_WORD中msg_type=3) public event EventHandler> OnShareMessage; public BiliBarrageGrab() { } /// /// 当前认证的房间ID (从AUTH包解析,供消息处理器读取) /// public string CurrentRoomId { get; private set; } = ""; public bool IsBilibiliHost(string hostname) { if (string.IsNullOrWhiteSpace(hostname)) return false; hostname = hostname.Trim().ToLower(); return biliWsReg.IsMatch(hostname) || hostname == BILI_LIVE_HOST; } public bool IsBilibiliWebSocket(string uri) { if (string.IsNullOrWhiteSpace(uri)) return false; return biliWsReg.IsMatch(uri); } /// /// 处理代理收到的B站WS数据 /// public void HandleWebSocketData(WsMessageEventArgs e) { var buff = e.Payload; if (buff == null || buff.Length == 0) return; try { // 一个WS帧可能包含多个B站数据包,需要循环解析 ProcessBuffer(buff, 0, buff.Length, e.ProcessName); } catch (Exception ex) { if (biliErrCount < 3) { Logger.LogError(ex, $"处理B站弹幕数据包时出错:{ex.Message}"); biliErrCount++; } if (biliErrCount == 3) { Logger.LogWarn("B站弹幕解析持续失败,已抑制后续错误日志"); biliErrCount++; } } } /// /// 处理客户端发送的B站WS数据 (解析AUTH认证包提取roomid) /// B站客户端连接WS后会发送AUTH包: {uid,roomid,protover,platform,type,key} /// public void HandleSentData(WsMessageEventArgs e) { var buff = e.Payload; if (buff == null || buff.Length < BiliSocketFrame.HEADER_SIZE) return; try { BiliSocketFrame frame; if (!BiliSocketFrame.TryParse(buff, 0, buff.Length, out frame)) return; // 只关注认证请求包 (Operation=7) if (frame.Operation != BiliOperation.AUTH) return; var payload = BiliSocketFrame.GetPayload(buff, 0, frame); if (payload == null || payload.Length == 0) return; var json = System.Text.Encoding.UTF8.GetString(payload); var jobj = JObject.Parse(json); var roomIdToken = jobj["roomid"]; if (roomIdToken == null) return; // roomid可能是整数或字符串 string roomId = roomIdToken.Type == JTokenType.Integer ? roomIdToken.Value().ToString() : roomIdToken.Value() ?? ""; if (!string.IsNullOrWhiteSpace(roomId)) { CurrentRoomId = roomId; Logger.LogInfo($"B站认证包解析成功,当前房间ID: {roomId}"); } } catch (Exception ex) { Logger.LogWarn($"解析B站AUTH认证包失败: {ex.Message}"); } } private static int biliErrCount = 0; /// /// 递归解析缓冲区中可能包含的多个B站数据包 /// private void ProcessBuffer(byte[] buffer, int offset, int count, string processName) { int processed = 0; while (processed + BiliSocketFrame.HEADER_SIZE <= count) { BiliSocketFrame frame; if (!BiliSocketFrame.TryParse(buffer, offset + processed, count - processed, out frame)) { break; } // 包长超过剩余缓冲区,说明数据不完整,等下次再处理 if (processed + frame.PacketLength > count) { break; } var payload = BiliSocketFrame.GetPayload(buffer, offset + processed, frame); processed += (int)frame.PacketLength; // 只处理服务器推送的消息和心跳回复 if (frame.Operation == BiliOperation.HEARTBEAT_REPLY) { // 心跳回复: body是4字节big-endian人气值 if (payload != null && payload.Length >= 4) { var online = (long)BiliSocketFrame.ReadUInt32BE(payload, 0); var stats = new BiliRoomStats { Online = online, Watched = 0 }; OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats)); } continue; } if (frame.Operation != BiliOperation.MESSAGE) continue; // 根据协议版本解压 byte[] jsonBytes; switch (frame.ProtocolVersion) { case BiliProtocolVersion.PLAIN: case BiliProtocolVersion.PLAIN_AUTH: jsonBytes = payload; break; case BiliProtocolVersion.ZLIB: jsonBytes = ZlibDecompress(payload); break; case BiliProtocolVersion.BROTLI: jsonBytes = BrotliDecompress(payload); break; default: jsonBytes = payload; break; } if (jsonBytes == null || jsonBytes.Length == 0) continue; // 解压后可能仍然是多个数据包 (Zlib/Brotli压缩的通常是合并包) if (frame.ProtocolVersion == BiliProtocolVersion.ZLIB || frame.ProtocolVersion == BiliProtocolVersion.BROTLI) { ProcessBuffer(jsonBytes, 0, jsonBytes.Length, processName); continue; } // 明文JSON: 解析为消息 DispatchJsonMessage(jsonBytes, processName); } } /// /// 将JSON字节解析为B站消息并触发对应事件 /// private void DispatchJsonMessage(byte[] jsonBytes, string processName) { string json; try { json = System.Text.Encoding.UTF8.GetString(jsonBytes); } catch { return; } if (string.IsNullOrWhiteSpace(json)) return; JObject jobj; try { jobj = JObject.Parse(json); } catch { return; } var cmdToken = jobj["cmd"]; if (cmdToken == null) return; string cmd = cmdToken.Value(); if (string.IsNullOrWhiteSpace(cmd)) return; // 兼容cmd带数字后缀的版本,如 DANMU_MSG-4b1cb6f3 int dashIdx = cmd.IndexOf('-'); if (dashIdx > 0) cmd = cmd.Substring(0, dashIdx); switch (cmd) { case BiliCmd.DANMU_MSG: { var msg = ParseDanmuMsg(jobj); if (msg != null) { OnChatMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg)); } break; } case BiliCmd.SEND_GIFT: { var msg = jobj["data"]?.ToObject(); if (msg != null) { OnGiftMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg)); } break; } case BiliCmd.INTERACT_WORD: { var msg = jobj["data"]?.ToObject(); if (msg != null) { // msg_type: 1=进入 2=关注 3=分享 if (msg.MsgType == 3) { OnShareMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg)); } else { OnInteractMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg)); } } break; } case BiliCmd.LIKE_INFO_V3_CLICK: { var msg = jobj["data"]?.ToObject(); if (msg != null) { OnLikeMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, msg)); } break; } case BiliCmd.ONLINE_RANK_COUNT: { var count = jobj["data"]?["count"]?.Value() ?? 0; var stats = new BiliRoomStats { Online = count, Watched = 0 }; OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats)); break; } case BiliCmd.WATCHED_CHANGE: { var num = jobj["data"]?["num"]?.Value() ?? 0; var stats = new BiliRoomStats { Online = 0, Watched = num }; OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats)); break; } case BiliCmd.ROOM_REAL_TIME_MESSAGE_UPDATE: { // 同时包含在线人数和看过人数 var online = jobj["data"]?["online"]?.Value() ?? 0; var watched = jobj["data"]?["watched_show"]?["num"]?.Value() ?? 0; var stats = new BiliRoomStats { Online = online, Watched = watched }; OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs(processName, stats)); break; } default: // 未知消息类型忽略 break; } } /// /// 解析DANMU_MSG消息: info[1]=内容, info[2]=[uid,uname,isAdmin,rank,?anchor] /// private BiliDanmuMsg ParseDanmuMsg(JObject jobj) { try { var info = jobj["info"] as JArray; if (info == null || info.Count < 3) return null; var content = info[1]?.Value() ?? ""; var userInfo = info[2] as JArray; if (userInfo == null || userInfo.Count < 2) return null; var msg = new BiliDanmuMsg { Content = content, Uid = userInfo[0]?.Value() ?? 0, Uname = userInfo[1]?.Value() ?? "", IsAdmin = userInfo.Count > 2 && userInfo[2]?.Value() == 1, IsAnchor = userInfo.Count > 4 && userInfo[4] != null && userInfo[4].Type != JTokenType.Null }; return msg; } catch { return null; } } /// Zlib解压 private byte[] ZlibDecompress(byte[] data) { if (data == null || data.Length == 0) return null; try { using (var ms = new MemoryStream(data)) using (var zlib = new DeflateStream(ms, CompressionMode.Decompress)) using (var outMs = new MemoryStream()) { // zlib前2字节是header(0x78 0x9c等),DeflateStream需要跳过 // 但 .NET Core 2.1+ 的 DeflateStream 可以处理 zlib header, .NET Framework 需要跳过 if (data.Length >= 2 && data[0] == 0x78) { ms.Position = 2; } zlib.CopyTo(outMs); return outMs.ToArray(); } } catch (Exception ex) { Logger.LogWarn($"B站Zlib解压失败: {ex.Message}"); return null; } } /// Brotli解压 (使用 BrotliSharpLib) private byte[] BrotliDecompress(byte[] data) { if (data == null || data.Length == 0) return null; try { using (var ms = new MemoryStream(data)) using (var brotli = new BrotliSharpLib.BrotliStream(ms, CompressionMode.Decompress)) using (var outMs = new MemoryStream()) { brotli.CopyTo(outMs); return outMs.ToArray(); } } catch (Exception ex) { Logger.LogWarn($"B站Brotli解压失败: {ex.Message}"); return null; } } } /// B站消息事件参数 public class BiliRoomMessageEventArgs : EventArgs where T : class { public string Process { get; set; } public T Message { get; set; } public BiliRoomMessageEventArgs() { } public BiliRoomMessageEventArgs(string process, T data) { this.Process = process; this.Message = data; } } }