.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消息通 ...
随机推荐
- day108:MoFang:首页检测用户是否登录&在项目中使用MongoDB&用户页面更新用户信息&交易密码界面实现
目录 1.首页页面也要检测用户是否登录 2.在flask中使用MongoDB 3.用户页面更新用户信息 4.交易密码界面/密码修改界面/昵称修改界面初始化 5.交易密码实现 1.首页页面也要检测用户是 ...
- django 初始化项目 和modelviewset 使用
django初始化项目 1.初始化项目结构└─shiyanlou_project │ .gitignore│ README.en.md # 英文│ README.md # 中文项目简介│├─celer ...
- 五、Jmeter的目录结构
进入安装Jmeter可以看到路径 bin目录 jmeter.bat windows的启动文件 jmeter.log jmeter运行日志文件 jmeter.sh linux的启动文件 jmeter. ...
- pandas的学习8-pandas-plot出图
import pandas as pd import numpy as np import matplotlib.pyplot as plt ''' 这次我们讲如何将数据可视化. 首先import我们 ...
- grafana 的主体架构是如何设计的?
grafana 的主体架构是如何设计的? grafana 是非常强大的可视化项目,它最早从 kibana 生成出来,渐渐也已经形成了自己的生态了.研究完 grafana 生态之后,只有一句话:可视化 ...
- windows 任何软件出现异常有日志 w3wp.exe [10608]中发生了未处理的Microsoft .Net Framework异常
右键我的电脑 管理
- 使用js方法将table表格中指定列指定行中相同内容的单元格进行合并操作。
前言 使用js方法对html中的table表格进行单元格的行列合并操作. 网上执行此操作的实例方法有很多,但根据实际业务的区别,大多不适用. 所以在网上各位大神写的方法的基础上进行了部分修改以适合自己 ...
- Windows无法访问共享文件夹
问题描述 今天打开vss连接代码,提示如下信息 解决办法 可行:重置登录用户信息 原博文 https://zhidao.baidu.com/question/1174230805440255699.h ...
- 【Windows系统常用命令集合】
查看建立的TCP连接:netstat -n 查看建立的TCP连接的进程:netstat -nb 查看本机侦听的端口: netstat -an (说明:如果端口没有侦听 检查服务) 测试到远程计算机的某 ...
- vs2010新特性
下面列出了一些新的功能:1.代码编辑器新的代码编辑器使代码更易于阅读.可以通过按 CTRL 并滚动鼠标轮放大文本.此外,单击 Visual C# 或 Visual Basic 中的符号时该符号的所有实 ...