.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,这是第三方包。现在我们尽量不用第三方包,官方提供的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-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骚操作】

.NET斗鱼直播弹幕客户端(2021)的更多相关文章
- .NET斗鱼直播弹幕客户端(下)
.NET斗鱼直播弹幕客户端(下) 在上篇文章中,我们提到了如何使用.NET连接斗鱼TV直播弹幕的基本操作.然而想要做得好,做得容易扩展,就需要做进一步的代码整理. 本文将涉及以下内容: 介绍如何使用R ...
- .NET斗鱼直播弹幕客户端(上)
现在直播平台由于弹幕的存在,主播与观众可以更轻松地进行互动,非常受年轻群众的欢迎.斗鱼TV就是一款非常流行的直播平台,弹幕更是非常火爆.看到有不少主播接入弹幕语音播报器.弹幕点歌等模块,这都需要首先连 ...
- android文件管理器源码、斗鱼直播源码、企业级erp源码等
Android精选源码 文件清理管理器 自定义水平带数字的进度条以及自定义圆形带数字的进度条 利用sectionedRecyclerViewAdapter实现分组列表的recyclerView源码 流 ...
- Android Studio 直播弹幕
我只是搬运:https://blog.csdn.net/HighForehead/article/details/55520199 写的很好很详细,挺有参考价值的 demo直通车:https://do ...
- 使用Selenium模拟浏览器抓取斗鱼直播间信息
获取斗鱼直播间每个房间的名称.观看人数.tag.主播名字 代码: import time from multiprocessing import Pool from selenium import w ...
- Python爬虫实例(二)使用selenium抓取斗鱼直播平台数据
程序说明:抓取斗鱼直播平台的直播房间号及其观众人数,最后统计出某一时刻的总直播人数和总观众人数. 过程分析: 一.进入斗鱼首页http://www.douyu.com/directory/all 进入 ...
- ubuntu下使用OBS开斗鱼直播
系统环境:ubuntu 15.10,OBS Studio 0.13.1 OBS是可以在linux,windows,mac下直播的开源软件,官方地址:https://obsproject.com/ 斗鱼 ...
- Scrapy项目 - 实现斗鱼直播网站信息爬取的爬虫设计
要求编写的程序可爬取斗鱼直播网站上的直播信息,如:房间数,直播类别和人气等.熟悉掌握基本的网页和url分析,同时能灵活使用Xmind工具对Python爬虫程序(网络爬虫)流程图进行分析. 一.项目 ...
- 直播弹幕抓取逆向分析流程总结 websocket,flash
前端无秘密 直播的逆向抓取说到底是前端的调试和逆向技术,加上部分的dpa(深入包分析,个人能力尚作不到深入,只能作简单分析)难度较低 目前互联网直播弹幕主要是两种技术实现. 1websocket消息通 ...
随机推荐
- SseEmitter推送
后端代码SseController.java package com.theorydance.mywebsocket.server; import java.util.HashMap; import ...
- 实验:非GTID 级联复制架构变为一主多从
- linux下postgresql安装
1.下载地址 https://www.enterprisedb.com/download-postgresql-binaries 2.创建用户并修改密码 [root@node01 ~]# userad ...
- json 注释
一.背景 今天聊个小东西,Json的的的注释.Json十分常见,大家用的很多,在语法上,规范的Json要求,文件里不可以写注释.原因呢,我调查了一下: I removed comments from ...
- scala安装遇到的问题:[ERROR] Failed to construct terminal; falling back to unsupported java.lang.NumberFormatException: For input string: "0x100"
安装scala时遇到的问题 lion@king:/opt$ scala Welcome to Scala 2.11.12 (Java HotSpot(TM) 64-Bit Server VM, Jav ...
- get \post 接口代码及断言编写
post 请求接口 import requests import json url_path = "http://www.baidu.com" data = {"user ...
- Spring Data JPA的基本学习之了解
Spring Data JPA 是 什 么 可以理解为JPA规范的再次封装抽象,底层还是使用了Hibernate的JPA技术实现,引用JPQL(Java Persistence Query Langu ...
- 微服务之服务治理_Eureka
首先需要明确,不管是什么事物需要"治理",那一定是该事物存在一定问题.比如环境治理.那么服务,或者说微服务为什么需要治理?对于服务来说,如果它承担的业务职责简单,那其实治理的必要性 ...
- 请收好 Linux 网络命令集锦
看着kali linux上百个网络命令,我陷入了沉思.专业的网络命令实在是太多了,如果要罗列,上千个也是有的.个人不是渗透测试工作者,大部分功能只知皮毛.所以本文是非常浅显的技术总结,仅聚焦工作中常用 ...
- CountDownLatch深度剖析
场景引入 日常开发中,有个需求,要求主线程开启多个线程去并行执行任务,并且主线程需要等待所有的子线程执行完成后进行汇总.我们很容易找到 jion()方法来实现这个功能 缺点:由于工作中,我们不会直接创 ...