.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消息通 ...
随机推荐
- EasyX 简易绘图工具接口整理
EasyX Library for C++ (Ver:20190415(beta)) http://www.easyx.cn EasyX.h 1 #pragma once 2 3 #ifndef ...
- JDK下载与安装
Java有很多个版本,最新的版本会兼容之前的. 先附上下载地址:https://www.oracle.com/technetwork/java/javase/downloads/jdk8-downlo ...
- 08-flask-使用pymysql
代码 from flask import Flask from flask import render_template import pymysql # 创建flask对象 app = Flask( ...
- 谷歌学术: but your computer or network may be sending automated queries. To protect our users, we can't process your request right now. See Google Help for more information.
原因是屏蔽了日本和新加坡的服务器,切换服务器为其他地方即可
- [日常摸鱼]bzoj2875[NOI2012]随机数生成器-矩阵快速幂
好裸的矩阵快速幂-然而我一开始居然构造不出矩阵- 平常两个的情况都是拿相邻两项放在矩阵里拿去递推的-然后我就一直构造不出来-其实把矩阵下面弄成1就好了啊orz #include<cstdio&g ...
- MySQL优化索引
1. MySQL如何使用索引 索引用于快速查找具有特定列值的行.如果没有索引,MySQL必须从第一行开始,然后遍历整个表以找到相关的行.表越大,花费越多.如果表中有相关列的索引,MySQL可以快速确 ...
- 容器编排系统K8s之Volume的基础使用
前文我们聊到了k8s上的ingress资源相关话题,回顾请参考:https://www.cnblogs.com/qiuhom-1874/p/14167581.html:今天们来聊一下k8s上volum ...
- 用git上传项目到GitHub或者码云全过程
用git上传项目到GitHub或者码云全过程 1. 会生成一个隐藏文件夹".git".这是一个不可删文件,因为暂存区和历史区还有一些其他的信息都在这里,删掉就不是一个完整的仓库了 ...
- Android基础工具移植说明
早前开展的计划因各种杂事而泡汤,而当遇到了具体任务后,在压力下花了两个多周的业余时间把这件事完成了. 这就是我的引以为傲的Mercury-Project,它的核心目标是移植一些Android底层轮子到 ...
- java中使用IO流将以文件中的内容去取到指定的文件中
public class Demo12 { public static void main(String[] args) throws IOException { File file=new File ...