1. 程式人生 > >Unity C# 自定義TCP傳輸協議以及封包拆包、解決粘包問題(網路應用層協議)

Unity C# 自定義TCP傳輸協議以及封包拆包、解決粘包問題(網路應用層協議)

本文只是初步實現了一個簡單的基於TCP的自定協議,更為複雜的協議可以根據這種方式去擴充套件。

網路應用層協議,通俗一點的講,它是一種基於socket傳輸的由傳送方和接收方事先協商好的一種訊息包組成結構,主要由訊息頭和訊息體組成。 

眾所周知,基於socket的資訊互動有兩個問題: 

第一、接收方不能主動識別傳送方傳送的資訊型別,例如A方(客戶端)向B方(伺服器)傳送了一條資訊:123,沒有事先經過協議規定的話,B方不可能知道這條資訊123到底是一個int型123還是一個string型123,甚至他根本就不知道這條資訊解析出來是123,所以B方找不到處理這條資訊的方式; 

第二、接收方不能主動拆分發送方傳送的多條資訊,例如A方連續向B方傳送了多條資訊:123、456、789,由於網路延遲或B方接收緩衝區大小的不同設定,B方收到的資訊可能是:1234、5678、9,也可能是123456789,也可能是1、2、3、4、5、6、7、8、9,還可能是更多意想不到的情況...... 

所以網路應用層協議就是為了解決這兩個問題而存在的,當然為訊息包加密也是它的另一個主要目的。 

網路應用層協議的格式一般都是:訊息頭+訊息體,訊息頭的長度是固定的,A方和B方都事先知道訊息頭長度,以及訊息頭中各個部位的值所代表的意義,其中包含了對訊息體的描述,包括訊息體長度,訊息體裡的訊息型別,訊息體的加密方式等。 

B方在收到A方訊息後,先按協議中規定的方式解析訊息頭,獲取到裡面對訊息體的描述資訊,他就可以知道訊息體的長度是多少,以便於跟這條訊息後面所緊跟的下一條訊息進行拆分,他也可以從描述資訊中得知訊息體中的訊息型別,並按正確的解析方式進行解析,從而完成資訊的互動。 

這裡以一個簡單的基於TCP協議的網路應用層協議作為例子:

第一步:定義協議(我們將協議定義如下)

訊息頭(28位元組):(int)訊息校驗碼4位元組 + (int)訊息體長度4位元組 + (long)身份ID8位元組 + (int)主命令4位元組 + (int)子命令4位元組 + (int)加密方式4位元組

訊息體:(int)訊息1長度4位元組 + (string)訊息1 + (int)訊息2長度4位元組 + (string)訊息2 + (int)訊息3長度4位元組 + (string)訊息3 + ......  

第二步:伺服器建立監聽

    //SocketTCPServer.cs     private static string ip = "127.0.0.1";     private static int port = 5690;     private static Socket socketServer;     public static List<Socket> listPlayer = new List<Socket>();     private static Socket sTemp;     ///<summary>     ///繫結地址並監聽     ///</summary>     ///ip地址 埠 型別預設為TCP     public static void init(string ipStr, int iPort)     {         try         {             ip = ipStr;             port = iPort;             socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);             socketServer.Bind(new IPEndPoint(IPAddress.Parse(ip), port));              Thread threadListenAccept = new Thread(new ThreadStart(ListenAccept));             threadListenAccept.Start();         }         catch (ArgumentNullException e)         {             Debug.Log(e.ToString());         }         catch (SocketException e)         {             Debug.Log(e.ToString());         }     }     ///<summary>     ///監聽使用者連線     ///</summary>     private static void ListenAccept()     {         socketServer.Listen(0);                       //對於socketServer繫結的IP和埠開啟監聽         sTemp = socketServer.Accept();                //如果在socketServer上有新的socket連線,則將其存入sTemp,並新增到連結串列         listPlayer.Add(sTemp);         Thread threadReceiveMessage = new Thread(new ThreadStart(ReceiveMessage));         threadReceiveMessage.Start();         while (true)         {             sTemp = socketServer.Accept();             listPlayer.Add(sTemp);         }     }

第三步:客戶端連線伺服器

 //SocketTCPClient.cs     private static string ip = "127.0.0.1";     private static int port = 5690;     private static Socket socketClient;     public static List<string> listMessage = new List<string>();     ///<summary>     ///建立一個SocketClient例項     ///</summary>     ///ip地址 埠 型別預設為TCP     public static void CreateInstance(string ipStr, int iPort)     {         ip = ipStr;         port = iPort;         socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);         ConnectServer();     }     /// <summary>     ///連線伺服器     /// </summary>     private static void ConnectServer()     {         try         {             socketClient.Connect(IPAddress.Parse(ip), port);             Thread threadConnect = new Thread(new ThreadStart(ReceiveMessage));             threadConnect.Start();         }         catch (ArgumentNullException e)         {             Debug.Log(e.ToString());         }         catch (SocketException e)         {             Debug.Log(e.ToString());         }     }

第四步:封包以及傳送訊息包 

    /// <summary>     /// 構建訊息資料包     /// </summary>     /// <param name="Crccode">訊息校驗碼,判斷訊息開始</param>     /// <param name="sessionid">使用者登入成功之後獲得的身份ID</param>     /// <param name="command">主命令</param>     /// <param name="subcommand">子命令</param>     /// <param name="encrypt">加密方式</param>     /// <param name="MessageBody">訊息內容(string陣列)</param>     /// <returns>返回構建完整的資料包</returns>     public static byte[] BuildDataPackage(int Crccode,long sessionid, int command,int subcommand, int encrypt, string[] MessageBody)     {         //訊息校驗碼預設值為0x99FF         Crccode = 65433;         //訊息頭各個分類資料轉換為位元組陣列(非字元型資料需先轉換為網路序  HostToNetworkOrder:主機序轉網路序)         byte[] CrccodeByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Crccode));         byte[] sessionidByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(sessionid));         byte[] commandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(command));         byte[] subcommandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(subcommand));         byte[] encryptByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(encrypt));         //計算訊息體的長度         int MessageBodyLength = 0;         for (int i = 0; i < MessageBody.Length; i++)         {             if (MessageBody[i] == "")                 break;             MessageBodyLength += Encoding.UTF8.GetBytes(MessageBody[i]).Length;         }         //定義訊息體的位元組陣列(訊息體長度MessageBodyLength + 每個訊息前面有一個int變數記錄該訊息位元組長度)         byte[] MessageBodyByte = new byte[MessageBodyLength + MessageBody.Length*4];         //記錄已經存入訊息體陣列的位元組數,用於下一個訊息存入時檢索位置         int CopyIndex = 0;         for (int i = 0; i < MessageBody.Length; i++)         {             //單個訊息             byte[] bytes = Encoding.UTF8.GetBytes(MessageBody[i]);             //先存入單個訊息的長度             BitConverter.GetBytes(IPAddress.HostToNetworkOrder(bytes.Length)).CopyTo(MessageBodyByte, CopyIndex);             CopyIndex += 4;             bytes.CopyTo(MessageBodyByte, CopyIndex);             CopyIndex += bytes.Length;         }         //定義總資料包(訊息校驗碼4位元組 + 訊息長度4位元組 + 身份ID8位元組 + 主命令4位元組 + 子命令4位元組 + 加密方式4位元組 + 訊息體)         byte[] totalByte = new byte[28 + MessageBodyByte.Length];         //組合資料包頭部(訊息校驗碼4位元組 + 訊息長度4位元組 + 身份ID8位元組 + 主命令4位元組 + 子命令4位元組 + 加密方式4位元組)         CrccodeByte.CopyTo(totalByte,0);         BitConverter.GetBytes(IPAddress.HostToNetworkOrder(MessageBodyByte.Length)).CopyTo(totalByte,4);         sessionidByte.CopyTo(totalByte, 8);         commandByte.CopyTo(totalByte, 16);         subcommandByte.CopyTo(totalByte, 20);         encryptByte.CopyTo(totalByte, 24);         //組合資料包體         MessageBodyByte.CopyTo(totalByte,28);         Debug.Log("傳送資料包的總長度為:"+ totalByte.Length);         return totalByte;     }     ///<summary>     ///傳送資訊     ///</summary>     public static void SendMessage(byte[] sendBytes)     {         //確定是否連線         if (socketClient.Connected)         {             //獲取遠端終結點的IP和埠資訊             IPEndPoint ipe = (IPEndPoint)socketClient.RemoteEndPoint;             socketClient.Send(sendBytes, sendBytes.Length, 0);         }     }

第五步:接收訊息以及解析訊息包 

  ///<summary>     ///接收訊息     ///</summary>     private static void ReceiveMessage()     {         while (true)         {             //接受訊息頭(訊息校驗碼4位元組 + 訊息長度4位元組 + 身份ID8位元組 + 主命令4位元組 + 子命令4位元組 + 加密方式4位元組 = 28位元組)             int HeadLength = 28;             //儲存訊息頭的所有位元組數             byte[] recvBytesHead = new byte[HeadLength];             //如果當前需要接收的位元組數大於0,則迴圈接收             while (HeadLength > 0)             {                 byte[] recvBytes1 = new byte[28];                 //將本次傳輸已經接收到的位元組數置0                 int iBytesHead = 0;                 //如果當前需要接收的位元組數大於快取區大小,則按快取區大小進行接收,相反則按剩餘需要接收的位元組數進行接收                 if (HeadLength >= recvBytes1.Length)                 {                     iBytesHead = socketClient.Receive(recvBytes1, recvBytes1.Length, 0);                 }                 else                 {                     iBytesHead = socketClient.Receive(recvBytes1, HeadLength, 0);                 }                 //將接收到的位元組數儲存                 recvBytes1.CopyTo(recvBytesHead, recvBytesHead.Length - HeadLength);                 //減去已經接收到的位元組數                 HeadLength -= iBytesHead;             }             //接收訊息體(訊息體的長度儲存在訊息頭的4至8索引位置的位元組裡)             byte[] bytes = new byte[4];             Array.Copy(recvBytesHead, 4, bytes, 0, 4);             int BodyLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0));             //儲存訊息體的所有位元組數             byte[] recvBytesBody = new byte[BodyLength];             //如果當前需要接收的位元組數大於0,則迴圈接收             while (BodyLength > 0)             {                 byte[] recvBytes2 = new byte[BodyLength < 1024 ? BodyLength : 1024];                 //將本次傳輸已經接收到的位元組數置0                 int iBytesBody = 0;                 //如果當前需要接收的位元組數大於快取區大小,則按快取區大小進行接收,相反則按剩餘需要接收的位元組數進行接收                 if (BodyLength >= recvBytes2.Length)                 {                     iBytesBody = socketClient.Receive(recvBytes2, recvBytes2.Length, 0);                 }                 else                 {                     iBytesBody = socketClient.Receive(recvBytes2, BodyLength, 0);                 }                 //將接收到的位元組數儲存                 recvBytes2.CopyTo(recvBytesBody, recvBytesBody.Length - BodyLength);                 //減去已經接收到的位元組數                 BodyLength -= iBytesBody;             }             //一個訊息包接收完畢,解析訊息包             UnpackData(recvBytesHead,recvBytesBody);         }     }     /// <summary>     /// 解析訊息包     /// </summary>     /// <param name="Head">訊息頭</param>     /// <param name="Body">訊息體</param>     public static void UnpackData(byte[] Head, byte[] Body)     {         byte[] bytes = new byte[4];         Array.Copy(Head, 0, bytes, 0, 4);         Debug.Log("接收到資料包中的校驗碼為:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));           bytes = new byte[8];         Array.Copy(Head, 8, bytes, 0, 8);         Debug.Log("接收到資料包中的身份ID為:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt64(bytes, 0)));           bytes = new byte[4];         Array.Copy(Head, 16, bytes, 0, 4);         Debug.Log("接收到資料包中的資料主命令為:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));           bytes = new byte[4];         Array.Copy(Head, 20, bytes, 0, 4);         Debug.Log("接收到資料包中的資料子命令為:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));           bytes = new byte[4];         Array.Copy(Head, 24, bytes, 0, 4);         Debug.Log("接收到資料包中的資料加密方式為:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));           bytes = new byte[Body.Length];         for (int i = 0; i < Body.Length;)         {             byte[] _byte = new byte[4];             Array.Copy(Body, i, _byte, 0, 4);             i += 4;             int num = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_byte, 0));               _byte = new byte[num];             Array.Copy(Body, i, _byte, 0, num);             i += num;             Debug.Log("接收到資料包中的資料有:" + Encoding.UTF8.GetString(_byte, 0, _byte.Length));         }     }

第六步:測試,同時傳送兩個包到伺服器  private string Ip = "127.0.0.1";     private int Port = 5690;     void Start()     {         SocketTCPServer.init(Ip, Port);           //開啟並初始化伺服器         SocketTCPClient.CreateInstance(Ip, Port); //客戶端連線伺服器     }     void Update()     {         if (Input.GetKeyDown(KeyCode.Space))         {             string[] str = {"測試字串1","test1","test11"};             SocketTCPClient.SendMessage(SocketTCPClient.BuildDataPackage(1, 2, 3, 4,5, str));             string[] str2 = { "我是與1同時傳送的測試字串2,請注意我是否與其他資訊粘包", "test2", "test22" };             SocketTCPClient.SendMessage(SocketTCPClient.BuildDataPackage(1, 6, 7, 8, 9, str2));         }     }     void OnApplicationQuit()     {         SocketTCPClient.Close();         SocketTCPServer.Close();     }

輸出結果如下,可見粘包問題已得到解決:

---------------------  作者:神碼程式設計  來源:CSDN  原文:https://blog.csdn.net/qq992817263/article/details/50164931  版權宣告:本文為博主原創文章,轉載請附上博文連結!