.NET斗鱼直播弹幕客户端(2021)

.NET斗鱼直播弹幕客户端(2021)

离之前更新的两篇《.NET斗鱼直播弹幕客户端》已经有一段时间,近期有许多客户向我反馈刚好有这方面的需求,但之前的代码不能用了——但网上许多流传的Node.jsPython脚本却可以用,这岂能忍?(刚好我终于找回了我的发布密码????)因此我有动力重新对此进行好(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.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-KeySec-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);
}

见最后两个方法JObjectFromUrlChatMessageFromUrl,基于IAsyncEnumerable,可以获得与LINQSystem.Reactive完全一致的开发体验,一行代码即可完成异步流的筛选、数据转换。

说在最后

以上所有的完整代码和示例,都已经上传到我的博客专用Github仓库,各位可以自行前往下载:https://github.com/sdcb/blog-data/tree/master/2021/20191011-douyu-barrage-with-dotnet

喜欢的朋友 请关注我的微信公众号:【DotNet骚操作】

DotNet骚操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/305689.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

McAfee推免费版SiteAdvisor安全上网工具

McAfee近期宣布推出完全免费版McAfee SiteAdvisor上网工具。McAfee SiteAdvisor是业界第一款Web安全工具&#xff0c;能够主动地提醒用户在浏览、搜索和即时通信或收发电子邮件时所遇到的危险站点&#xff0c;避免遭到网络钓鱼、间谍软件等恶意程序的***。 McAfee SiteAdvisor是…

9050 端口 linux 进程,windows和linux查看端口占用情况

一、Windows平台在windows命令行窗口下执行&#xff1a;1.查看所有的端口占用情况C:\>netstat -ano协议 本地地址 外部地址 状态 PIDTCP 127.0.0.1:1434 0.0.0.0:0 LISTENING 3236TC…

Exceptionless服务端本地化部署

背景分布式异常日志收集框架Exceptionless是开源的工具&#xff0c;根据官方给出的说明&#xff1a;Exceptionless提供两种使用方式&#xff0c;一种是官网创建账号,需要付费&#xff0c;免费版有限制&#xff1b;一种是自己搭建本地项目&#xff0c;无任何限制。准备安装包准备…

微软所谓的无人工介入的自动的机器翻译系统

近日在微软网站上查找资料,发现一个资料里有如下的声明性描述:注意&#xff1a;这篇文章是由无人工介入的自动的机器翻译系统翻译完成。这些文章是微软为不懂英语的用户提供的, 以使他们能够理解这些文章的内容。微软不保证机器翻译的正确度&#xff0c;也不对由于内容的误译或…

ABP vnext模块化架构的最佳实践的实现

在上一篇文章《手把手教你用Abp vnext构建API接口服务》中&#xff0c;我们用ABP vnext实现了WebAPI接口服务&#xff0c;但是并非ABP模块化架构的最佳实践。我本身也在学习ABP&#xff0c;我认为ABP新手应该从最佳实践开始学习&#xff0c;可以少走很多弯路&#xff0c;所以写…

WebCast学习链接

全部下载列表1. C#面向对象设计模式纵横谈系列课程 讲师&#xff1a;李建忠 上海祝成信息科技有限公司 高级培训讲师 MSDN特邀讲师2. ASP.NET AJAX深入浅出系列课程 讲师: 老赵 课程(1)&#xff1a;ASP.NET AJAX 概述 课程(2)&#xff1a;UpdatePanel的使用(…

linux删除第二次出现的字符,linux下 怎样删除文件名中包含特殊字符的文件

目录中无意间出现了 -- 这个文件[rootdev tmp]# ls-- 00 01 02 03 04 05 06 07 08 09[rootdev tmp]# lltotal 0-rw-r--r-- 1 root root 0 Oct 23 15:31 ---rw-r--r-- 1 root root 0 Oct 23 15:37 00-rw-r--r-- 1 root root 0 Oct 23 15:37 01-rw-r--r-- 1 root root 0 Oct 23 1…

全球知名跨境电商,.Net软件工程师招聘,约么?

公司&#xff1a;际客国际电子商务有限公司&#xff0c;网址&#xff1a;http://geekbuy.cn/工作地点&#xff1a;深圳市龙岗区五和大道南雅宝路1号星河WORLD B座岗位职责&#xff1a;中级开发工程师。薪资待遇&#xff1a;10K-20K&#xff0c;具体面议。 职位要求&#x…

被关起来日子的流水帐

2007年3月19日 老上号经过3个半小时的颠簸&#xff0c;在晚上7&#xff1a;30到达了哈尔滨&#xff0c;还是住在黑龙江大学旁边的学府宾馆&#xff0c;由于是在学校的边上&#xff0c;相对来说比较安全&#xff0c;宾馆的条件已经不如以前了&#xff0c;稍微显得有点陈旧&#…

krc 编辑 linux,Linux网络编程

6 berkeley - 145 -struct in_addr {unsigned long s_addr;};ina struct sockaddr_instruct in_addrina.sin_addr.s_addr 4 IP4 IP #defines6.5.21IP2h to nto Network Short Hosts H-to-n-s htons()68000n h to s l IPstolh() Short to Long Host?l htons() “Host to Networ…

iphone查看删除的短信_手机资讯:iPhone手机可以批量删除短信吗如何操作

如今使用IT数码设备的小伙伴们是越来越多了&#xff0c;那么IT数码设备当中是有很多知识的&#xff0c;这些知识很多小伙伴一般都是不知道的&#xff0c;就好比最近就有很多小伙伴们想要知道iPhone手机可以批量删除短信吗如何操作&#xff0c;那么既然现在大家对于iPhone手机可…

如何在 C# 8 中使用 模式匹配

模式匹配 是在 C# 7 中引入的一个非常????的特性&#xff0c;你可以在任何类型上使用 模式匹配&#xff0c;甚至是自定义类型&#xff0c;而且在 C# 8 中得到了增强&#xff0c;引入了大量的新模式类型&#xff0c;这篇文章就来讨论如何在 C# 8 中使用模式匹配。C# 8 中的表…

linux下找不到libc 库,Linux-覆盖libc open()库函数

我在库&中有glibc提供的相同的覆盖open().我首先在库中设置了LD_PRELOAD,因此当进程调用open()时,将调用库中定义的open.问题&#xff1a;-glibc中还有其他几个函数,一旦示例为getpt(),就会调用open(),当getpt()调用open()时,将调用glibc中定义的open(),我将如何使getpt ()…

Hadoop 中zoo_0基础如何入门HADOOP

原标题&#xff1a;0基础如何入门HADOOP学习一样东西&#xff0c;肯定先要了解这个东西是什么&#xff0c;那什么是HADOOP呢&#xff1f;我们就来看看什么是HADOOP和如何学习HADOOP及学习内容。一&#xff0c;什么是HADOOPHADOOP是apache旗下的一套开源软件平台HADOOP提供的功能…

.NET 5 程序高级调试-WinDbg

上周和大家分享了.NET 5开源工作流框架elsa&#xff0c;程序跑起来后&#xff0c;想看一下后台线程的执行情况。抓了个进程Dump后&#xff0c;使用WinDbg调试&#xff0c;加载SOS调试器扩展&#xff0c;结果无法正常使用了&#xff1a;0:000> .loadby sos clrUnable to find…

精妙Sql语句

1&#xff0e; 判断a表中有而b表中没有的记录 select a.* from tbl1 a left join tbl2 b on a.key b.key where b.key is null 虽然使用in也可以实现&#xff0c;但是这种方法的效率更高一些 2&#xff0e; 新建一个与某个表相同结构的表 select * into b from a where 1&l…

.Net在线编辑工具.NET Fiddle

介绍推荐工具&#xff1a;.NET Fiddle推荐理由&#xff1a;在线调试&#xff0c;编译&#xff0c;运行.net代码&#xff0c;同时支持C#&#xff0c;VB.NET&#xff0c;F#推荐说明&#xff1a;&#xff1a;对于.NET开发者来说是福音&#xff0c;因为我们可以不用再担心环境与庞大…

Typora markdown公式换行等号对齐_下了31个markdown编辑器,我就不信选不出一个好用的...

markdown编辑器测评标准总体标准渲染领域编辑领域数据管理其他TyporaVnoteMwebJoplinZettlrmacdownulyssesMarktextghostwriterfocusedbywordmarkedFarBoxNotablebear(熊掌笔记)iA writerMarxico(马克飞象)JetBrains系列的IDEsublime&#xff08;贫穷&#xff0c;没有插件&…

WSUS专题之二:部署与规划1

部署场景: 我们这里仅讨论和Internet完全物理隔离的企业内网的WSUS部署 Internet断开的WSUS服务器环境 部署WSUS服务时&#xff0c;并不要求你必须连接到Internet。对于没有连接到Internet的网络环境&#xff0c;你一样可以部署WSUS服务。通过在其他连接到Internet上的WSUS服务…

linux gcc出错,编译arm-linux-gcc出错

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼在ubuntu12.04下编译arm的交叉编译工具链执行../gcc-4.6.0/configure --target$TARGET --prefix$PREFIX --without-headers --enable-languagesc --disable-threads --with-newlib --disable-shared --disable-libmudflap --disabl…