.NET斗鱼直播弹幕客户端(2021)
.NET斗魚直播彈幕客戶端(2021)
離之前更新的兩篇《.NET斗魚直播彈幕客戶端》已經有一段時間,近期有許多客戶向我反饋剛好有這方面的需求,但之前的代碼不能用了——但網上許多流傳的Node.js、Python腳本卻可以用,這豈能忍?(剛好我終于找回了我的發布密碼????)因此我有動力重新對此進行好(xie)好(xie)研(bo)究(ke)。
為何之前的不能用了
重新運行之前的C#腳本,發現是在這一行報錯的:
using?var?client?=?new?TcpClient(); await?client.ConnectAsync("openbarrage.douyutv.com",?8601);?//?這里報錯網上查了查,發現斗魚確實已經停止使用openbarrage.douyutv.com:8601了。進一步查資料顯示,新url改成了danmuproxy.douyu.com,斗魚已經統一使用WebSocket協議(之前為TCP協議),經過進一步對比新協議代碼示例,發現協議過程沒有任何區別,序列化也依然用的STT算法。
私貨時間: 我認為斗魚這樣做合理,因為WebSocket性能不差,且不需再為瀏覽器和第三方接口各自維護兩套不同的代碼。具體過程如下:
建立WebSocket連接
發送登錄請求(可匿名)
加入指定的房間號
每隔45秒,響應一次心跳包
(此時,即可)正常接收彈幕數據
新代碼實現
.NET中有許多提供WebSocket功能的庫和第三方包,之前我經常用websocket-sharp,這是第三方包?,F在我們盡量不用第三方包,官方提供的WebSocket客戶端叫System.Net.WebSockets.ClientWebSocket,同時支持.NET 4.5和.NET Core。
按正常的思路,我們會這樣寫:
return?Observable.Create<string>(async?(roomId,?cancellationToken)?=> {using?var?ws?=?new?ClientWebSocket();await?ws.ConnectAsync(new?Uri("wss://danmuproxy.douyu.com:8506/"),?cancellationToken);await?MsgTool.LoginAsync(ws,?roomId,?cancellationToken);//?other?codes });但實際運行卻不行,會報這個錯:
WebSocketException: The?'Sec-WebSocket-Accept'?header?value?'Kfh9QIsMVZcl6xEPYxPHzW8SZ8w='?is?invalid.相信我,如果你仔細對比Node/Python和.NET代碼,整個代碼中沒任何區別,但打開Fiddler仔細分析協議,發現事情沒這么簡單,這是一個無法成功連上服務器的包:
請求: GET?https://danmuproxy.douyu.com:8506/?HTTP/1.1 Host:?danmuproxy.douyu.com:8506 Connection:?Upgrade Upgrade:?websocket Sec-WebSocket-Version:?13 Sec-WebSocket-Key:?VsPg1/SSskKrbYouGm3ROQ==響應: HTTP/1.1?101?Switching?Protocols Upgrade:?websocket Connection:?Upgrade Sec-WebSocket-Accept:?Kfh9QIsMVZcl6xEPYxPHzW8SZ8w= Sec-WebSocket-Version:?13 EndTime:?09:37:44.958 ReceivedBytes:?0 SentBytes:?0研究原因
其中請注意看請求中的Sec-WebSocket-Key項,和響應中的Sec-WebSocket-Accept項。
按照WebSocket協議(https://tools.ietf.org/html/rfc6455#p-11.3.3),服務器響應頭Sec-WebSocket-Accept項的值,應該為請求頭Sec-WebSocket-Key項字符串追加固定值"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",然后計算其SHA1哈希值,再求Base64,用C#代碼說,這一過程如下:
string?WebSocketComputeAccept(string?key) {using?var?sha?=?SHA1.Create();byte[]?hash?=?sha.ComputeHash(Encoding.UTF8.GetBytes(key?+?"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));return?Convert.ToBase64String(hash); }如上的VsPg1/SSskKrbYouGm3ROQ==按這個計算過程,它應該返回VrPdUdxpPeBXDi1ttGN607h8ct0=,但實際卻是Kfh9QIsMVZcl6xEPYxPHzW8SZ8w=,這就是為何C#會報錯,因此服務端返回了錯誤值。
進一步研究原因
我嘗試了許多次,C#用客戶端連接時,總是會生成隨機的Sec-WebSocket-Key值,但不管值如何,服務端總是會返回相同的值——但一旦切換為Node.js,返回的值就完全正常。
我仔細分析了其它語言的WebSocket頭與.NET的區別,發現一個重要因素:.NET客戶端請求中的Sec-WebSocket-Key項,一定是最后一條,但其它語言中不是最后一條。
如果我們使用Fiddler手動發送握手請求,將Sec-WebSocket-Key與Sec-WebSocket-Version順序對調一下,發現響應值如下(服務器響應匹配):
HTTP/1.1?101?Switching?Protocols Upgrade:?websocket Connection:?Upgrade Sec-WebSocket-Accept:?VrPdUdxpPeBXDi1ttGN607h8ct0= Sec-WebSocket-Version:?13然而用ClientWebSocket是無法控制請求頭順序的,這一點可以在源代碼中找到。
最終答案
雖然無法控制請求頭順序,但可以控制Sec-WebSocket-Key不是最后一個,只需添加一個子協議頭,值無所謂:ws.Options.AddSubProtocol("-");,因此重點代碼如下(完整代碼請見LINQPad腳本——douyu-2020.linq):
using?var?ws?=?new?ClientWebSocket(); ws.Options.AddSubProtocol("-"); await?ws.ConnectAsync(new?Uri("wss://danmuproxy.douyu.com:8506"),?QueryCancelToken); await?ws.SendAsync(SerializeDouyu($"type@=loginreq/roomid@=74751/ver@=20190610/"),?WebSocketMessageType.Binary,?false,?QueryCancelToken); await?ws.SendAsync(SerializeDouyu($"type@=joingroup/rid@=74751/gid@=-9999/"),?WebSocketMessageType.Binary,?false,?QueryCancelToken); _?=?Task.Run(async?()?=> {while?(!QueryCancelToken.IsCancellationRequested){await?Task.Delay(45000,?QueryCancelToken);await?ws.SendAsync(SerializeDouyu($"type@=mrkl/"),?WebSocketMessageType.Binary,?false,?QueryCancelToken);} });while?(!QueryCancelToken.IsCancellationRequested) {var?buffer?=?new?byte[4096];WebSocketReceiveResult?r?=?await?ws.ReceiveAsync(buffer,?QueryCancelToken);string?result?=?DeserializeDouyu(new?Memory<byte>(buffer,?0,?r.Count),?QueryCancelToken);DecodeStringToJObject(result).Dump(); }運行效果:
封裝優化
之前我是基于System.Reactive庫做的封裝,但C# 9.0已經發布許久,這次我重新基于IAsyncEnumerable寫了一版,這個以C# 9.0作為異步流的基礎,擴展可以用System.Linq.Async,從而獲得與正常的LINQ完全一致的體驗,核心代碼如下:
public?class?DouyuBarrage {static?HttpClient?http?=?new?HttpClient();public?static?async?IAsyncEnumerable<string>?RawFromUrl(string?url,?[EnumeratorCancellation]?CancellationToken?cancellationToken?=?default){HttpResponseMessage?html?=?await?http.GetAsync(url,?cancellationToken);var?roomId?=?Regex.Match(await?html.Content.ReadAsStringAsync(),?@"\$ROOM.room_id[?]?=[?]?(\d+);").Groups[1].Value;using?var?ws?=?new?ClientWebSocket();ws.Options.AddSubProtocol("-");await?ws.ConnectAsync(new?Uri("wss://danmuproxy.douyu.com:8506/"),?cancellationToken);await?MsgTool.LoginAsync(ws,?roomId,?cancellationToken);await?MsgTool.JoinGroupAsync(ws,?roomId,?cancellationToken);var?task?=?Task.Run(async?()?=>{while?(!cancellationToken.IsCancellationRequested){await?MsgTool.SendTick(ws,?cancellationToken);await?Task.Delay(45000,?cancellationToken);}},?cancellationToken);while?(ws.State?==?WebSocketState.Open?&&?!cancellationToken.IsCancellationRequested){yield?return?await?MsgTool.RecieveAsync(ws,?cancellationToken);}GC.KeepAlive(task);await?MsgTool.Logout(ws,?cancellationToken);}public?static?IAsyncEnumerable<JToken>?JObjectFromUrl(string?url)?=>?RawFromUrl(url).Select(MsgTool.DecodeStringToJObject);public?static?IAsyncEnumerable<Barrage>?ChatMessageFromUrl(string?url)?=>?JObjectFromUrl(url).Where(x?=>?x["type"].Value<string>()?==?"chatmsg").Select(Barrage.FromJToken); }見最后兩個方法JObjectFromUrl、ChatMessageFromUrl,基于IAsyncEnumerable,可以獲得與LINQ、System.Reactive完全一致的開發體驗,一行代碼即可完成異步流的篩選、數據轉換。
說在最后
以上所有的完整代碼和示例,都已經上傳到我的博客專用Github倉庫,各位可以自行前往下載:https://github.com/sdcb/blog-data/tree/master/2021/20191011-douyu-barrage-with-dotnet
喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】
DotNet騷操作總結
以上是生活随笔為你收集整理的.NET斗鱼直播弹幕客户端(2021)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 公司高层要我转Java 我直接邮件回怼.
- 下一篇: Exceptionless服务端本地化部