日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > asp.net >内容正文

asp.net

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

發布時間:2023/12/4 asp.net 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 .NET斗鱼直播弹幕客户端(上) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

現在直播平臺由于彈幕的存在,主播與觀眾可以更輕松地進行互動,非常受年輕群眾的歡迎。斗魚TV就是一款非常流行的直播平臺,彈幕更是非常火爆。看到有不少主播接入 彈幕語音播報器彈幕點歌等模塊,這都需要首先連接斗魚彈幕。

經常看到其它編程語言的開發者,分享了他們斗魚彈幕客戶端的代碼。.NET當然也能做,還能做得更好(只是不知為何很少見人分享?)。

本文將包含以下內容:

  • 我將使用斗魚TV官方公開的彈幕PDF文檔,使用?Socket/?TcpClient連續斗魚彈幕;

  • 分析如何利用?.NET強大的?ValueTask特性,在保持代碼簡潔的同時,輕松享受高性能異步代碼的快樂;

  • 然后將使用?ReactiveExtensions(?RX),演示如何將一系列復雜的彈幕接入操作,就像寫?HelloWorld一般容易;

  • 用我自制的“準游戲引擎”?FlysEngine,只需少量代碼,即可將斗魚TV的彈幕顯示左右飛過的效果;

  • 本文內容可能比較多,因此分上、下兩篇闡述,上篇將具體聊聊第1、2點,第3、4點將在下篇進行,整篇完成后,最終效果如下:

    斗魚直播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 }

    該文檔中提到所有數據包格式如下:

    注意前兩個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語句都精簡不少,可謂一氣呵成。

    支持異步/?ValueTask/?Memory<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); } }

    總結

    最終運行效果如下:

    這一篇【DotNet騷操作】文章介紹了如何使用斗魚tv開放彈幕 API,下篇將會:

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

    • 介紹如何使用?ReactiveExtensions(?RX),演示這一系列操作用起來,就像寫?HelloWorld一樣簡單;

    • 用我自制的“準游戲引擎”?FlysEngine,只需少量代碼,即可實現桌面彈幕的效果;

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

    總結

    以上是生活随笔為你收集整理的.NET斗鱼直播弹幕客户端(上)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。