技术标签: python java web http redis
离之前更新的两篇《.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#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-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骚操作】
文章浏览阅读375次。<script src="js/jquery-1.8.3.min.js" type="text/javascript"></script> <script type="text/javascript"> function checkName() { var name = document.getElementB..._jsp前端页面将表单是否提交成功作为限制条件
文章浏览阅读1k次。Sequence Numberlzyws7393074532892018-04-25Number Sequenceqq_391789932452017-09-21理解TCP序列号(Sequence Number)和确认号(Acknowledgment Number)hebbely9822017-01-14Number Sequence(规律)l25336363712902017-07-18Numb..._ack num
文章浏览阅读5.9k次。笔记本电脑怎么进CMOS密码巧设置笔记本电脑怎么进CMOS密码巧设置 笔记本电脑为了保护用户的数据安全,往往采用加密的方式,最常见的还是CMOS密码加密技术。为了让你的重要数据更加安全,你可能需要设置不同的密码,这也就要求你记住许多密码。对于笔记本电脑用户来说,真的需要设置一道道密码关卡吗?非也非也! 一、认识与设置笔记本电脑的CMOS密码 笔记本电脑的CMOS密码大致分为超级密码(Supervi..._电脑第一道密码修改
文章浏览阅读2.5k次,点赞2次,收藏5次。迟到的文章,就当库存发出来吧~_jangow01
文章浏览阅读1.7w次,点赞2次,收藏5次。默认情况下RDD的transformation是lazy形式,实际计算只有在ation时才会进行,而且rdd的计算结果默认都是临时的,用过即丢弃,每个action都会触发整个DAG的从头开始计算,因此在迭代计算时都会想到用cache或persist进结果进行缓存。敝人看到很多资料或书籍有的说是persist或cache会触发transformation真正执行计算,有的说是不会!敝人亲自实验了一把..._spark cache和persist不生效
文章浏览阅读2.4k次。HTML之marquee(文字滚动)详解语法:以下是一个最简单的例子:代码如下:Hello, World下面这两个事件经常用到:onMouseOut=“this.start()” :用来设置鼠标移出该区域时继续滚动onMouseOver=“this.stop()”:用来设置鼠标移入该区域时停止滚动代码如下:onMouseOut=“this.start()” :用来设置鼠标移出该区域时继续滚动 onMouseOver=“this.stop()”:用来设置鼠标移入该区域时停止滚动这是一个完_html滚动
文章浏览阅读637次。树莓派的GPIO操作被抽象为文件读写,下面以一个例子来说明GPIO操作。_树莓派怎么读取gpio口上的信息
文章浏览阅读1.7k次。QNX是一种商用的遵从POSIX规范的类Unix实时操作系统,目标市场主要是面向嵌入式系统。它可能是最成功的微内核操作系统之一。QNX是一种商用的类Unix实时操作系统,遵从POSⅨ规范,目标市场主要是嵌入式系统[1]。QNX成立于1980年,是加拿大一家知名的嵌入式系统开发商。QNX的应用范围极广,包含了:控制保时捷跑车的音乐和媒体功能、核电站和美国陆军无人驾驶Crusher坦克的控制系统[2],还有RIM公司的BlackBerry PlayBook平板电脑。_车机qnx虚拟化软件系统架构
文章浏览阅读1k次,点赞20次,收藏22次。代码功能:信号发生器设计信号发生器由波形选择开关控制波形的输出,分别能输出正弦波、方汉和三角波三种波形,波形的周期为2秒(由40M有源晶振分频控制)。考虑程序的容量,每种波形在一个周期内均取16个取样点,每个样点数据是8位(数值范围:00000000~1111111)要求将D/A变换前的8位二进数据(以十进制方式)输出到数码管动态演示出来。_vhdl正弦波信号发生器
文章浏览阅读629次。Java Concurrency in Practice中对线程安全的定义:当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。显然只有资源竞争时才会导致线程不安全,因此无状态对象永远是线程安全的 。过多的同步会产生死锁的问题,死锁属于程序运行的时_java 线程概述
文章浏览阅读1.2w次,点赞10次,收藏61次。读取表单Sheet2中部分信息。_matlab读取数据
文章浏览阅读1.4w次。最近项目中用到ItemBased Collaborative Filtering,实践过spark mllib中的ALS,但是因为其中涉及到降维操作,大数据量的计算实在不能恭维。所以自己实践实现基于spark的分布式cf,已经做了部分优化。目测运行效率还不错。以下代码package modelimport org.apache.spark.broadcast.Broadcastimp_spark cf