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

前言

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

经常看到其它编程语言的开发者,分享了他们斗鱼弹幕客户端的代码。.NET当然也能做,还能做得更好(只是不知为何很少见人分享?)。

本文将包含以下内容:

  1. 我将使用斗鱼TV官方公开的弹幕PDF文档,使用 SocketTcpClient连续斗鱼弹幕;

  2. 分析如何利用 .NET强大的 ValueTask特性,在保持代码简洁的同时,轻松享受高性能异步代码的快乐;

  3. 然后将使用 ReactiveExtensions( RX),演示如何将一系列复杂的弹幕接入操作,就像写 HelloWorld一般容易;

  4. 用我自制的“准游戏引擎” FlysEngine,只需少量代码,即可将斗鱼TV的弹幕显示左右飞过的效果;

本文内容可能比较多,因此分上、下两篇阐述,上篇将具体聊聊第1、2点,第3、4点将在下篇进行,整篇完成后,最终效果如下:

640?wx_fmt=gif

斗鱼直播API

现在网上可以轻松找到 斗鱼弹幕服务器第三方接入协议v1.6.2.pdf(网上搜索该关键字即可找到)。文档提到,第三方接入弹幕服务的服务器为 openbarrage.douyutv.com:8601,我们可以使用 TcpClient来方便连接:

using (var client = new TcpClient())	
{	client.ConnectAsync("openbarrage.douyutv.com", 8601).Wait();	Stream stream = client.GetStream();	// do other works	
}

该文档中提到所有数据包格式如下:

640?wx_fmt=png

注意前两个4字节的消息长度是完全一样的,可以使用 Debug.Assert进行断言。

其中所有数字都为小端整数,刚好 .NETBinaryWriter类默认都以小端整数进行转换。可以利用起来。

因此,读取一个消息包的完整代码如下:

using (var reader = new BinaryReader(stream, Encoding.UTF8, true))	
{	var fullMsgLength = reader.ReadInt32();	var fullMsgLength2 = reader.ReadInt32();	Debug.Assert(fullMsgLength == fullMsgLength2);	var length = fullMsgLength - 1 - 4 - 4;	var packType = reader.ReadInt16();	Debug.Assert(packType == ServerSendToClient);	var encrypted = reader.ReadByte();	Debug.Assert(encrypted == Encrypted);	var reserved = reader.ReadByte();	Debug.Assert(reserved == Reserved);	var bytes = reader.ReadBytes(length);	var zero = reader.ReadByte();	Debug.Assert(zero == ByteZero);	
}

其中 bytes既是数据部分,根据 pdf文档中的规定,该部分为 UTF-8编码,在 C#中使用 Encoding.UTF8.GetString()即可获取其字符串,该字符串长这样子:

type@=chatmsg/rid@=633019/ct@=1/uid@=124155/nn@=夜科扬羽/txt@=这不压个蜥蜴/cid@=602c7f1becf2419962a6520300000000/ic@=avatar@S000@S12@S41@S55_avatar/level@=21/sahf@=0/cst@=1570891500125/bnn@=賊开心/bl@=8/brid@=5789561/hc@=21ebd5b2c86c01e0565453e45f14ca5b/el@=/lk@=/urlev@=10/ 

该格式不是 JSON/ XML等,但仔细分析又确实有逻辑,有层次感,根据文档,该格式为所谓的 STT序列化,该格式包含键值对、数组等多种格式。虽然不懂为什么不用 JSON。还好协议简单,我可以通过寥寥几行代码,即可转换为 Json.NETJToken格式:

public static JToken DecodeStringToJObject(string str)	
{	if (str.Contains("//")) // 数组	{	var result = new JArray();	foreach (var field in str.Split(new[] { "//" }, StringSplitOptions.RemoveEmptyEntries))	{	result.Add(DecodeStringToJObject(field));	}	return result;	}	if (str.Contains("@=")) // 对象	{	var result = new JObject();	foreach (var field in str.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries))	{	var tokens = field.Split(new[] { "@=" }, StringSplitOptions.None);	var k = tokens[0];	var v = UnscapeSlashAt(tokens[1]);	result[k] = DecodeStringToJObject(v);	}	return result;	}	else if (str.Contains("@A=")) // 键值对	{	return DecodeStringToJObject(UnscapeSlashAt(str));	}	else	{	return UnscapeSlashAt(str); // 值	}	
}	
static string EscapeSlashAt(string str)	
{	return str	.Replace("/", "@S")	.Replace("@", "@A");	
}	
static string UnscapeSlashAt(string str)	
{	return str	.Replace("@S", "/")	.Replace("@A", "@");	
}

这样一来,即可将 STT格式转换为 JSON格式,因此只需像 JSON格式取出 nn字段和 txt字段即可,还有一个 col字段,可以用来确定弹幕颜色,我可以将其转换为 RGBint32值:

Color = (x["col"] ?? new JValue(0)).Value<int>() switch	
{	1 => 0xff0000, // 红	2 => 0x1e87f0, // 浅蓝	3 => 0x7ac84b, // 浅绿	4 => 0xff7f00, // 橙色	5 => 0x9b39f4, // 紫色	6 => 0xff69b4, // 洋红	_ => 0xffffff, // 默认,白色	
}

该代码使用了 C# 8.0switchexpression功能,可以一个表达式转成整个颜色转换,比 if/elseswitch/case语句都精简不少,可谓一气呵成。

支持异步/ ValueTaskMemory<T>优化

C# 5.0提供了强大的异步 API—— async/await,通过异步API,以前难以用编程实现的操作现在可以像写串行代码一样轻松完成,还能轻松加入取消任务操作。

然后 C# 7.0发布了 ValueTaskValueTask是值类型,因此在频繁调用异步操作(如使用 Stream读取字节)时,不会因为创建过多的 Task而分配没必要的内存。这里,我确实是使用 TCP连接流读取字节,是使用 ValueTask的最佳时机。

这里我们将尝试将代码切换为 ValueTask版本。

首先第一个问题是 BinaryReader类,该类提供了便利的字节操作方式,且能确保字节端为小端,但该类不提供异步 API,因此需要作一些特殊处理:

public static async Task<string> RecieveAsync(Stream stream, CancellationToken cancellationToken)	
{	int fullMsgLength = await ReadInt32().ConfigureAwait(false);	int fullMsgLength2 = await ReadInt32().ConfigureAwait(false);	Debug.Assert(fullMsgLength == fullMsgLength2);	int length = fullMsgLength - 1 - 4 - 4;	short packType = await ReadInt16().ConfigureAwait(false);	Debug.Assert(packType == ServerSendToClient);	short encrypted = await ReadByte().ConfigureAwait(false);	Debug.Assert(encrypted == Encrypted);	short reserved = await ReadByte().ConfigureAwait(false);	Debug.Assert(reserved == Reserved);	Memory<byte> bytes = await ReadBytes(length).ConfigureAwait(false);	byte zero = await ReadByte().ConfigureAwait(false);	Debug.Assert(zero == ByteZero);	return Encoding.UTF8.GetString(bytes.Span);	
}

如代码所示,我封装了 ReadInt16()ReadInt32()两个方法,

var intBuffer = new byte[4];	
var int32Buffer = new Memory<byte>(intBuffer, 0, 4);	
async ValueTask<int> ReadInt32()	
{	var memory = int32Buffer;	int read = 0;	while (read < 4)	{	read += await stream.ReadAsync(memory.Slice(read), cancellationToken).ConfigureAwait(false);	}	Debug.Assert(read == memory.Length);	return 	(intBuffer[0] << 0) + 	(intBuffer[1] << 8) +	(intBuffer[2] << 16) + 	(intBuffer[3] << 24);	
}

如图,我还使用了一个 while语句,因为不像 BinaryReader,如果一次无法读取所需的字节数(4个字节), stream.ReadAsync()并不会堵塞线程。然后需要将 int32Buffer转换为 int类型。

注意:此处我没有使用 BitConverter.ToInt32(),也不能使用该方法,因为该方法不像 BinaryReader,它在大端/小端的 CPU上会有不同的行为。(其中在大端 CPU上将有错误的行为)涉及二进制序列化需要传输的,不能使用 BitConverter类。

同样的,写 TCP流也需要有相应的变化:

static async Task SendAsync(Stream stream, byte[] body, CancellationToken cancellationToken)	
{	var buffer = new byte[4];	await stream.WriteAsync(GetBytesI32(4 + 4 + body.Length + 1), cancellationToken).ConfigureAwait(false);	await stream.WriteAsync(GetBytesI32(4 + 4 + body.Length + 1), cancellationToken).ConfigureAwait(false);	await stream.WriteAsync(GetBytesI16(ClientSendToServer), cancellationToken).ConfigureAwait(false);	await stream.WriteAsync(new byte[] { Encrypted}, cancellationToken).ConfigureAwait(false);	await stream.WriteAsync(new byte[] { Reserved}, cancellationToken).ConfigureAwait(false);	await stream.WriteAsync(body, cancellationToken).ConfigureAwait(false);	await stream.WriteAsync(new byte[] { ByteZero}, cancellationToken).ConfigureAwait(false);	Memory<byte> GetBytesI32(int v)	{	buffer[0] = (byte)v;	buffer[1] = (byte)(v >> 8);	buffer[2] = (byte)(v >> 16);	buffer[3] = (byte)(v >> 24);	return new Memory<byte>(buffer, 0, 4);	}	Memory<byte> GetBytesI16(short v)	{	buffer[0] = (byte)v;	buffer[1] = (byte)(v >> 8);;	return new Memory<byte>(buffer, 0, 2);	}	
}

总结

最终运行效果如下:

640?wx_fmt=gif

这一篇【DotNet骚操作】文章介绍了如何使用斗鱼tv开放弹幕 API,下篇将会:

  • 共享本文所使用的所有完整的源代码;

  • 介绍如何使用 ReactiveExtensions( RX),演示这一系列操作用起来,就像写 HelloWorld一样简单;

  • 用我自制的“准游戏引擎” FlysEngine,只需少量代码,即可实现桌面弹幕的效果;

敬请期待!“刷一波666???”

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/313429.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

程序员后期,架构师发展路线!

作者:zollty&#xff0c;资深程序员和架构师&#xff0c;私底下是个爱折腾的技术极客&#xff0c;架构师社区合伙人&#xff01;我总结了3个阶段。先说一下各个阶段的感受&#xff1a;1、系统架构阶段&#xff1a;系统架构实际上包括了 业务功能架构 和 技术功能架构。业务上&a…

YUV格式学习

转载自http://blog.csdn.net/searchsun/article/details/2443867 YUV是指亮度参量和色度参量分开表示的像素格式&#xff0c;而这样分开的好处就是不但可以避免相互干扰&#xff0c;还可以降低色度的采样率而不会对图像质量影响太大。YUV是一个比较笼统地说法&#xff0c;针对它…

Flappy Bird游戏python完整源码

通过pygame实现当年风靡一时的flappy bird小游戏。 当前只设定了同样长度的管道&#xff0c;图片和声音文件自行导入。 效果如下&#xff1a; # -*- coding:utf-8 -*- """ 通过pygame实现曾风靡一时的flappybird游戏。 小鸟x坐标不变&#xff0c;画布左移实现…

【 .NET Core 3.0 】框架之九 || 依赖注入 与 IoC

本文有配套视频&#xff1a;https://www.bilibili.com/video/av58096866/?p5前言1、重要&#xff1a;如果你实现了解耦&#xff0c;也就是 api 层只引用了 IService 和 IRepository 的话&#xff0c;那每次修改 service 层&#xff0c;都需要清理解决方案&#xff0c;重新编译…

10月数据库排行:Microsoft SQL Server分数增加最多

DB-Engines 数据库流行度排行榜 10 月更新已发布&#xff0c;排名前二十如下&#xff1a;这期的数据比较有意思&#xff0c;到了这个月&#xff0c;Microsoft SQL Server 马上扭转局势&#xff0c;成了分数增长最多的一个&#xff0c;与上个月相比其增加了 9.66 分&#xff0c;…

VS Code 1.39 发布!Web 版 VS Code 是否离我们越来越近了?(文末彩蛋)

今天&#xff08;北京时间 2019 年 10 月 10 日&#xff09;&#xff0c;微软发布了 Visual Studio Code 1.39 版本。此版本主要更新的内容包括&#xff1a;Source Control tree view - 可以通过列表或者树状图两种方式来展示被改变的文件。Toggle region folding keyboard sho…

二叉树分析(两点最大距离)

转载自&#xff1a;http://blog.csdn.net/lalor/article/details/7626678 http://blog.csdn.net/lalor/article/details/7618120 把二叉树看成一个图&#xff0c;父子节点之间的连线看成是双向的&#xff0c;我们姑且定义"距离"为两节点之间边的个数。写…

IT从业的迷思与破解之道(更新)

我只是单纯做技术的程序员&#xff0c;什么靠微信广告攒钱这些&#xff0c;跟我没有半毛钱关系&#xff0c;初衷很简单&#xff0c;只重视正三观的正确技术知识分享在这到处都是线上培训&#xff0c;付费知识的社群里&#xff0c;随便搜个词都有您想要的内容哪轮到我们。技术的…

graphcut 用于最优缝合先寻找_Image Stitching

Graphcut 求解最佳缝合线&#xff1a; 主要参照硕士学位论文《基于不同视点样图的图像修复》 Graphcut 主要参照&#xff1a; http://blog.csdn.net/zouxy09/article/details/8532111 Graph cuts是一种十分有用和流行的能量优化算法&#xff0c;在计算机视觉领域普遍应用于…

.netcore 开发的 iNeuOS 物联网平台部署在 Ubuntu 操作系统,无缝跨平台

1. 概述参见前两篇文章&#xff1a;《iNeuOS完全使用.netcore开发&#xff0c;主要为企业、集成商打造从网关、边缘应用、云端建设的物联网/工业互联网平台产品级解决方案。面向应用场景&#xff1a;&#xff08;1&#xff09;嵌入式硬件网关的开发和部署&#xff0c;形成自己…

按照前序遍历和中序遍历构建二叉树

转载自&#xff1a;http://blog.csdn.net/sbitswc/article/details/26433051 Given preorder and inorder traversal of a tree, construct the binary tree. Note: You may assume that duplicates do not exist in the tree. There is an example._______7______/ …

刷新:重新发现.NET与未来

是新朋友吗&#xff1f;记得先点蓝字关注我哦&#xff5e;微软在比尔盖茨手中创立并崛起, 成为PC互联网时代的霸主&#xff0c;很多70&#xff0c;80后都有MVP Edi Wang 的体验<“ 当时的微软对我来说就是神的存在。因为我认识电脑到使用电脑的一切几乎都离不开这家伟大的公…

合并区间

题目描述 给出一个区间的集合&#xff0c;请合并所有重叠的区间。 示例 1: 输入: [[1,3],[2,6],[8,10],[15,18]] 输出: [[1,6],[8,10],[15,18]] 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].示例 2: 输入: [[1,4],[4,5]] 输出: [[1,5]] 解释: 区间 [1,4] 和 [4,5]…

程序员修神之路--设计一套RPC框架并非易事

菜菜哥&#xff0c;我最近终于把Socket通信调通了这么底层的东西你现在都会了&#xff0c;恭喜你离涨薪又进一步呀http协议不也是利用的Socket吗可以这么说&#xff0c;http协议是基于TCP协议的&#xff0c;底层的数据传输可以说是利用的socket既然Socket通信会了&#xff0c;那…

GPU Shader 编程基础

转载自&#xff1a;http://www.cnblogs.com/youthlion/archive/2012/12/07/2807919.html 几个基本概念&#xff1a; Vertex buffer&#xff1a;存储顶点的数组。当构成模型的所有顶点都放进vertex buffer后&#xff0c;就可以把vertex buffer送进GPU&#xff0c;然后GPU就可…

Azure pipeline 配置根据条件执行脚本

Azure pipeline 配置根据条件执行脚本Intro我的应用通过 azure pipeline 来做持续集成&#xff0c;之前已经介绍了根据不同分支去打包不同的package&#xff0c;具体的就不再这里详细介绍了&#xff0c;可以参考 Solution来看一下修改之后的 azure-pipelines.yaml 示例配置吧&a…

C# 8 新特性 - 可空引用类型

Nullable Reference Type.在写C#代码的时候&#xff0c;你可能经常会遇到这个错误&#xff1a; 但如果想避免NullReferenceException的发生&#xff0c;确实需要做很多麻烦的工作。 可空引用类型 Null Reference Type 所以&#xff0c;C# 8的可空引用类型就出现了。 C# 8可以让…

统计学习笔记(1) 监督学习概论(1)

原作品&#xff1a;The Elements of Statistical Learning Data Mining, Inference, and Prediction, Second Edition, by Trevor Hastie, Robert Tibshirani and Jerome Friedman An Introduction to Statistical Learning. by Gareth JamesDaniela WittenTrevor Hastie andR…

.NET Core 3.0之深入源码理解ObjectPool(一)

写在前面对象池是一种比较常用的提高系统性能的软件设计模式&#xff0c;它维护了一系列相关对象列表的容器对象&#xff0c;这些对象可以随时重复使用&#xff0c;对象池节省了频繁创建对象的开销。它使用取用/归还的操作模式&#xff0c;并重复执行这些操作。如下图所示&…

Deep Boltzmann Machines

转载自&#xff1a;http://blog.csdn.net/win_in_action/article/details/25333671 http://blog.csdn.net/zouxy09/article/details/8775518 深度神经网络&#xff08;Deep neural network&#xff09; 深度学习的概念源于人工神经网络的研究。含多隐层的多层感知器就是一种…