zoukankan      html  css  js  c++  java
  • 【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

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

  • 相关阅读:
    第三方驱动备份与还原
    Greenplum 解决 gpstop -u 指令报错
    yum安装(卸载)本地rpm包的方法(卸载本地安装的greenplum 5.19.rpm)
    Java JUC(java.util.concurrent工具包)
    netty 详解(八)基于 Netty 模拟实现 RPC
    netty 详解(七)netty 自定义协议解决 TCP 粘包和拆包
    netty 详解(六)netty 自定义编码解码器
    netty 详解(五)netty 使用 protobuf 序列化
    netty 详解(四)netty 开发 WebSocket 长连接程序
    netty 详解(三)netty 心跳检测机制案例
  • 原文地址:https://www.cnblogs.com/briny/p/12653625.html
Copyright © 2011-2022 走看看