查看: 1926|回复: 0

[ASP.NET教程] C#中TCP粘包问题的解决方法

发表于 2018-2-10 08:00:07

一、TCP粘包产生的原理

1.TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

2.发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。

3.这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。、

二、解决原理及代码实现

1.采用包头(固定长度,里面存着包体的长度,发送时动态获取)+包体的传输机制。如图

HeaderSize 存放着包体的长度,其HeaderSize本身是定长4字节;

一个完整的数据包(L)=HeaderSize+BodySize;

2.分包算法

  其基本思路是首先将待处理的接收数据流即系统缓冲区数据(长度设为M)强行转换成预定的结构数据形式,并从中取出结构数据长度字段L,而后根据包头计算得到第一包数据长度。

M=系统缓冲区大小;L=用户发送的数据包=HeaderSize+BodySize;

1)若L

2)若L=M,则表明数据流内容恰好是一完整结构数据(即用户自定义缓冲区等于系统接收缓冲区大小),直接将其存入临时缓冲区即可。

3)若L>M,则表明数据流内容尚不够构成一完整结构数据,需留待与下一包数据合并后再行处理。

4)下面是代码代码实现(HP-SOCKET框架的服务器端来接收数据)

  1. int headSize = 4;//包头长度 固定4
  2. byte[] surplusBuffer = null;//不完整的数据包,即用户自定义缓冲区
  3. /// <summary>
  4. /// 接收客户端发来的数据
  5. /// </summary>
  6. /// <param name="connId">每个客户的会话ID</param>
  7. /// <param name="bytes">缓冲区数据</param>
  8. /// <returns></returns>
  9. private HandleResult OnReceive(IntPtr connId, byte[] bytes)
  10. {
  11. //bytes 为系统缓冲区数据
  12. //bytesRead为系统缓冲区长度
  13. int bytesRead = bytes.Length;
  14. if (bytesRead > 0)
  15. {
  16. if (surplusBuffer == null)//判断是不是第一次接收,为空说是第一次
  17. surplusBuffer = bytes;//把系统缓冲区数据放在自定义缓冲区里面
  18. else
  19. surplusBuffer = surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包
  20. //已经完成读取每个数据包长度
  21. int haveRead = 0;
  22. //这里totalLen的长度有可能大于缓冲区大小的(因为 这里的surplusBuffer 是系统缓冲区+不完整的数据包)
  23. int totalLen = surplusBuffer.Length;
  24. while (haveRead <= totalLen)
  25. {
  26. //如果在N此拆解后剩余的数据包连一个包头的长度都不够
  27. //说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包
  28. if (totalLen - haveRead < headSize)
  29. {
  30. byte[] byteSub = new byte[totalLen - haveRead];
  31. //把剩下不够一个完整的数据包存起来
  32. Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
  33. surplusBuffer = byteSub;
  34. totalLen = 0;
  35. break;
  36. }
  37. //如果够了一个完整包,则读取包头的数据
  38. byte[] headByte = new byte[headSize];
  39. Buffer.BlockCopy(surplusBuffer, haveRead, headByte, 0, headSize);//从缓冲区里读取包头的字节
  40. int bodySize = BitConverter.ToInt32(headByte, 0);//从包头里面分析出包体的长度
  41. //这里的 haveRead=等于N个数据包的长度 从0开始;0,1,2,3....N
  42. //如果自定义缓冲区拆解N个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
  43. if (haveRead + headSize + bodySize > totalLen)
  44. {
  45. byte[] byteSub = new byte[totalLen - haveRead];
  46. Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
  47. surplusBuffer = byteSub;
  48. break;
  49. }
  50. else
  51. {
  52. //挨个分解每个包,解析成实际文字
  53. String strc = Encoding.UTF8.GetString(surplusBuffer, haveRead + headSize, bodySize);
  54. //AddMsg(string.Format(" > [OnReceive] -> {0}", strc));
  55. //依次累加当前的数据包的长度
  56. haveRead = haveRead + headSize + bodySize;
  57. if (headSize + bodySize == bytesRead)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
  58. {
  59. surplusBuffer = null;//设置空 回到原始状态
  60. totalLen = 0;//清0
  61. }
  62. }
  63. }
  64. }
  65. return HandleResult.Ok;
  66. }
复制代码

值此完成拆包解析文字工作。但实际上还没完成,如果这段代码是客户端接收来自服务器的数据的话就没问题了。

仔细看IntPtr connId 每个连接的会话ID

private HandleResult OnReceive(IntPtr connId, byte[] bytes)

{

}

但是服务器端还要分辨出 每个数据包是哪个会话产生的,因为服务器端是多线程,多用户的模式,第一个数据包和第二个可能来自不同会话的数据,所以上面的代码只适用于单会话模式。

下面我要解决这个问题。

采用C#安全的ConcurrentDictionary,具体参考 https://msdn.microsoft.com/zh-cn/library/dd287191(v=vs.110).aspx

最新的代码

  1. //线程安全的字典
  2. ConcurrentDictionary<IntPtr, byte[]> dic = new ConcurrentDictionary<IntPtr, byte[]>();
  3. int headSize = 4;//包头长度 固定4
  4. /// <summary>
  5. /// 接收客户端发来的数据
  6. /// </summary>
  7. /// <param name="connId">每个客户的会话ID</param>
  8. /// <param name="bytes">缓冲区数据</param>
  9. /// <returns></returns>
  10. private HandleResult OnReceive(IntPtr connId, byte[] bytes)
  11. {
  12. //bytes 为系统缓冲区数据
  13. //bytesRead为系统缓冲区长度
  14. int bytesRead = bytes.Length;
  15. if (bytesRead > 0)
  16. {
  17. byte[] surplusBuffer = null;
  18. if (dic.TryGetValue(connId, out surplusBuffer))
  19. {
  20. byte[] curBuffer = surplusBuffer.Concat(bytes).ToArray();//拼接上一次剩余的包
  21. //更新会话ID 的最新字节
  22. dic.TryUpdate(connId, curBuffer, surplusBuffer);
  23. surplusBuffer = curBuffer;//同步
  24. }
  25. else
  26. {
  27. //添加会话ID的bytes
  28. dic.TryAdd(connId, bytes);
  29. surplusBuffer = bytes;//同步
  30. }
  31. //已经完成读取每个数据包长度
  32. int haveRead = 0;
  33. //这里totalLen的长度有可能大于缓冲区大小的(因为 这里的surplusBuffer 是系统缓冲区+不完整的数据包)
  34. int totalLen = surplusBuffer.Length;
  35. while (haveRead <= totalLen)
  36. {
  37. //如果在N此拆解后剩余的数据包连一个包头的长度都不够
  38. //说明是上次读取N个完整数据包后,剩下的最后一个非完整的数据包
  39. if (totalLen - haveRead < headSize)
  40. {
  41. byte[] byteSub = new byte[totalLen - haveRead];
  42. //把剩下不够一个完整的数据包存起来
  43. Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
  44. dic.TryUpdate(connId, byteSub, surplusBuffer);
  45. surplusBuffer = byteSub;
  46. totalLen = 0;
  47. break;
  48. }
  49. //如果够了一个完整包,则读取包头的数据
  50. byte[] headByte = new byte[headSize];
  51. Buffer.BlockCopy(surplusBuffer, haveRead, headByte, 0, headSize);//从缓冲区里读取包头的字节
  52. int bodySize = BitConverter.ToInt32(headByte, 0);//从包头里面分析出包体的长度
  53. //这里的 haveRead=等于N个数据包的长度 从0开始;0,1,2,3....N
  54. //如果自定义缓冲区拆解N个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
  55. if (haveRead + headSize + bodySize > totalLen)
  56. {
  57. byte[] byteSub = new byte[totalLen - haveRead];
  58. Buffer.BlockCopy(surplusBuffer, haveRead, byteSub, 0, totalLen - haveRead);
  59. dic.TryUpdate(connId, byteSub, surplusBuffer);
  60. surplusBuffer = byteSub;
  61. break;
  62. }
  63. else
  64. {
  65. //挨个分解每个包,解析成实际文字
  66. String strc = Encoding.UTF8.GetString(surplusBuffer, haveRead + headSize, bodySize);
  67. AddMsg(string.Format(" > {0}[OnReceive] -> 小贝", connId, strc));
  68. //依次累加当前的数据包的长度
  69. haveRead = haveRead + headSize + bodySize;
  70. if (headSize + bodySize == bytesRead)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
  71. {
  72. byte[] xbtye=null;
  73. dic.TryRemove(connId, out xbtye);
  74. surplusBuffer = null;//设置空 回到原始状态
  75. totalLen = 0;//清0
  76. }
  77. }
  78. }
  79. }
  80. return HandleResult.Ok;
  81. }
复制代码

这样就解决了,多客户端会话造成的接收混乱。至此所有工作完成。以上代码就是为了参考学习,如果实在不想这么麻烦。可以直接使用HP-SOCKET通信框架的PACK模型,里面自动实现了解决粘包的问题。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持程序员之家。

您可能感兴趣的文章:

  • 在C#中对TCP客户端的状态封装详解
  • C# Socket粘包处理讲解示例
  • C#实现TCP连接信息统计的方法
  • 使用C#实现基于TCP和UDP协议的网络通信程序的基本示例
  • C#基于TCP协议的服务器端和客户端通信编程的基础教程


回复

使用道具 举报