【Visual C#】基於《斗魚彈幕服務器第三方接入協議v1.6.2》實現斗魚彈幕服務器接入


最近在給某個主播開發斗魚直播間輔助工具,為了程序的高效穩定,也搜索了大量的資料,經過大量什么百度,谷歌搜索。。。

雖然有很多Python的腳本及JS腳本實現了拉取斗魚彈幕信息,但是這些年來的開發職業病告訴我,這滿足不了對系統的控制欲望。。

后來,找啊。。。找啊。。。意外間發現這個文檔。。。。廢話不多說了,說正題吧。

 

斗魚很人性化的提供了一個基於Socket TCP傳輸協議的標准文檔,通過接口我們可以安全穩定高效的獲取斗魚直播間彈幕信息,實現多種多樣化的輔助功能。

 

一、協議組成

  眾所周知,受TCP最大傳輸單(MTU)限制及連包機制影響,應用層協議需自己設計協議頭,以保證不同消息的隔離性和消息完整性。

  斗魚后台協議頭設計如下:

 

字節 Byte0 Byte 1 Byte 2 Byte 3
長度 消息長度
頭部 消息長度
消息類型 加密字段 保留字段
數據部 數據部分(結尾必須為 '\0')

 

  斗魚消息協議格式如上所示,其中字段說明如下:

  消息長度:4字節小端整數,表示整條消息(包括自身)長度(字節數)。消息長度出現兩遍,二者相同。

  消息類型:2字節小端整數,表示消息類型。取值如下:

    689  客戶端發送給彈幕服務器的文本格式數據

    690  彈幕服務器發送給客戶端的文本格式數據。

  加密字段:暫未使用,默認為0。

  保留字段:暫未使用,默認為0。

  數據部分:斗魚獨創序列化文本數據,結尾必須為 ‘\0’。詳細序列化、反序列化算法見下節。(所有協議內容均為UTF-8編碼)

 

二、序列化

  為增強兼容性、可讀性斗魚后台通訊協議采用文本形式的明文數據。同時針對平台數據特點,斗魚自創序列化、反序列化算法。即STT序列化。下面詳細

  介紹STT序列化和反序列化。STT序列化支持鍵值對類型、數組類型(意外發現有的報文還有JSON類型)。規定如下:

  1、鍵key和值value直接采用 '@='分割

  2、數組采用 '/' 分割

  3、如果key或者value中含有字符 '/',則使用 '@S' 轉義

  4、如果key或者value中含有字符 '@',則使用 '@A' 轉義

  舉例:

    (1)多個鍵值對數據:key1@=value1/key2@=value2/key3@=value3/

    (2)數組數組:value1/value2/value3/

  不同消息有相同的協議頭、序列化方式。

 

三、客戶端消息格式(部分)

  1、登錄請求消息

    該消息用於完成登陸授權,完整的數據部分應包含的字段如下:

    type@=loginreq/roomid@=58839/

    type:表示登陸請求消息,固定為loginreq

    roomid:登陸房間的ID

  2、客戶端心跳消息

    該消息用於維持與后台間的心跳,完整的數據部分應包含的字段如下:

    type@=mrkl/

    type:表示心跳消息,固定為mrkl

  3、加入房間分組消息

    該消息用於完成加入房間分組,完整的數據部分應包含的字段如下:

    type@=joingroup/rid@=59872/gid@=-9999/

    type:表示為加入房間分組消息,固定為joingroup

    rid:所登錄的房間號

    gid:分組號,第三方平台建議選擇-9999(即海量彈幕模式)

  4、登出消息

    type@=logout/

    該消息用於完成登出后台服務,完整的數據部分應包含的字段如下:

    type:表示為登出消息,固定為logout

 

三、實現斗魚直播彈幕服務器API

  現在網上可以輕松找到《斗魚彈幕服務器第三方接入協議v1.6.2》接口文檔,在文檔中有提到兩個重要的數據:

    彈幕服務器地址:openbarrage.douyutv.com

    彈幕服務器端口:8601

  我們可以通過.NET Framework 提供的TcpClient類庫來方便連接SOCKET彈幕服務器。為了實現服務的穩定性,我這里使用了異步SOCKET客戶端完成連接。

 

  1、彈幕服務器報文頭:

/// <summary>
    /// 彈幕報文頭
    /// </summary>
    [StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
    public struct BARRAGE_PACKAGE
    {
        /// <summary>
        /// 長度
        /// </summary>
        public int dwLen;
        /// <summary>
        /// 長度
        /// </summary>
        public int dwLen2;
        /// <summary>
        /// 發送方向
        /// </summary>
        public Int16 bType;
        /// <summary>
        /// 加密字段(保留)
        /// </summary>
        public byte encrypt;
        /// <summary>
        /// 備注字段(保留)
        /// </summary>
        public byte reserved;
    }

  2、異步套接字格式

// <summary>
    /// 套接字數據
    /// </summary>
    public class SOCKET_PACKAGE
    {
        /// <summary>
        /// Socket套接字主對象
        /// </summary>
        public Socket Socket = null;
        /// <summary>
        /// 緩沖區大小
        /// </summary>
        public const int BufferSize = 4;    // 說明一下,這里由於有的包並不夠1024緩沖區,經過大量測試,緩沖區設置為4最合適了
        /// <summary>
        /// 套接字緩沖區
        /// </summary>
        public byte[] SocketBuffer = new byte[BufferSize];
        /// <summary>
        /// 套接字流緩存
        /// </summary>
        public NetworkStream Stream = null;
    }

  3、SOCKET幫助類

    這個類封裝了直接通過NetworkStream對象並格式化報文向斗魚發送報文(僅僅為了提高開發效率)

#region SOCKET幫助類
    /// <summary>
    /// SOCKET幫助類
    /// </summary>
    public static class SocketHelper
    {
        /// <summary>
        /// 發送斗魚報文
        /// </summary>
        /// <param name="message"></param>
        /// <param name="ms"></param>
        /// <returns></returns>
        public static void LiveMessagePush(string message, NetworkStream ms)
        {
            #region 斗魚報文
            BARRAGE_PACKAGE package = new BARRAGE_PACKAGE();
            package.bType = 689;
            byte[] buffer = Encoding.UTF8.GetBytes(message);
            package.dwLen = buffer.Length + 8;
            package.dwLen2 = package.dwLen;
            package.encrypt = 0x00;
            package.reserved = 0x00;
            #endregion

            #region 發送數據
            byte[] block = new byte[buffer.Length + 12];
            Array.Copy(StreamSerializationHelper.StructureToBytes(package), 0, block, 0, 12);
            Array.Copy(buffer, 0, block, 12, buffer.Length);
            ms.Write(block, 0, block.Length);
            ms.Flush();
            #endregion
        }
    }
    #endregion

  這里可能會有人問到 StreamSerializationHelper這個類庫從哪里來的,這個是自己寫的一個實現對struct結構體序列化的方法。下面也提供一下,如果有更好的可自行更換:)

    /// <summary>
    /// 本基類提供和二進制結構體數據處理的相關函數,這里包含的所有方法都是與標准語言二進制結構體操作
    /// 相關函數
    /// </summary>
    /// <remarks>
    /// 本基類提供和二進制結構體數據處理的相關函數。這里采用靜態方法的形式提供出各種數據對象進行互轉
    /// 的方法
    /// <list type="bullet">
    /// <item>二進制文件到結構體的轉換</item>
    /// <item>結構體文件轉換為二進制數據</item>
    /// </list>
    /// </remarks>
    public static class StreamSerializationHelper
    {
        /// <summary>
        /// 將托管格式結構體轉換為byte數組格式
        /// </summary>
        /// <param name="graph">源數據</param>
        /// <returns></returns>
        public static byte[] StructureToBytes(object graph)
        {
            // 獲取數據結構體大小(非托管)
            int dwStructureSize = Marshal.SizeOf(graph);
            // 從進程的非托管內存中分配內存
            IntPtr iter = Marshal.AllocHGlobal(dwStructureSize);
            // 將數據從托管對象封裝送往非托管內存塊
            Marshal.StructureToPtr(graph, iter, true);
            // 分配指定大小數組塊
            byte[] mBytes = new byte[dwStructureSize];
            // 將數據從非托管內存復制到托管數組中
            Marshal.Copy(iter, mBytes, 0, dwStructureSize);
            Marshal.FreeHGlobal(iter);
            return mBytes;
        }
        /// <summary>
        /// 將非托管數組轉換至托管結構體
        /// </summary>
        /// <typeparam name="T">數據類型</typeparam>
        /// <param name="graph">非托管數組</param>
        /// <returns></returns>
        public static T BytesToStructure<T>(byte[] graph)
        {
            // 獲取數據結構體大小(托管)
            int dwStructureSize = Marshal.SizeOf(typeof(T));
            // 從進程的非托管內存中分配內存
            IntPtr iter = Marshal.AllocHGlobal(dwStructureSize);
            // 將數據從托管內存數組復制到非托管內存指針
            Marshal.Copy(graph, 0, iter, dwStructureSize);
            // 將數據從非托管內存塊送到新分配並指定類型的托管對象並返回
            T obj = (T)Marshal.PtrToStructure(iter, typeof(T));

            Marshal.FreeHGlobal(iter);
            return obj;
        }

        /// <summary>
        /// 通過序列化復制對象
        /// </summary>
        /// <param name="graph"></param>
        /// <returns></returns>
        public static object CloneObject(object graph)
        {
            ExceptionHelper.FalseThrow<ArgumentNullException>(graph != null, "graph");

            using (MemoryStream memoryStream = new MemoryStream(1024))
            {
                BinaryFormatter formatter = new BinaryFormatter();

                formatter.Serialize(memoryStream, graph);

                memoryStream.Position = 0;

                return formatter.Deserialize(memoryStream);
            }
        }
    }

  4、實現登陸彈幕服務器代碼如下:

  

                #region 私有變量
                int dwMrkl = Environment.TickCount;     // 記錄執行的時間,因為斗魚規定每45秒要向斗魚發送一次心跳消息(否則踢下線)
                #endregion

                #region 連接彈幕
                TcpClient tcpClient = new TcpClient();
                tcpClient.Connect("openbarrage.douyutv.com",8601);
                #endregion

                #region 網絡數據
                using (NetworkStream ms = tcpClient.GetStream())
                {
                    #region 登陸請求
                    SocketHelper.LiveMessagePush(string.Format("type@=loginreq/roomid@={0}/\0", 99999), ms);
                    #endregion

                    #region 接收數據
                    while (environment_semaphore && tcpClient.Connected)
                    {
                        #region 發送心跳
                        if (!ms.DataAvailable && tcpClient.Connected)       
                        {
                            // 不管是否有數據,只要SOCKET連接那么就進行心跳判斷

                            if (Environment.TickCount - dwMrkl >= 45000)
                            {
                                dwMrkl = Environment.TickCount;                     // 重新計算心跳消息時間

                                SocketHelper.LiveMessagePush("type@=mrkl/\0", ms);
                            }

                            Thread.Sleep(5);
                            continue;
                        }
                        #endregion

                        #region 發送心跳
                        if (Environment.TickCount - dwMrkl >= 45000)
                        {
                            dwMrkl = Environment.TickCount;

                            SocketHelper.LiveMessagePush("type@=mrkl/\0", ms);
                        }

                        #region 數據處理
                        byte[] buffer = new byte[SOCKET_PACKAGE.BufferSize];

                        ms.Read(buffer, 0, buffer.Length);

                        int dwLen = BitConverter.ToInt32(buffer, 0);

                        int unReadOfBytes = dwLen;
                        #endregion

                        #region 報文處理
                        using (MemoryStream s = new MemoryStream())
                        {
                            #region 粘包處理
                            // 大家都知道TCP有粘包數據,因為不是優雅的一問一答式,所以要自行處理,這是我想到的最簡單處理粘包的辦法咯
                            do
                            {
                                buffer = new byte[unReadOfBytes >= 1024 ? 1024 : unReadOfBytes];

                                int dwBytesOfRead = ms.Read(buffer, 0, buffer.Length);

                                s.Write(buffer, 0, dwBytesOfRead);

                                unReadOfBytes -= dwBytesOfRead;

                            } while (unReadOfBytes > 0);

                            s.Position = 0;
                            #endregion

                            #region 報文處理
                            if (true)
                            {
                                string content = Encoding.UTF8.GetString(s.ToArray(), 8, dwLen - 8);

                                foreach (string target in Regex.Split(content, "/", RegexOptions.IgnoreCase))
                                {
                                    if (!string.IsNullOrWhiteSpace(target))
                                    {
                                        string[] items = Regex.Split(target, "@=", RegexOptions.IgnoreCase);

                                        if (string.Compare("type", items[0], true) == 0 && string.Compare("loginres", items[1], true) == 0)
                                        {
                          // 當我們收到loginres消息后再發送加入房間分組消息 SocketHelper.LiveMessagePush(string.Format("type@=joingroup/rid@={0}/gif@=-9999/\0", 99999), ms); } if (string.Compare("type", items[0], true) == 0 && string.Compare("loginres", items[1], true) != 0) { string message_type = items[1].Replace("@S", "/").Replace("@A", "@"); if (!string.IsNullOrWhiteSpace(message_type) && string.Compare("mrkl", message_type, true) != 0) { // 這里拿到的content數據就是不含心跳報文的數據,具體要怎么處理看你自己需求了 // TO DO : } } } } } #endregion } #endregion } #endregion } #endregion

  好了,上面就是基本全部代碼了,具體的自行研究吧,有空的話提供大家一些報文的詳情數據。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM