广东建设行业招聘 什么网站,网页平台做个业务推广,长沙网站seo源头厂家,什么是网络营销的tgi值.NET斗鱼直播弹幕客户端(2021)离之前更新的两篇《.NET斗鱼直播弹幕客户端》已经有一段时间#xff0c;近期有许多客户向我反馈刚好有这方面的需求#xff0c;但之前的代码不能用了——但网上许多流传的Node.js、Python脚本却可以用#xff0c;这岂能忍#xff1f;#xff… .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这是第三方包。现在我们尽量不用第三方包官方提供的WebSocket客户端叫System.Net.WebSockets.ClientWebSocket同时支持.NET 4.5和.NET Core。按正常的思路我们会这样写return Observable.Createstring(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.linqusing var ws new ClientWebSocket();
ws.Options.AddSubProtocol(-);
await ws.ConnectAsync(new Uri(wss://danmuproxy.douyu.com:8506), QueryCancelToken);
await ws.SendAsync(SerializeDouyu($typeloginreq/roomid74751/ver20190610/), WebSocketMessageType.Binary, false, QueryCancelToken);
await ws.SendAsync(SerializeDouyu($typejoingroup/rid74751/gid-9999/), WebSocketMessageType.Binary, false, QueryCancelToken);
_ Task.Run(async ()
{while (!QueryCancelToken.IsCancellationRequested){await Task.Delay(45000, QueryCancelToken);await ws.SendAsync(SerializeDouyu($typemrkl/), WebSocketMessageType.Binary, false, QueryCancelToken);}
});while (!QueryCancelToken.IsCancellationRequested)
{var buffer new byte[4096];WebSocketReceiveResult r await ws.ReceiveAsync(buffer, QueryCancelToken);string result DeserializeDouyu(new Memorybyte(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 IAsyncEnumerablestring 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 IAsyncEnumerableJToken JObjectFromUrl(string url) RawFromUrl(url).Select(MsgTool.DecodeStringToJObject);public static IAsyncEnumerableBarrage ChatMessageFromUrl(string url) JObjectFromUrl(url).Where(x x[type].Valuestring() 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骚操作