转载 Unity C# 自定义TCP传输协议以及封包拆包、解决粘包问题

本文只是初步实现了一个简单的TCP自定协议,更为复杂的协议可以根据这种方式去扩展,并且我已经假定你搭建好了一个最为基本的socket TCP通信框架,本文就不对其做过多的讲解了,当然如果你不了解socket的TCP通信的话,可以去看一下我的另一篇博客:

Unity Socket 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,还可能是更多意想不到的情况...... 

所以TCP协议就是为了解决这两个问题而存在的,当然为消息包加密也是它的另一个主要目的。 

TCP协议的格式一般都是:消息头+消息体,消息头的长度是固定的,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 + ......  


第二步:服务器建立监听

  1. //SocketTCPServer.cs  
  2. private static string ip = "127.0.0.1";  
  3. private static int port = 5690;  
  4. private static Socket socketServer;  
  5. public static List<Socket> listPlayer = new List<Socket>();  
  6. private static Socket sTemp;  
  7. ///<summary>  
  8. ///绑定地址并监听  
  9. ///</summary>  
  10. ///ip地址 端口 类型默认为TCP  
  11. public static void init(string ipStr, int iPort)  
  12. {  
  13.     try  
  14.     {  
  15.         ip = ipStr;  
  16.         port = iPort;  
  17.         socketServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  
  18.         socketServer.Bind(new IPEndPoint(IPAddress.Parse(ip), port));   
  19.         Thread threadListenAccept = new Thread(new ThreadStart(ListenAccept));  
  20.         threadListenAccept.Start();  
  21.     }  
  22.     catch (ArgumentNullException e)  
  23.     {  
  24.         Debug.Log(e.ToString());  
  25.     }  
  26.     catch (SocketException e)  
  27.     {  
  28.         Debug.Log(e.ToString());  
  29.     }  
  30. }  
  31. ///<summary>  
  32. ///监听用户连接  
  33. ///</summary>  
  34. private static void ListenAccept()  
  35. {  
  36.     socketServer.Listen(0);                       //对于socketServer绑定的IP和端口开启监听  
  37.     sTemp = socketServer.Accept();                //如果在socketServer上有新的socket连接,则将其存入sTemp,并添加到链表  
  38.     listPlayer.Add(sTemp);  
  39.     Thread threadReceiveMessage = new Thread(new ThreadStart(ReceiveMessage));  
  40.     threadReceiveMessage.Start();  
  41.     while (true)  
  42.     {  
  43.         sTemp = socketServer.Accept();  
  44.         listPlayer.Add(sTemp);  
  45.     }  
  46. }  



第三步:客户端连接服务器

  1. //SocketTCPClient.cs  
  2.    private static string ip = "127.0.0.1";  
  3.    private static int port = 5690;  
  4.    private static Socket socketClient;  
  5.    public static List<string> listMessage = new List<string>();  
  6.    ///<summary>  
  7.    ///创建一个SocketClient实例  
  8.    ///</summary>  
  9.    ///ip地址 端口 类型默认为TCP  
  10.    public static void CreateInstance(string ipStr, int iPort)  
  11.    {  
  12.        ip = ipStr;  
  13.        port = iPort;  
  14.        socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);  
  15.        ConnectServer();  
  16.    }  
  17.    /// <summary>  
  18.    ///连接服务器  
  19.    /// </summary>  
  20.    private static void ConnectServer()  
  21.    {  
  22.        try  
  23.        {  
  24.            socketClient.Connect(IPAddress.Parse(ip), port);  
  25.            Thread threadConnect = new Thread(new ThreadStart(ReceiveMessage));  
  26.            threadConnect.Start();  
  27.        }  
  28.        catch (ArgumentNullException e)  
  29.        {  
  30.            Debug.Log(e.ToString());  
  31.        }  
  32.        catch (SocketException e)  
  33.        {  
  34.            Debug.Log(e.ToString());  
  35.        }  
  36.    }  


第四步:封包以及发送消息包 

  1. /// <summary>  
  2. /// 构建消息数据包  
  3. /// </summary>  
  4. /// <param name="Crccode">消息校验码,判断消息开始</param>  
  5. /// <param name="sessionid">用户登录成功之后获得的身份ID</param>  
  6. /// <param name="command">主命令</param>  
  7. /// <param name="subcommand">子命令</param>  
  8. /// <param name="encrypt">加密方式</param>  
  9. /// <param name="MessageBody">消息内容(string数组)</param>  
  10. /// <returns>返回构建完整的数据包</returns>  
  11. public static byte[] BuildDataPackage(int Crccode,long sessionid, int command,int subcommand, int encrypt, string[] MessageBody)  
  12. {  
  13.     //消息校验码默认值为0x99FF  
  14.     Crccode = 65433;  
  15.     //消息头各个分类数据转换为字节数组(非字符型数据需先转换为网络序  HostToNetworkOrder:主机序转网络序)  
  16.     byte[] CrccodeByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(Crccode));  
  17.     byte[] sessionidByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(sessionid));  
  18.     byte[] commandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(command));  
  19.     byte[] subcommandByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(subcommand));  
  20.     byte[] encryptByte = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(encrypt));  
  21.     //计算消息体的长度  
  22.     int MessageBodyLength = 0;  
  23.     for (int i = 0; i < MessageBody.Length; i++)  
  24.     {  
  25.         if (MessageBody[i] == "")  
  26.             break;  
  27.         MessageBodyLength += Encoding.UTF8.GetBytes(MessageBody[i]).Length;  
  28.     }  
  29.     //定义消息体的字节数组(消息体长度MessageBodyLength + 每个消息前面有一个int变量记录该消息字节长度)  
  30.     byte[] MessageBodyByte = new byte[MessageBodyLength + MessageBody.Length*4];  
  31.     //记录已经存入消息体数组的字节数,用于下一个消息存入时检索位置  
  32.     int CopyIndex = 0;  
  33.     for (int i = 0; i < MessageBody.Length; i++)  
  34.     {  
  35.         //单个消息  
  36.         byte[] bytes = Encoding.UTF8.GetBytes(MessageBody[i]);  
  37.         //先存入单个消息的长度  
  38.         BitConverter.GetBytes(IPAddress.HostToNetworkOrder(bytes.Length)).CopyTo(MessageBodyByte, CopyIndex);  
  39.         CopyIndex += 4;  
  40.         bytes.CopyTo(MessageBodyByte, CopyIndex);  
  41.         CopyIndex += bytes.Length;  
  42.     }  
  43.     //定义总数据包(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节 + 消息体)  
  44.     byte[] totalByte = new byte[28 + MessageBodyByte.Length];  
  45.     //组合数据包头部(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节)  
  46.     CrccodeByte.CopyTo(totalByte,0);  
  47.     BitConverter.GetBytes(IPAddress.HostToNetworkOrder(MessageBodyByte.Length)).CopyTo(totalByte,4);  
  48.     sessionidByte.CopyTo(totalByte, 8);  
  49.     commandByte.CopyTo(totalByte, 16);  
  50.     subcommandByte.CopyTo(totalByte, 20);  
  51.     encryptByte.CopyTo(totalByte, 24);  
  52.     //组合数据包体  
  53.     MessageBodyByte.CopyTo(totalByte,28);  
  54.     Debug.Log("发送数据包的总长度为:"+ totalByte.Length);  
  55.     return totalByte;  
  56. }  
  57. ///<summary>  
  58. ///发送信息  
  59. ///</summary>  
  60. public static void SendMessage(byte[] sendBytes)  
  61. {  
  62.     //确定是否连接  
  63.     if (socketClient.Connected)  
  64.     {  
  65.         //获取远程终结点的IP和端口信息  
  66.         IPEndPoint ipe = (IPEndPoint)socketClient.RemoteEndPoint;  
  67.         socketClient.Send(sendBytes, sendBytes.Length, 0);  
  68.     }  
  69. }  


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

  1. ///<summary>  
  2.   ///接收消息  
  3.   ///</summary>  
  4.   private static void ReceiveMessage()  
  5.   {  
  6.       while (true)  
  7.       {  
  8.           //接受消息头(消息校验码4字节 + 消息长度4字节 + 身份ID8字节 + 主命令4字节 + 子命令4字节 + 加密方式4字节 = 28字节)  
  9.           int HeadLength = 28;  
  10.           //存储消息头的所有字节数  
  11.           byte[] recvBytesHead = new byte[HeadLength];  
  12.           //如果当前需要接收的字节数大于0,则循环接收  
  13.           while (HeadLength > 0)  
  14.           {  
  15.               byte[] recvBytes1 = new byte[28];  
  16.               //将本次传输已经接收到的字节数置0  
  17.               int iBytesHead = 0;  
  18.               //如果当前需要接收的字节数大于缓存区大小,则按缓存区大小进行接收,相反则按剩余需要接收的字节数进行接收  
  19.               if (HeadLength >= recvBytes1.Length)  
  20.               {  
  21.                   iBytesHead = socketClient.Receive(recvBytes1, recvBytes1.Length, 0);  
  22.               }  
  23.               else  
  24.               {  
  25.                   iBytesHead = socketClient.Receive(recvBytes1, HeadLength, 0);  
  26.               }  
  27.               //将接收到的字节数保存  
  28.               recvBytes1.CopyTo(recvBytesHead, recvBytesHead.Length - HeadLength);  
  29.               //减去已经接收到的字节数  
  30.               HeadLength -= iBytesHead;  
  31.           }  
  32.           //接收消息体(消息体的长度存储在消息头的4至8索引位置的字节里)  
  33.           byte[] bytes = new byte[4];  
  34.           Array.Copy(recvBytesHead, 4, bytes, 0, 4);  
  35.           int BodyLength = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0));  
  36.           //存储消息体的所有字节数  
  37.           byte[] recvBytesBody = new byte[BodyLength];  
  38.           //如果当前需要接收的字节数大于0,则循环接收  
  39.           while (BodyLength > 0)  
  40.           {  
  41.               byte[] recvBytes2 = new byte[BodyLength < 1024 ? BodyLength : 1024];  
  42.               //将本次传输已经接收到的字节数置0  
  43.               int iBytesBody = 0;  
  44.               //如果当前需要接收的字节数大于缓存区大小,则按缓存区大小进行接收,相反则按剩余需要接收的字节数进行接收  
  45.               if (BodyLength >= recvBytes2.Length)  
  46.               {  
  47.                   iBytesBody = socketClient.Receive(recvBytes2, recvBytes2.Length, 0);  
  48.               }  
  49.               else  
  50.               {  
  51.                   iBytesBody = socketClient.Receive(recvBytes2, BodyLength, 0);  
  52.               }  
  53.               //将接收到的字节数保存  
  54.               recvBytes2.CopyTo(recvBytesBody, recvBytesBody.Length - BodyLength);  
  55.               //减去已经接收到的字节数  
  56.               BodyLength -= iBytesBody;  
  57.           }  
  58.           //一个消息包接收完毕,解析消息包  
  59.           UnpackData(recvBytesHead,recvBytesBody);  
  60.       }  
  61.   }  
  62.   /// <summary>  
  63.   /// 解析消息包  
  64.   /// </summary>  
  65.   /// <param name="Head">消息头</param>  
  66.   /// <param name="Body">消息体</param>  
  67.   public static void UnpackData(byte[] Head, byte[] Body)  
  68.   {  
  69.       byte[] bytes = new byte[4];  
  70.       Array.Copy(Head, 0, bytes, 0, 4);  
  71.       Debug.Log("接收到数据包中的校验码为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));  
  72.   
  73.       bytes = new byte[8];  
  74.       Array.Copy(Head, 8, bytes, 0, 8);  
  75.       Debug.Log("接收到数据包中的身份ID为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt64(bytes, 0)));  
  76.   
  77.       bytes = new byte[4];  
  78.       Array.Copy(Head, 16, bytes, 0, 4);  
  79.       Debug.Log("接收到数据包中的数据主命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));  
  80.   
  81.       bytes = new byte[4];  
  82.       Array.Copy(Head, 20, bytes, 0, 4);  
  83.       Debug.Log("接收到数据包中的数据子命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));  
  84.   
  85.       bytes = new byte[4];  
  86.       Array.Copy(Head, 24, bytes, 0, 4);  
  87.       Debug.Log("接收到数据包中的数据加密方式为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));  
  88.   
  89.       bytes = new byte[Body.Length];  
  90.       for (int i = 0; i < Body.Length;)  
  91.       {  
  92.           byte[] _byte = new byte[4];  
  93.           Array.Copy(Body, i, _byte, 0, 4);  
  94.           i += 4;  
  95.           int num = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_byte, 0));  
  96.   
  97.           _byte = new byte[num];  
  98.           Array.Copy(Body, i, _byte, 0, num);  
  99.           i += num;  
  100.           Debug.Log("接收到数据包中的数据有:" + Encoding.UTF8.GetString(_byte, 0, _byte.Length));  
  101.       }  
  102.   }  



第六步:测试,同时发送两个包到服务器 
  1. private string Ip = "127.0.0.1";  
  2.     private int Port = 5690;  
  3.     void Start()  
  4.     {  
  5.         SocketTCPServer.init(Ip, Port);           //开启并初始化服务器  
  6.         SocketTCPClient.CreateInstance(Ip, Port); //客户端连接服务器  
  7.     }  
  8.     void Update()  
  9.     {  
  10.         if (Input.GetKeyDown(KeyCode.Space))  
  11.         {  
  12.             string[] str = {"测试字符串1","test1","test11"};  
  13.             SocketTCPClient.SendMessage(SocketTCPClient.BuildDataPackage(1, 2, 3, 4,5, str));  
  14.             string[] str2 = { "我是与1同时发送的测试字符串2,请注意我是否与其他信息粘包""test2""test22" };  
  15.             SocketTCPClient.SendMessage(SocketTCPClient.BuildDataPackage(1, 6, 7, 8, 9, str2));  
  16.         }  
  17.     }  
  18.     void OnApplicationQuit()  
  19.     {  
  20.         SocketTCPClient.Close();  
  21.         SocketTCPServer.Close();  
  22.     }  

输出结果如下,可见粘包问题已得到解决:



超简洁且可以直接使用的源码:

需要的话请在这篇博客回复留下邮箱

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值