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 版權宣告:本文為博主原創文章,轉載請附上博文連結!