《Unity3D網絡遊戲實踐》(第2版)要點摘錄 – 「通用客戶端網絡模塊」

書名:《Unity3D網絡遊戲實踐》(第2版)

作者:羅培羽

所讀版本:機械工業出版社

框架部分 – NetManager 網絡管理器

  • 網絡事件分發模塊

    • 網絡事件

      • 連接成功/失敗/關閉

  • 維護事件監聽列表,記錄每種網絡事件對應的回調方法

    • //網絡事件監聽列表
      private static Dictionary<NetEvent, EventListener> eventListeners = new Dictionary<NetEvent, EventListener>();

      //添加網絡事件監聽
      public static void AddEventListener(NetEvent netEvent, EventListener listener){//...}
      //移除網絡事件監聽
      public static void RemoveEventListener(NetEvent netEvent, EventListener listener){//...}
      //分發網絡事件
      public static void FireEvent(NetEvent netEvent, string err){//...}
  • 服務器連接模塊

    • 使用BeginConnect發起連接

    • 考慮發起連接後,回調返回前再次連接的情況

      • static bool isConnecting = false;
        //連接
        public static void Connect(string ip, int port)
        {
          //...
           isConnecting = true;
           socket.BeginConnect(ip, port, ConnectCallback, socket);
        }

        //連接回調
        private static void ConnectCallback(IAsyncResult ar)
        {
        //...
        //終止連接
        socket.EndConnect(ar);
        //分發連接消息  
          FireEvent(NetEvent.ConnectSucc, "");
        //重置連接狀態
        isConnecting
        = false;
        //繼續接收
          socket.BeginReceive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0, ReceiveCallback, socket);
        //...
        }
  • 數據傳輸模塊

    • 把協議名和協議體分別編碼後組合成新的編碼數據後發送

      • //發送數據
        public static void Send(MsgBase msg)
        {
        //...
           byte[] nameBytes = MsgBase.EncodeName(msg); //協議名數據:長度信息+協議名2進制數據
           byte[] bodyBytes = MsgBase.Encode(msg); //協議體數據:長度信息 + 協議體2進制數據
           //消息長度計算及組裝
          //...
           //寫入隊列
           ByteArray ba = new ByteArray(sendBytes);  
          writeQueue.Enqueue(ba);
        //...
           //發送
          socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);  
        }

        //發送數據回調
        static void SendCallback(IAsyncResult ar)
        {
        //...
           //獲取成功發送的數據長度
           int count = socket.EndSend(ar);

           //獲取寫入隊列的第一條數據
        //...
          writeQueue.Dequeue();
          ba = writeQueue.First();      
        //...

           //還有數據就繼續發送
          socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);
        //...
        }
  • 消息事件分發模塊

    • 與網絡事件類似,唯一不同的是不通過枚舉值,而是通過協議名去分發消息

    • 同樣需要維護一個事件監聽列表

  • 數據處理模塊

    • 連接回調後開啟異步接收數據:BeginReceive

    • 在接收回調中對數據進行處理

      • 接收回調

        • //消息列表
          static List<MsgBase> msgList = new List<MsgBase>();
          //消息列表長度
          static int msgCount = 0;
          //每一次Update處理的消息量
          readonly static int MAX_MESSAGE_FIRE = 10;

          //Receive回調
          private static void ReceiveCallback(IAsyncResult ar)
          {
            Socket socket = (Socket)ar.AsyncState;
            //獲取接收數據長度
            int count = socket.EndReceive(ar);
            //收到FIN信號(count == 0),斷開連接
             //...
          //更新寫入索引
            readBuff.writeIdx += count;
            //數據處理
            OnReceiveData();
            //剩餘空間即將不足,進行數據移位與擴容
          //...
          //繼續接收
            socket.BeginReceive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0, ReceiveCallback, socket);
             //...
          }
      • 數據處理

        • 解析協議

          • 粘包半包、大小端問題進行處理

            • 根據協議前2個字節判斷是否收到一條完整協議

        • 根據協議格式解析出協議對象,根據協議名獲得消息

        • 消息放在消息列表msgList中

        • 主線程Update讀取列表並進行消息處理

          • 優化:每次Update處理多條消息

          • //數據處理
            private static void OnReceiveData()
            {
               if (readBuff.length <= 2) { return; } //消息長度不足解析長度信息
            ​ //...
               //解析協議名
               //...
               //解析協議體
               //...
               //檢查接收緩沖區剩餘空間
               readBuff.CheckAndMoveBytes();
            //把解析出來的消息加到消息列表中

               lock (msgList){msgList.Add(msgBase);}
               msgCount++;
               //繼續讀取消息
               if(readBuff.length > 2){OnReceiveData();}
            }

            //Update
            public static void Update(){MsgUpdate();}

            //更新消息
            public static void MsgUpdate()
            {
               if (msgCount == 0) { return; }
               //處理多條消息
               for (int i = 0; i < MAX_MESSAGE_FIRE; i++)
              {
                   //分發消息
              }
            }
  • 心跳機制

    • 判斷當前時間與上次發送ping時間的間隔,超過指定時間後再發送一次ping

    • 判斷當前時間與上次收到pong時間的間隔,超過指定時間後斷開連接

      • //心跳間隔時間
        public static int pingInterval = 10;
        //上一次發送Ping的時間
        static float lastPingTime = 0;
        //上一次收到Pong的時間
        static float lastPongTime = 0;

        //發送Ping協議
        private static void PingUpdate()
        {
           //Check Ping Signal Overtime and try ping again
           if(Time.time - lastPingTime > pingInterval){//...}
           //Check Pong Signal Overtime
           if(Time.time - lastPongTime > pingInterval * 4){//...}
        }

        //監聽Pong協議
        private static void OnMsgPong(MsgBase msgBase)
        {
           lastPongTime = Time.time;
        }
  • ByteArray 緩沖區類

  • MsgBase 協議基類

協議部分

  • 客戶端和服務端通信的數據格式

  • 參數解析

    • 把一個協議對象轉換成2進制數據(編碼),再把2進制數據轉換成協議對象(解碼)

      • Json/ProtoBuf

    • Json協議

      • 消息長度 + 協議名長度 + 協議名 + 協議體

        • 消息長度 = 協議名長度描述字節數 + 協議名字節數 + 協議體字節數

      • 協議名編/解碼

        • MsgBase.EncodeName

          • //編碼協議名
            public static byte[] EncodeName(MsgBase msgBase)
            {
               //協議名和長度
              byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(msgBase.protoName);
               Int16 len = (Int16)nameBytes.Length;

               byte[] bytes = new byte[2 + len]; //2字節用於描述協議名長度

               //小端組裝長度信息
               bytes[0] = (byte)(len % 256);        
               bytes[1] = (byte)(len / 256);

               Array.Copy(nameBytes, 0, bytes, 2, len);

               return bytes;
            }
        • MsgBase.DecodeName

          • //解碼協議名
            public static string DecodeName(byte[] bytes, int offset, out int count)
            {
               count = 0;
               if(offset + 2 > bytes.Length) { return ""; } //字節數組長度<2,無法解析長度信息

               //小端模式讀取長度信息
             Int16 len = (Int16)((bytes[offset + 1] << 8) | bytes[offset]);
               if (len <= 0) { return ""; } //長度<=0

               if(offset + 2 + len > bytes.Length) { return ""; } //長度不足

               count = 2 + len;
               string name = System.Text.Encoding.UTF8.GetString(bytes, offset + 2, len);
               return name;
            }
      • 協議體編/解碼

        • MsgBase.Encode

          • //編碼協議體
            public static byte[] Encode(MsgBase msgBase)
            {
               string s = JsonUtility.ToJson(msgBase);
               return System.Text.Encoding.UTF8.GetBytes(s);
            }
        • MsgBase.Decode

          • //解碼協議體
            public static MsgBase Decode(string protoName, byte[] bytes, int offset, int count)
            {
               string s = System.Text.Encoding.UTF8.GetString(bytes, offset, count);
               MsgBase msgBase = (MsgBase)JsonUtility.FromJson(s, Type.GetType(protoName));
               return msgBase;
            }
    • ProtoBuf協議

      • Google發布的一套協議格式,規定了一系列的編解碼方法

        • 編碼後的數據量較小

      • 獲取protobuf-net.dll

      • 編寫proto文件

        • message MsgMove{
             optional int32 x = 1;
             optional int32 y = 2;
             optional int32 z = 3;
          }
      • 使用protogen,根據proto文件生成對應的協議類

        • 協議基類為global::ProtoBuf.IExtensible

      • Encode接口改為接受ProtoBuf.IExtensible參數,使用ProtoBuf.Serializer.Serialize將協議對象轉為字節流

        • //使用pb編碼
          public static byte[] ProtobufEncode(ProtoBuf.IExtensible msgBase)
          {
             using(MemoryStream ms = new MemoryStream())
            {
                 ProtoBuf.Serializer.Serialize(ms, msgBase);
                 return ms.ToArray();
            }
          }
      • Decode接口改為接受協議名、解碼對象byte數組、起始位置和長度參數,使用ProtoBuf.Serializer.NonGeneric.Deserialize將字節流轉為基於ProtoBuf.IExtensible基類的對象返回

        • //使用pb解碼
          public static ProtoBuf.IExtensible ProtobufDecode(string protoName, byte[] bytes, int offset, int count)
          {
             using (MemoryStream ms = new MemoryStream(bytes, offset, count))
            {
                 System.Type t = System.Type.GetType(protoName);
                 return (ProtoBuf.IExtensible)ProtoBuf.Serializer.NonGeneric.Deserialize(t, ms);
            }
          }