.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#section-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骚操作】

.NET斗鱼直播弹幕客户端(2021)的更多相关文章

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

    .NET斗鱼直播弹幕客户端(下) 在上篇文章中,我们提到了如何使用.NET连接斗鱼TV直播弹幕的基本操作.然而想要做得好,做得容易扩展,就需要做进一步的代码整理. 本文将涉及以下内容: 介绍如何使用R ...

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

    现在直播平台由于弹幕的存在,主播与观众可以更轻松地进行互动,非常受年轻群众的欢迎.斗鱼TV就是一款非常流行的直播平台,弹幕更是非常火爆.看到有不少主播接入弹幕语音播报器.弹幕点歌等模块,这都需要首先连 ...

  3. android文件管理器源码、斗鱼直播源码、企业级erp源码等

    Android精选源码 文件清理管理器 自定义水平带数字的进度条以及自定义圆形带数字的进度条 利用sectionedRecyclerViewAdapter实现分组列表的recyclerView源码 流 ...

  4. Android Studio 直播弹幕

    我只是搬运:https://blog.csdn.net/HighForehead/article/details/55520199 写的很好很详细,挺有参考价值的 demo直通车:https://do ...

  5. 使用Selenium模拟浏览器抓取斗鱼直播间信息

    获取斗鱼直播间每个房间的名称.观看人数.tag.主播名字 代码: import time from multiprocessing import Pool from selenium import w ...

  6. Python爬虫实例(二)使用selenium抓取斗鱼直播平台数据

    程序说明:抓取斗鱼直播平台的直播房间号及其观众人数,最后统计出某一时刻的总直播人数和总观众人数. 过程分析: 一.进入斗鱼首页http://www.douyu.com/directory/all 进入 ...

  7. ubuntu下使用OBS开斗鱼直播

    系统环境:ubuntu 15.10,OBS Studio 0.13.1 OBS是可以在linux,windows,mac下直播的开源软件,官方地址:https://obsproject.com/ 斗鱼 ...

  8. Scrapy项目 - 实现斗鱼直播网站信息爬取的爬虫设计

    要求编写的程序可爬取斗鱼直播网站上的直播信息,如:房间数,直播类别和人气等.熟悉掌握基本的网页和url分析,同时能灵活使用Xmind工具对Python爬虫程序(网络爬虫)流程图进行分析.   一.项目 ...

  9. 直播弹幕抓取逆向分析流程总结 websocket,flash

    前端无秘密 直播的逆向抓取说到底是前端的调试和逆向技术,加上部分的dpa(深入包分析,个人能力尚作不到深入,只能作简单分析)难度较低 目前互联网直播弹幕主要是两种技术实现. 1websocket消息通 ...

随机推荐

  1. 数据结构,哈希表hash设计实验

    数据结构实验,hash表 采用链地址法处理hash冲突 代码全部自己写,转载请留本文连接, 附上代码 #include<stdlib.h> #include<stdio.h> ...

  2. js上 三、数据类型

    3.1.什么是数据类型 a. 什么是数据类型? 想从生活中出发: 考验智商的时刻到了: 1(只)+1(只)=1(双) 3(天)+4(天)=1(周) 5(月)+7(月)=1(年) 4(时)+9(时)=1 ...

  3. 使用BulkLoad恢复hbase数据

    问题: hbase 集群启动不了,maste一直在初始化,数据面临丢失风险. 解决: 把hbfs上 /hbase 目录移走 改名为/hbase-bak 删除zk上的数据,重新建立一个新的hbase集群 ...

  4. AddressSanitizer —— ASAN分析内存错误

    简介 AddressSanitizer 是一个性能非常好的C/C++ 内存错误探测工具. 它由编译器的插桩模块和替换了malloc函数的运行时库组成. 这个工具可以探测如下这些类型的错误: 对堆.栈和 ...

  5. day021|python之面向对象进阶1

    面向对象进阶 目录 面向对象进阶 1 继承 1.1 继承入门 1.1.1 继承基础 1.1.2 类的基本使用 1.2 多继承 1.2.1 多继承的基本使用 1.2.2 多继承以后的重复性 1.3 类的 ...

  6. Redis基础篇(四)持久化:内存快照(RDB)

    AOF好处是每次执行只需要记录操作命令,记录量不大.但在故障恢复时,需要逐一执行AOF的操作命令,如果日志很大,恢复就很慢. 今天学习另一种持久化方式:内存快照.内存快照,是Redis某一时刻的状态, ...

  7. 在 WSL Ubuntu 上使用 .NET 进行跨平台开发新手入门

    翻译自 haydenb 2020年6月3日的文章<Getting started with cross-platform development using .NET on Ubuntu on ...

  8. Tensorflow2.0-mnist手写数字识别示例

    Tensorflow2.0-mnist手写数字识别示例   读书不觉春已深,一寸光阴一寸金. 简介:通过CNN 卷积神经网络训练后识别出手写图片,测试图片mnist数据集中的0.1.2.4.     ...

  9. springmvc 统一处理异常

    1.自定义统一异常处理器 自定义Exception实现 HandlerExceptionResolver接口或继承AbstractHandlerExceptionResolver类 1.实现接口Han ...

  10. Thread.join详解

    /** * 如果某个线程在另一个线程t上调用t.join:那么此线程将被挂起,直到目标t线程的结束才恢复即t.isAlive返回为假 * * @date:2018年6月27日 * @author:zh ...