帝国CMS模板大全
www.admin99.cn
www.92cms.cn 帝国CMS模板下载站!,情怀,养站,二次开发!源码需求比较大的一站式会员下载,价更省!!!

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

一、tcp粘包产生的原理

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

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

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

二、解决原理及代码实现

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

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

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

2.分包算法

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

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

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

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

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

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

?

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,具体参考

最新的代码

?

//线程安全的字典

   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模型,里面自动实现了解决粘包的问题。

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

dy(“nrwz”);

赞(0)
版权声明:本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
文章名称:《C#中TCP粘包问题的解决方法》
文章链接:https://www.admin99.cn/7524
本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。
QQ站长交流群:953950264

登录

找回密码

注册