• 【Visual C#】基于《斗鱼弹幕服务器第三方接入协议v1.6.2》实现斗鱼弹幕服务器接入


    最近在给某个主播开发斗鱼直播间辅助工具,为了程序的高效稳定,也搜索了大量的资料,经过大量什么百度,谷歌搜索。。。

    虽然有很多Python的脚本及JS脚本实现了拉取斗鱼弹幕信息,但是这些年来的开发职业病告诉我,这满足不了对系统的控制欲望。。

    后来,找啊。。。找啊。。。意外间发现这个文档。。。。废话不多说了,说正题吧。

    斗鱼很人性化的提供了一个基于Socket TCP传输协议的标准文档,通过接口我们可以安全稳定高效的获取斗鱼直播间弹幕信息,实现多种多样化的辅助功能。

    一、协议组成

      众所周知,受TCP最大传输单(MTU)限制及连包机制影响,应用层协议需自己设计协议头,以保证不同消息的隔离性和消息完整性。

      斗鱼后台协议头设计如下:

    字节 Byte0 Byte 1 Byte 2 Byte 3
    长度 消息长度
    头部 消息长度
    消息类型 加密字段 保留字段
    数据部 数据部分(结尾必须为 '')

     

      斗鱼消息协议格式如上所示,其中字段说明如下:

      消息长度:4字节小端整数,表示整条消息(包括自身)长度(字节数)。消息长度出现两遍,二者相同。

      消息类型:2字节小端整数,表示消息类型。取值如下:

        689  客户端发送给弹幕服务器的文本格式数据

        690  弹幕服务器发送给客户端的文本格式数据。

      加密字段:暂未使用,默认为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}/", 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/", ms);
                                }
    
                                Thread.Sleep(5);
                                continue;
                            }
                            #endregion
    
                            #region 发送心跳
                            if (Environment.TickCount - dwMrkl >= 45000)
                            {
                                dwMrkl = Environment.TickCount;
    
                                SocketHelper.LiveMessagePush("type@=mrkl/", 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/", 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

      好了,上面就是基本全部代码了,具体的自行研究吧,有空的话提供大家一些报文的详情数据。

  • 相关阅读:
    洛谷P1368 均分纸牌(加强版) [2017年6月计划 数论14]
    洛谷P1621 集合 [2017年6月计划 数论13]
    NOIP模拟 6.30
    洛谷P1390 公约数的和 [2017年6月计划 数论12]
    洛谷P1573 栈的操作 [2017年6月计划 数论11]
    洛谷P2429 制杖题 [2017年6月计划 数论10]
    Vijos1212 Way Selection [2017年6月计划 二分图03]
    NOIP模拟赛 6.29
    RQNOJ PID192 梦幻大PK [2017年6月计划 二分图02]
    RQNOJ PID141 / 寻找代表元 [2017年6月计划 二分图01]
  • 原文地址:https://www.cnblogs.com/briny/p/12653625.html
Copyright © 2020-2023  润新知