Unity3D C# 从零自定义通讯协议之通信框架

Unity3D C# 从零自定义通讯协议之通信框架

2020年8月6日 0 作者 老王

上篇文章写了自定义通讯协议的内容,但是留了个坑,这里来填上。这个框架是自己总结所得,并不是最优的解决方案,我自己也觉得还有很大的改善空间,但是现在工作中没有继续维护这个项目了,所以也就不了了之了。这里写下来方方便以后查看吧。
ps:这里还有不完善的地方,①是没处理客户端发送指令超时未收到回复的问题 ②是没处理不需要服务器回复的指令 ③是没处理断线重连的问题。

1.逻辑分析

服务端我随手写的用于测试用的,客户端发什么服务器就回复什么,就不多说了。主要看客户端这边,客户端和服务端使用的Tcp来通信。ps:客户端程序的启动入口是ClientAppStart
协议如下:
协议

1.1 CommunicateController

通信管理类CommunicateController:CommunicateController只负责开启和关闭连接,然后处理接收到的数据,同时对外提供方法来发送数据。至于连接或发送数据是底层是怎么实现,交给了TcpClient来负责。CommunicateController开了两个线程,一个处理接收到的数据的线程,一个处理发送指令的线程。

public void Init()
{
    // 连接服务器
    m_Client = new TcpClient();
    m_Client.onReceiveData += ReceiveData;
    ((TcpClient)m_Client).Open("127.0.0.1", 10086);

    // 开启接收线程
    m_HandleReceiveDataThread = new Thread(HandleReceiveData);
    m_HandleReceiveDataThread.Start();

    // 开启发送线程
    m_HandleSendOrderThread = new Thread(HandleSendOrder);
    m_HandleSendOrderThread.Start();
}

处理接收数据的线程,用来提取指令:TcpClient收到数据后,通过委托交给CommunicateController,Controller将收到的数据添加至list集合,然后在处理接收数据的线程中不断去判断集合前两个数据是否为帧头帧尾,如果为帧头帧尾,则根据内容长度一直等待,直到数据接收完毕,然后提取出加密后的内容并计算校验码,如果校验不通过,则移除集合中第一个数据,若通过,则进行解密,并通过委托交给其他地方处理。

private void HandleReceiveData()
{
    const int lengthStartBit = 4;          // 内容起始位 
    while (true)
    {
        lock (lockObj)
        {
            if (m_ReceiveDataCacheList.Count >= OrderConst.FixedPkgLength)
            {
                if (m_ReceiveDataCacheList[0] == 0xAA && m_ReceiveDataCacheList[1] == 0x99)
                {
                    int msgLen = m_ReceiveDataCacheList[lengthStartBit] << 8 | m_ReceiveDataCacheList[lengthStartBit + 1];          // 内容长度
                    if (m_ReceiveDataCacheList.Count < msgLen + OrderConst.FixedPkgLength)
                    {
                        // 数据未接收够,等待
                    }
                    else
                    {
                        // 接收完毕
                        // 1.拷贝内容出来
                        byte[] msg = new byte[msgLen];
                        m_ReceiveDataCacheList.CopyTo(lengthStartBit + 2, msg, 0, msgLen);
                        // 2.计算校验码
                        byte[] crc = MathUtil.CRC16(msg);
                        if (crc[0] == m_ReceiveDataCacheList[lengthStartBit + 2 + msgLen] && crc[1] == m_ReceiveDataCacheList[lengthStartBit + 2 + msgLen + 1])
                        {
                            // 校验通过
                            // 解密数据
                            byte[] desMsg = EncryptUtil.DESDecrypt(msg, OrderConst.DesKey);
                            //DebugUtil.PrintBytes(desMsg, "解密后的数据");

                            // 处理解密的数据
                            onReceiveData?.Invoke(desMsg);
                            m_ReceiveDataCacheList.RemoveRange(0, msgLen + OrderConst.FixedPkgLength);
                        }
                        else
                        {
                            Debug.Log("校验失败");
                            // 校验失败,移除
                            m_ReceiveDataCacheList.RemoveAt(0);
                        }
                    }
                }
                else
                {

                    m_ReceiveDataCacheList.RemoveAt(0);
                }
            }
        }

        Thread.Sleep(10);
    }
}

发送指令的线程:将要发送的指令添加到队列中,然后在此线程中依次发送出去,防止阻塞。

public void SendOrder(IOrder order)
{
    lock (m_SendOrderQueue)
    {
        m_SendOrderQueue.Enqueue(order);
    }
}
private void HandleSendOrder()
 {
     while (true)
     {
         lock (m_SendOrderQueue)
         {
             if (m_SendOrderQueue.Count > 0)
             {
                 IOrder order = m_SendOrderQueue.Dequeue();
                 m_Client.SendData(order.GetOrder());

                 onSendOrder?.Invoke(order);
             }
         }

         Thread.Sleep(10);
     }
 }

1.2 Order、Cmd、Mediator

Order是我们发送给服务器的指令;
Cmd是服务器发下来的指令;
Mediator是两者的中介,将Order和Cmd两者连接起来,方便两者交互。

1.2.1 Order

OrderBase是我们发送给服务器的指令的基类,主要是按照我们规定的协议来构建指令的byte数组,同时里面包含一个标志位handleOver,用于标记此条指令是否处理完毕。
OrderController用来管理所有发送的Order,它把我们发送的Order都按照指令号缓存起来(这里要注意断线时会收不到服务的回复,一直不断发送指令会导致内存溢出),然后单独开了个线程去检测这些指令是否处理完毕(handleOver == true),处理完毕了就移除,没处理完毕的到时会在Mediator中将cmd(带了服务器回过来的参数)拿过来处理。

/// <summary>
/// 指令基类.
/// </summary>
public class OrderBase : IOrder
{
    protected byte[] orderIdBytes;                     // 指令号
    protected byte[] msgBytes;                         // 内容 指令号+带的信息
    protected byte[] allOrderBytes;                    // 整条指令

    protected bool handleOver = false;                 // 该条指令是否处理完毕
    public bool HandleOver => handleOver;

    public int GetOrderId
    {
        get
        {
            if (orderIdBytes == null || orderIdBytes.Length == 0)
            {
                return 0;
            }

            return orderIdBytes[0] << 8 | orderIdBytes[1];
        }
    }

    public byte[] GetOrder()
    {
        return allOrderBytes;
    }

    public void Build(int orderId, byte[] content)
    {
        const int orderIdLen = 2;                   // 指令号长度
        orderIdBytes = new byte[orderIdLen];
        orderIdBytes[0] = (byte)(orderId / 255);    // 高8位
        orderIdBytes[1] = (byte)(orderId % 255);    // 低8位

        msgBytes = new byte[orderIdLen + content.Length];
        Array.Copy(orderIdBytes, 0, msgBytes, 0, 2);
        Array.Copy(content, 0, msgBytes, 2, content.Length);

        allOrderBytes = OrderUtil.BuildOrder(msgBytes);
    }

    public virtual void OnHandleOver() { }
}

OrderUtil工具类用来构建指令。

/// <summary>
/// 构建指令
/// </summary>
/// <param name="msgBytes">所有内容</param>
public static byte[] BuildOrder(byte[] msgBytes)
{
    // 加密内容
    byte[] encryptBytes = EncryptUtil.DESEncrypt(msgBytes, OrderConst.DesKey);

    byte[] allOrderBytes = new byte[OrderConst.FixedPkgLength + encryptBytes.Length];
    // 帧头
    allOrderBytes[0] = OrderConst.OrderHeader[0];
    allOrderBytes[1] = OrderConst.OrderHeader[1];
    // 版本号
    allOrderBytes[2] = OrderConst.MajorVer;
    allOrderBytes[3] = OrderConst.MinorVer;

    // 长度
    allOrderBytes[4] = (byte)(encryptBytes.Length / 255);
    allOrderBytes[5] = (byte)(encryptBytes.Length % 255);
    // 内容 指令号+该指令号中带的信息
    Array.Copy(encryptBytes, 0, allOrderBytes, 6, encryptBytes.Length);
    // 校验码
    byte[] crc = MathUtil.CRC16(encryptBytes);
    allOrderBytes[allOrderBytes.Length - 4] = crc[0];
    allOrderBytes[allOrderBytes.Length - 3] = crc[1];
    // 帧尾
    allOrderBytes[allOrderBytes.Length - 2] = OrderConst.OrderHeader[1];
    allOrderBytes[allOrderBytes.Length - 1] = OrderConst.OrderHeader[0];

    return allOrderBytes;
}

1.2.2 Cmd

Cmd是服务器发下来对应的类。
CmdController通过委托获取到CommunicateController采集到的服务器发下来的内容,然后CmdFactory工厂根据指令号创建Cmd,Cmd内部自己去解析内容。

// CmdController解析指令的部分

/// <summary>
/// 解析服务器发下来的指令.
/// </summary>
/// <param name="desMsg">解密后的内容</param>
private void Parse(byte[] desMsg)
{
    ICmd cmd = CmdFactory.CreateCmd(desMsg);
    lock (LockObj)
    {
        m_CmdQueue.Enqueue(cmd);
    }
}
public class CmdFactory
{
   public static ICmd CreateCmd(byte[] msgBytes)
   {
       int orderId = msgBytes[0] << 8 | msgBytes[1];

       ICmd order = null;
       switch (orderId)
       {
           case 0x0001:
               order = new Cmd_0x0001(msgBytes);
               break;
           default:
               throw new ArgumentOutOfRangeException($"orderId: {orderId : x16}");
       }

       return order;
   }
}
// 指令自己去解析服务器发下来的内容

  /// <summary>
  /// 服务器回复的心跳指令.
  /// </summary>
  public class Cmd_0x0001 : CmdBase
  {
      private int m_Cnt;
      public int Cnt => m_Cnt;
      public Cmd_0x0001(byte[] msgBytes) : base(msgBytes)
      {
      }

      public override void Parse(byte[] msgBytes)
      {
          base.Parse(msgBytes);
          m_Cnt = MathUtil.Bytes2Int(msgBytes, 2);
      }
  }

同时将Cmd添加到队列中,并在Update中进行处理(项目中是一帧处理一条),为什么要放在Update中,不单独开个线程去处理呢?因为收到服务器的回复后,我们可能需要根据回复的内容去更新UI,由于Unity不允许在子线程中处理Unity原生的Object如GamObject,所以我们把处理服务器的回复放到了Update中。

1.2.3 Mediator

怎么处理呢?这里是创建了一个中介者Mediator,然后在中介者的Execute方法中将Cmd和Order联系起来,Mediator也是通过工厂构建。

// CmdController

public void Update()
{
   lock (LockObj)
   {
       if (m_CmdQueue.Count > 0)
       {
           ICmd cmd = m_CmdQueue.Dequeue();
           IMeditor meditor = MediatorFactory.CreateMediator(cmd.GetOrderId, cmd);
           meditor.Execute();
       }
   }
}
public class MediatorFactory
{
    public static IMeditor CreateMediator(int orderId, ICmd cmd)
    {
        IMeditor meditor = null;
        switch (orderId)
        {
            case 0x0001:
                meditor = new Mediator_0x0001(cmd);
                break;
            default:
                Debug.Log($"指令号{orderId : x}未生成中介者");
                break;
        }

        return meditor;
    }
}
public class Mediator_0x0001 : MediatorBase
{
    private const int OrderId = 0x0001;
    public Mediator_0x0001(ICmd cmd) : base(cmd)
    {
    }

    public override void Execute()
    {
        // 将Cmd_0x0001和Order_0x0001联系起来
        Cmd_0x0001 cmd0001 = (Cmd_0x0001)cmd;
        List<IOrder> orderList = OrderController.Instance.GetOrdersById(OrderId);
        foreach (IOrder order in orderList)
        {
            Order_0x0001 order0001 = (Order_0x0001) order;
            order0001.Execute(cmd0001.Cnt);
        }
    }
}

2 项目

项目结构
先要运行ServerExe文件夹下的ServerDemo.exe,然后再运行Client场景就能看到log了。
服务端程序
在这里插入图片描述
项目在这儿,提取码:ldhw。

3 参考文章