← 筆記

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

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

作者:羅培羽

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

Client Module Code unitypackage(Google Drive):

https://drive.google.com/file/d/1C6ykannRduaaJf0LdeuehGP_ATKRzYJe/view?usp=sharing

框架部分 - 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();
                }
            }
            
            //Updatepublic 
            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) //字節數組長度<2,無法解析長度信息 
                { 
                    return "";
                } 
            
                //小端模式讀取長度信息 
                Int16 len = (Int16)((bytes[offset + 1] << 8) | bytes[offset]); 
                if (len <= 0) //長度<=0  
                {
                    return ""; 
                } 
                //長度不足
                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);
                  }
              }