TCP的粘包

参考https://www.cnblogs.com/kex1n/p/6502002.html

1.  首先看一下Nagle 算法:它是用于处理小报文段(微小分组)的发送问题。

   Nagle 算法可以减少网络中微小分组的数量,比如客户端需要依次向服务器发送大小为1,2,3,1,2字节的5个分组。在没有开启nagle算法的情况下,这些小分组会被依次发送(不需要等待上一个小分组的应答,因为没启动Nagle ),总共发送的报文段(分组)个数为5

   当开启Nagle 算法时,客户端首先发送大小为1字节的第一个分组,随后其它分组到达发送缓冲区,由于上一个分组的应答还没有收到,所以TCP会先缓存新来的这4个小分组,并将其重新分组,组成一个大小为8(2+3+1+2)字节的”较大的”小分组。当第一个小分组的应答收到后,客户端将这个8字节的分组发送。总共发送的报文段(分组)个数为2。可以看到,当传输数据存在大量交互数据时,Nagle 算法可以有效减少网络中的报文段个数。

   Nagle 基本定义是任意时刻,最多只能有一个未被确认的小段。所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

   Nagle 算法的规则:

  (1)如果包长度达到MSS,则允许发送;

  (2)如果该包含有FIN,则允许发送;

  (3)设置了TCP_NODELAY选项,则允许发送;

  (4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;

  (5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。

   虽然Nagle 算法可以减少网络中小分组的个数,但是对于那些需要实时预览的通讯程序而言,客户端可能需要不断发送更新数据并得到服务器的响应,这种情况下Nagle 算法会造成客户端明显的延迟,所以需要禁用Nagle 算法。将套接字描述符设置TCP_NODELAY选项可以禁止Nagle 算法。

   配合Nagle 的还有延迟确认和累积确认机制这些可以再看看。

2.  再看一下两种连接:

   1.长连接

   Client方与Server方先建立通讯连接,连接建立后 不断开, 然后再进行报文发送和接收。

   2.短连接

   Client方与Server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点通讯,比如多个Client连接一个Server.

3.  什么是保护消息边界和流?

   保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。而面向流则是指无保护消息保护边界的,如果发送端连续发送数据,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。

   例如,我们连续发送三个数据包,大小分别是2k,4k ,8k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使用UDP协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有三次接收动作,才能够把所有的数据包接收完.而使用TCP协议,我们只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的数据包接收下来,只需要有一次接收动作。

   因此这就是因为UDP协议的保护消息边界使得每一个消息都是独立的。而流传输却把数据当作一串数据流,他不认为数据是一个一个的消息。所以有很多人在使用tcp协议通讯的时候,并不清楚tcp是基于流的传输,当连续发送数据的时候,他们时常会认识tcp会丢包。其实不然,因为当他们使用的缓冲区足够大时,他们有可能会一次接收到两个甚至更多的数据包,而往往只解析检查了第一个数据包。

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

   粘包出现原因

   简单得说,在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows网络编程)

   1.发送端需要等缓冲区满才发送出去,造成粘包

   2.接收方不及时接收缓冲区的包,造成多个包接收

   什么时候需要考虑粘包问题

   1.如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题。

   2.如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包3如果双方建立连接,需要在连接后一段时间内发送不同结构数据,就需要考虑了。

5.  为了避免粘包现象,可采取以下几种措施:

  (1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;

  (2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;

  (3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。

6.  网络通讯的封包和拆包

   对于基于TCP开发的通讯程序,有个很重要的问题需要解决,就是封包和拆包。

   为什么基于TCP的通讯程序需要进行封包和拆包

   TCP是个"流"协议,所谓流,就是没有界限的一串数据,大家可以想想河里的流水,是连成一片的,其间是没有分界线的。但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包。由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况。

   假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).

   A.先接收到data1,然后接收到data2.

   B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.

   C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.

   D.一次性接收到了data1和data2的全部数据.

   对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包,为了拆包就必须在发送端进行封包。

   另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收。

   为什么会出现B.C.D的情况

   1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去.这是对Nagle算法一个简单的解释,详细的请看相关书籍. C和D的情况就有可能是Nagle算法造成的.

   2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据.当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据.

    怎样封包和拆包

   最初遇到"粘包"的问题时,我是通过在两次send之间调用sleep来休眠一小段时间来解决。这个解决方法的缺点是显而易见的,使传输效率大大降低,而且也并不可靠。后来就是通过应答的方式来解决,尽管在大多数时候是可行的,但是不能解决B的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷. 再后来就是对数据包进行封包和拆包的操作。

    封包

   封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容)。包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

    拆包

   对于拆包目前我最常用的是以下两种方式:

  (1)动态缓冲区暂存方式。之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度。

   大概过程描述如下:

   A.为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.

   B.当接收到数据时首先把此段数据存放在缓冲区中.

   C.判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.

   D.根据包头数据解析出里面代表包体长度的变量.

   E.判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.

   F.取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

   这种方法有两个缺点.

   1) 为每个连接动态分配一个缓冲区增大了内存的使用.

   2) 有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.第二种拆包的方法会解决和完善这些缺点.

   前面提到过这种方法的缺点.下面给出一个改进办法, 即采用环形缓冲.但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).第2种拆包方式会解决这两个问题。环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.

  (2)利用底层的缓冲区来进行拆包

   由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了。另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据。利用这两个条件我们就可以对第一种方法进行优化。

   对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据。

posted @ 2019-04-18 13:22  从让帝到the_rang  阅读(556)  评论(0编辑  收藏  举报