tcp和upd粘包、拆包、ip分片问题

我们都知道TCP属于传输层的协议,传输层除了有TCP协议外还有UDP协议。那么UDP是否会发生粘包或拆包的现象呢?答案是不会UDP是基于报文发送的,从UDP的帧结构可以看出,在UDP首部采用了16bit来指示UDP数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。而TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;另外从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段,基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。

TCP粘包、拆包表现形式

现在假设客户端向服务端连续发送了两个数据包,用packet1和packet2来表示,那么服务端收到的数据可以分为三种,现列举如下:

第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。normal

第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。one

第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。half_oneone_half

TCP粘包、拆包发生原因

发生TCP粘包或拆包有很多原因,现列出常见的几点,可能不全面,欢迎补充,

1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。

2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。

3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。

4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

等等

TCP粘包、拆包解决办法

通过以上分析,我们清楚了粘包或拆包发生的原因,那么如何解决这个问题呢?解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下几个:

1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。


IP分片

1 IP协议简单介绍

就个人而言,网络中,抛开网络安全加密这些,就只单单讨论协议本身,比较难的有三个地方: 

  • IP分片与重组
  • TCP滑动窗口与TCP状态的改变
  • TCP定时器

其实协议本身根据《TCP/IP详解卷1》理解起来并不难,但是实现起来就很难:数据的操作,标志位的设置,网络状态的变换,中断多线程通讯等等;

在下图的七层网络协议参考模型中,IP层属于网络层,网络层最主要的作用就是:将指定IP的数据报传输到对应的主机。

网络七层参考模型

下图是以太帧封装格式(RFC 894),RFC 894封装格式也是我们最常用的。

下面做个简单介绍:

  • 目的地址:6字节,即我们常说的以太网物理地址,这里是目标主机的物理地址,物理地址为唯一的。大家可能疑惑,发送网络数据时只写了IP地址,并没有写目的地址啊。这个是底层协议实现的,根据ARP协议,首先将目的地址设为全1,然后根据IP地址来获取目的地址。
  • 源地址:6字节,发数据时自己的物理地址。
  • 类型:2字节,协议类型,比如0x0800代表IP协议,0x0806代表ARP协议等等
  • 数据:802.3标准规定,一个以太帧最少64字节,最大1518字节,那么去掉6字节目的地址、6字节源地址、2字节类型、4字节CRC,对应区间即为48~1500字节,如果不足48字节可以填充。
  • CRC:顾名思义,校验部分,所以网络数据是很准确的,而且绝大多数都支持硬件CRC校验,速度非常快,几乎不会在这上面消耗时间。

下面再看看数据部分:IP数据报

IP数据报封装

图中从左到右为0~31位,共四个字节,从上到下依次增长,IP头部占20字节,剩下的为数据,如果传输层为TCP则还有20字节的TCP头部,如果是UDP则还有8字节(如果分片的话,中间的包没有UDP头部,即0字节)的UDP头部。剩下的才是真正的用户要传送的数据。可以看出传送同样多的数据,UDP协议要比TCP传送的数据多,但从这一点来说UDP速度也要比TCP快。

下面对一些字段做个简单介绍:

  • 4位版本:比如IPv4、IPv6
  • 4位手部长度:指的是首部占32 bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部最长为60个字节
  • 16位总长度:一共16位,理论最大长度为65535字节,但是受硬件限制,和其它方面的考虑,大部分路由器或主机支持8192字节。
  • 3位标志:标识是否IP分片.第一位无用,第二位0:允许分片,1:不允许。第三位0:最后一片,1:后面还有分片
  • 13位片偏移:此分片在原始数据的偏移,用于分片重组,因为13位,所以支持的最大字节为8192
  • 8位生存时间TTL:规定网络数据包在网际层传输时,最多可以经过路由器的个数,它的大小一般为256/64,每经过一个路由值就会减1,当它为0时,数据会被丢弃,并回传一个ICMP包来通知发送者。

2 IP分片

从上面的介绍我们知道,一个以太帧最大为1518字节 (14字节以太首部,20字节IP首部,UDP8/TCP20,因此IP包每次最大为1500==MTU。去掉协议头UDP有效数据1472字节,TCP为1460字节。还有最后的4字节CRC),但是一个IP数据报则可能会有8192字节,超过以太帧的最大限制,那么这时就需要IP分片,分批进行传输。

发送方会在IP层将要发送的数据分成多个数据包分批发送,而接收方则将数据按照顺序再从新组织起来,等接收到一个完整的数据报之后,然后再提交给上一层传输层。

注意,TCP协议为可靠的传输协议,它避免了IP分片的发生,它会在TCP层对数据进行处理,对数据进行分段(不在详述),IP分片用的多的在UDP协议

我们知道,协议本身并没有对数据在各个层中间怎么传递做出要求,比如嵌入式实现和BSD实现就不太一样,因为嵌入式内存比较少,数据在层与层之间传递时会尽量避免数据拷贝,而只是指针的操作。下面我们以嵌入式中用的比较多的LwIP举例

LwIP允许的最大IP由如下决定:IP_REASS_MAX_PBUFS决定IP分片允许最大pbuf数量,IP_REASS_MAXAGE分片的生存时间,超过则错误并将之前接收的IP分片丢弃。

如果数据大于IP_REASS_MAX_PBUFS则有两种选择,一,直接删除数据返回;二,是删除生存时间最长的IP分片PBUF,这个通过IP_REASS_FREE_OLDEST来使能。

当为UDP协议时,如果缓冲区描述符大小小于完整的IP数据包,IP分片数据包到来时,很快将描述符耗尽,后来的IP包由于无缓冲区描述符而丢弃,UDP没有重传机制,很可能永远不会接收到完整的IP分片包。从而大于IP_REASS_MAXAGE出现错误,因此缓冲区描述符也应增大以适应IP分片重装。

TCP发送数据时,将大于MSS的数据分段(segment不叫分片),MSS一般为1460.所以,TCP数据包不会在IP层分片。

IP头部有3位标志字段,标志是否为分片包。第一位无用,第二位0:允许分片,1:不允许。第三位0:最后一片,1:后面还有分片。13位offset表示偏移,用于IP重组时数据排序,13位因此支持最大IP数据包为8192字节。

标准的BSD协议实现如下图所示,采用两个结构体,IPQ为表头,将各个IP分片表头连接起来,并存储IP信息。Ipasfrag为具体的分片数据。

BSD实现


TCP、UDP数据包大小的限制


1、概述

首先要看TCP/IP协议,涉及到四层:链路层,网络层,传输层,应用层。   
其中以太网(Ethernet)的数据帧在链路层   
IP包在网络层   
TCP或UDP包在传输层   
TCP或UDP中的数据(Data)在应用层   
它们的关系是 数据帧{IP包{TCP或UDP包{Data}}}   

    不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报(datagram),在链路层叫做帧(frame)。数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部,最后将应用层数据交给应用程序处理。


在应用程序中我们用到的Data的长度最大是多少,直接取决于底层的限制。   
我们从下到上分析一下:   
1.在链路层,由以太网的物理特性决定了数据帧的长度为(46+18)-(1500+18),其中的18是数据帧的头和尾,也就是说数据帧的内容最大为1500(不包括帧头和帧尾),即MTU(Maximum Transmission Unit)为1500;  
2.在网络层,因为IP包的首部要占用20字节,所以这的MTU为1500-20=1480; 
3.在传输层,对于UDP包的首部要占用8字节,所以这的MTU为1480-8=1472;   
所以,在应用层,你的Data最大长度为1472。当我们的UDP包中的数据多于MTU(1472)时,发送方的IP层需要分片fragmentation进行传输,而在接收方IP层则需要进行数据报重组,由于UDP是不可靠的传输协议,如果分片丢失导致重组失败,将导致UDP数据包被丢弃。   
从上面的分析来看,在普通的局域网环境下,UDP的数据最大为1472字节最好(避免分片重组)。   
但在网络编程中,Internet中的路由器可能有设置成不同的值(小于默认值),Internet上的标准MTU值为576,所以Internet的UDP编程时数据长度最好在576-20-8=548字节以内。
 

2、TCP、UDP数据包最大值的确定     

        UDP和TCP协议利用端口号实现多项应用同时发送和接收数据。数据通过源端口发送出去,通过目标端口接收。有的网络应用只能使用预留或注册的静态端口;而另外一些网络应用则可以使用未被注册的动态端口。因为UDP和TCP报头使用两个字节存放端口号,所以端口号的有效范围是从0到65535。动态端口的范围是从1024到65535。  

        MTU最大传输单元,这个最大传输单元实际上和链路层协议有着密切的关系,EthernetII帧的结构DMAC+SMAC+Type+Data+CRC由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64Bytes最大不能超过1518Bytes,对于小于或者大于这个限制的以太网帧我们都可以视之为错误的数据帧,一般的以太网转发设备会丢弃这些数据帧。


        由于以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bits=6Bytes+SMAC源MAC地址48bits=6Bytes+Type域2Bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。


UDP 包的大小就应该是 1500 - IP头(20) - UDP头(8) = 1472(Bytes)
TCP 包的大小就应该是 1500 - IP头(20) - TCP头(20) = 1460 (Bytes)


注*PPPoE所谓PPPoE就是在以太网上面跑“PPP”。随着宽带接入(这种宽带接入一般为Cable Modem或者xDSL或者以太网的接入),因为以太网缺乏认证计费机制而传统运营商是通过PPP协议来对拨号等接入服务进行认证计费的,所以引入PPPoE。PPPoE导致MTU变小了以太网的MTU是1500,再减去PPP的包头包尾的开销(8Bytes),就变成1492。不过目前大多数的路由设备的MTU都为1500。


        如果我们定义的TCP和UDP包没有超过范围,那么我们的包在IP层就不用分包了,这样传输过程中就避免了在IP层组包发生的错误;如果超过范围,既IP数据报大于1500字节,发送方IP层就需要将数据包分成若干片,而接收方IP层就需要进行数据报的重组。更严重的是,如果使用UDP协议,当IP层组包发生错误,那么包就会被丢弃。接收方无法重组数据报,将导致丢弃整个IP数据报。UDP不保证可靠传输;但是TCP发生组包错误时,该包会被重传,保证可靠传输。

        UDP数据报的长度是指包括报头和数据部分在内的总字节数,其中报头长度固定,数据部分可变。数据报的最大长度根据操作环境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为65535字节(64K)。

       我们在用Socket编程时,UDP协议要求包小于64K。TCP没有限定,TCP包头中就没有“包长度”字段,而完全依靠IP层去处理分帧。这就是为什么TCP常常被称作一种“流协议”的原因,开发者在使用TCP服务的时候,不必去关心数据包的大小,只需讲SOCKET看作一条数据流的入口,往里面放数据就是了,TCP协议本身会进行拥塞/流量控制。 

       不过鉴于Internet(非局域网)上的标准MTU值为576字节,所以建议在进行Internet的UDP编程时,最好将UDP的数据长度控制在548字节 (576-8-20)以内。


3、TCP、UDP数据包最小值的确定

     在用UDP局域网通信时,经常发生“Hello World”来进行测试,但是“Hello World”并不满足最小有效数据(64-46)的要求,为什么小于18个字节,对方仍然可用收到呢?因为在链路层的MAC子层中会进行数据补齐,不足18个字节的用0补齐。但当服务器在公网,客户端在内网,发生小于18个字节的数据,就会出现接收端收不到数据的情况。

       以太网EthernetII规定,以太网帧数据域部分最小为46字节,也就是以太网帧最小是6+6+2+46+4=64。除去4个字节的FCS,因此,抓包时就是60字节。当数据字段的长度小于46字节时,MAC子层就会在数据字段的后面填充以满足数据帧长不小于64字节。由于填充数据是由MAC子层负责,也就是设备驱动程序。不同的抓包程序和设备驱动程序所处的优先层次可能不同,抓包程序的优先级可能比设备驱动程序更高,也就是说,我们的抓包程序可能在设备驱动程序还没有填充不到64字节的帧的时候,抓包程序已经捕获了数据。因此不同的抓包工具抓到的数据帧的大小可能不同。下列是本人分别用wireshark和sniffer抓包的结果,对于TCP 的ACK确认帧的大小一个是54字节,一个是60字节,wireshark抓取时没有填充数据段,sniffer抓取时有填充数据段。

       

4、实际应用

        用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) - UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。  

        用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。



猜你喜欢

转载自blog.csdn.net/jinxinliu1/article/details/80609272