C# 自定义TCP传输协议以及封包拆包、解决粘包问题(网络应用层协议)


作者:ych

网络应用层协议,通俗一点的讲,它是一种基于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 + ……

第二步:服务器建立监听

  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. bytes = new byte[8];
  73. Array.Copy(Head, 8, bytes, 0, 8);
  74. Debug.Log("接收到数据包中的身份ID为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt64(bytes, 0)));
  75. bytes = new byte[4];
  76. Array.Copy(Head, 16, bytes, 0, 4);
  77. Debug.Log("接收到数据包中的数据主命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));
  78. bytes = new byte[4];
  79. Array.Copy(Head, 20, bytes, 0, 4);
  80. Debug.Log("接收到数据包中的数据子命令为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));
  81. bytes = new byte[4];
  82. Array.Copy(Head, 24, bytes, 0, 4);
  83. Debug.Log("接收到数据包中的数据加密方式为:" + IPAddress.NetworkToHostOrder(BitConverter.ToInt32(bytes, 0)));
  84. bytes = new byte[Body.Length];
  85. for (int i = 0; i < Body.Length;)
  86. {
  87. byte[] _byte = new byte[4];
  88. Array.Copy(Body, i, _byte, 0, 4);
  89. i += 4;
  90. int num = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(_byte, 0));
  91. _byte = new byte[num];
  92. Array.Copy(Body, i, _byte, 0, num);
  93. i += num;
  94. Debug.Log("接收到数据包中的数据有:" + Encoding.UTF8.GetString(_byte, 0, _byte.Length));
  95. }
  96. }

第六步:测试,同时发送两个包到服务器

  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. }

相关推荐

WPF中实现两个窗口之间传值
SharpPcap抓包工具
C#使用selenium实现爬虫
.NET跨端大杀器MAUI基础学习
很多开发的同学碰到.net framework或.net core升级到最新框架的问题,如何解决?
Unity通过构造函数实现依赖注入
WPF简单MVVM
js加密处理实战

评论区

版权所有:机遇屋在线 Copyright © 2021-2025 jiyuwu Co., Ltd.

鲁ICP备16042261号-1