服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - C#中TCP粘包问题的解决方法

C#中TCP粘包问题的解决方法

2022-01-17 12:43白云随风 C#

这篇文章主要为大家详细介绍了C#中TCP粘包问题的解决方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

一、tcp粘包产生的原理

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

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

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

二、解决原理及代码实现

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

C#中TCP粘包问题的解决方法

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

一个完整的数据包(l)=headersize+bodysize;

2.分包算法

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

       m=系统缓冲区大小;l=用户发送的数据包=headersize+bodysize;

1)若l<m,则表明数据流包含多包数据,从其头部截取若干个字节存入临时缓冲区,剩余部分数据依此继续循环处理,直至结束。

C#中TCP粘包问题的解决方法

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

C#中TCP粘包问题的解决方法

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

C#中TCP粘包问题的解决方法

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
int headsize = 4;//包头长度 固定4
  byte[] surplusbuffer = null;//不完整的数据包,即用户自定义缓冲区
  /// <summary>
  /// 接收客户端发来的数据
  /// </summary>
  /// <param name="connid">每个客户的会话id</param>
  /// <param name="bytes">缓冲区数据</param>
  /// <returns></returns>
  private handleresult onreceive(intptr connid, byte[] bytes)
  {
   //bytes 为系统缓冲区数据
   //bytesread为系统缓冲区长度
   int bytesread = bytes.length;
   if (bytesread > 0)
   {
    if (surplusbuffer == null)//判断是不是第一次接收,为空说是第一次
     surplusbuffer = bytes;//把系统缓冲区数据放在自定义缓冲区里面
    else
     surplusbuffer = surplusbuffer.concat(bytes).toarray();//拼接上一次剩余的包
    //已经完成读取每个数据包长度
    int haveread = 0;
    //这里totallen的长度有可能大于缓冲区大小的(因为 这里的surplusbuffer 是系统缓冲区+不完整的数据包)
    int totallen = surplusbuffer.length;
    while (haveread <= totallen)
    {
     //如果在n此拆解后剩余的数据包连一个包头的长度都不够
     //说明是上次读取n个完整数据包后,剩下的最后一个非完整的数据包
     if (totallen - haveread < headsize)
     {
      byte[] bytesub = new byte[totallen - haveread];
      //把剩下不够一个完整的数据包存起来
      buffer.blockcopy(surplusbuffer, haveread, bytesub, 0, totallen - haveread);
      surplusbuffer = bytesub;
      totallen = 0;
      break;
     }
     //如果够了一个完整包,则读取包头的数据
     byte[] headbyte = new byte[headsize];
     buffer.blockcopy(surplusbuffer, haveread, headbyte, 0, headsize);//从缓冲区里读取包头的字节
     int bodysize = bitconverter.toint32(headbyte, 0);//从包头里面分析出包体的长度
 
     //这里的 haveread=等于n个数据包的长度 从0开始;0,1,2,3....n
     //如果自定义缓冲区拆解n个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
     if (haveread + headsize + bodysize > totallen)
     {
      byte[] bytesub = new byte[totallen - haveread];
      buffer.blockcopy(surplusbuffer, haveread, bytesub, 0, totallen - haveread);
      surplusbuffer = bytesub;
      break;
     }
     else
     {
      //挨个分解每个包,解析成实际文字
      string strc = encoding.utf8.getstring(surplusbuffer, haveread + headsize, bodysize);
      //addmsg(string.format(" > [onreceive] -> {0}", strc));
      //依次累加当前的数据包的长度
      haveread = haveread + headsize + bodysize;
      if (headsize + bodysize == bytesread)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
      {
       surplusbuffer = null;//设置空 回到原始状态
       totallen = 0;//清0
      }
     }
    }
   }
   return handleresult.ok;
  }

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

仔细看intptr connid 每个连接的会话id

private handleresult onreceive(intptr connid, byte[] bytes)

{

}

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

下面我要解决这个问题。

采用c#安全的concurrentdictionary,具体参考

最新的代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//线程安全的字典
  concurrentdictionary<intptr, byte[]> dic = new concurrentdictionary<intptr, byte[]>();
  int headsize = 4;//包头长度 固定4
  /// <summary>
  /// 接收客户端发来的数据
  /// </summary>
  /// <param name="connid">每个客户的会话id</param>
  /// <param name="bytes">缓冲区数据</param>
  /// <returns></returns>
  private handleresult onreceive(intptr connid, byte[] bytes)
  {
   //bytes 为系统缓冲区数据
   //bytesread为系统缓冲区长度
   int bytesread = bytes.length;
   if (bytesread > 0)
   {
    byte[] surplusbuffer = null;
    if (dic.trygetvalue(connid, out surplusbuffer))
    {
     byte[] curbuffer = surplusbuffer.concat(bytes).toarray();//拼接上一次剩余的包
     //更新会话id 的最新字节
     dic.tryupdate(connid, curbuffer, surplusbuffer);
     surplusbuffer = curbuffer;//同步
    }
    else
    {
     //添加会话id的bytes
     dic.tryadd(connid, bytes);
     surplusbuffer = bytes;//同步
    }
 
    //已经完成读取每个数据包长度
    int haveread = 0;
    //这里totallen的长度有可能大于缓冲区大小的(因为 这里的surplusbuffer 是系统缓冲区+不完整的数据包)
    int totallen = surplusbuffer.length;
    while (haveread <= totallen)
    {
     //如果在n此拆解后剩余的数据包连一个包头的长度都不够
     //说明是上次读取n个完整数据包后,剩下的最后一个非完整的数据包
     if (totallen - haveread < headsize)
     {
      byte[] bytesub = new byte[totallen - haveread];
      //把剩下不够一个完整的数据包存起来
      buffer.blockcopy(surplusbuffer, haveread, bytesub, 0, totallen - haveread);
      dic.tryupdate(connid, bytesub, surplusbuffer);
      surplusbuffer = bytesub;
      totallen = 0;
      break;
     }
     //如果够了一个完整包,则读取包头的数据
     byte[] headbyte = new byte[headsize];
     buffer.blockcopy(surplusbuffer, haveread, headbyte, 0, headsize);//从缓冲区里读取包头的字节
     int bodysize = bitconverter.toint32(headbyte, 0);//从包头里面分析出包体的长度
 
     //这里的 haveread=等于n个数据包的长度 从0开始;0,1,2,3....n
     //如果自定义缓冲区拆解n个包后的长度 大于 总长度,说最后一段数据不够一个完整的包了,拆出来保存
     if (haveread + headsize + bodysize > totallen)
     {
      byte[] bytesub = new byte[totallen - haveread];
      buffer.blockcopy(surplusbuffer, haveread, bytesub, 0, totallen - haveread);
      dic.tryupdate(connid, bytesub, surplusbuffer);
      surplusbuffer = bytesub;
      break;
     }
     else
     {
      //挨个分解每个包,解析成实际文字
      string strc = encoding.utf8.getstring(surplusbuffer, haveread + headsize, bodysize);
      addmsg(string.format(" > {0}[onreceive] -> {1}", connid, strc));
      //依次累加当前的数据包的长度
      haveread = haveread + headsize + bodysize;
      if (headsize + bodysize == bytesread)//如果当前接收的数据包长度正好等于缓冲区长度,则待拼接的不规则数据长度归0
      {
       byte[] xbtye=null;
       dic.tryremove(connid, out xbtye);
       surplusbuffer = null;//设置空 回到原始状态
       totallen = 0;//清0
      }
     }
    }
   }
   return handleresult.ok;
  }

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

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

延伸 · 阅读

精彩推荐
  • C#VS2012 程序打包部署图文详解

    VS2012 程序打包部署图文详解

    VS2012虽然没有集成打包工具,但它为我们提供了下载的端口,需要我们手动安装一个插件InstallShield。网上有很多第三方的打包工具,但为什么偏要使用微软...

    张信秀7712021-12-15
  • C#利用C#实现网络爬虫

    利用C#实现网络爬虫

    这篇文章主要介绍了利用C#实现网络爬虫,完整的介绍了C#实现网络爬虫详细过程,感兴趣的小伙伴们可以参考一下...

    C#教程网11852021-11-16
  • C#C#设计模式之Strategy策略模式解决007大破密码危机问题示例

    C#设计模式之Strategy策略模式解决007大破密码危机问题示例

    这篇文章主要介绍了C#设计模式之Strategy策略模式解决007大破密码危机问题,简单描述了策略模式的定义并结合加密解密算法实例分析了C#策略模式的具体使用...

    GhostRider10972022-01-21
  • C#深入理解C#的数组

    深入理解C#的数组

    本篇文章主要介绍了C#的数组,数组是一种数据结构,详细的介绍了数组的声明和访问等,有兴趣的可以了解一下。...

    佳园9492021-12-10
  • C#C#微信公众号与订阅号接口开发示例代码

    C#微信公众号与订阅号接口开发示例代码

    这篇文章主要介绍了C#微信公众号与订阅号接口开发示例代码,结合实例形式简单分析了C#针对微信接口的调用与处理技巧,需要的朋友可以参考下...

    smartsmile20127762021-11-25
  • C#SQLite在C#中的安装与操作技巧

    SQLite在C#中的安装与操作技巧

    SQLite,是一款轻型的数据库,用于本地的数据储存。其优点有很多,下面通过本文给大家介绍SQLite在C#中的安装与操作技巧,感兴趣的的朋友参考下吧...

    蓝曈魅11162022-01-20
  • C#三十分钟快速掌握C# 6.0知识点

    三十分钟快速掌握C# 6.0知识点

    这篇文章主要介绍了C# 6.0的相关知识点,文中介绍的非常详细,通过这篇文字可以让大家在三十分钟内快速的掌握C# 6.0,需要的朋友可以参考借鉴,下面来...

    雨夜潇湘8272021-12-28
  • C#如何使用C#将Tensorflow训练的.pb文件用在生产环境详解

    如何使用C#将Tensorflow训练的.pb文件用在生产环境详解

    这篇文章主要给大家介绍了关于如何使用C#将Tensorflow训练的.pb文件用在生产环境的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴...

    bbird201811792022-03-05