Netty数据包大小的确定

来源:互联网 发布:犀牛软件 垃圾袋 编辑:程序博客网 时间:2024/05/27 08:13

Netty接收包大小的确定

  1. 技术点描述

    1. 关于NETTY内部TCP、UDP数据包问题

    当客户端数据量过大时,TCP协议会自动分包进行数据传输(何时分包,如何分包,每包大小尚未研究), 使用netty做server时,netty会根据当前接收到的数据包大小(适用于当前连接),自动调整下次接收到数据包大小(TCP默认大小为1024,当数据包不超过1024时,会一次接收完毕,当超过1024时,下次自动增长为2048,然后下次增长为3072,然后下次增长为4096;然而,再往下增长,则增长了2048变为6144,然后变为8192,数据包增长到8192就已经是极限了(8KB),当客户端数据再大时,接收到的数据包也不再改变。此后,只要发送的数据包大小不超过8192,netty server一律按照一个包接收,不再分包)

     

    确定TCP接收包大小的类AdaptiveReceiveBufferSizePredictor,其中设置默认的接收数据包大小的变量static final int DEFAULT_INITIAL = 1024;

    当设置的此值小于1024时,设置的值不起作用,数据接收时仍按照最大1024接收第一个数据包

    对于TCP的分包bootstrap.setOption("receiveBufferSizePredictorFactory", new FixedReceiveBufferSizePredictorFactory(65535));不起任何作用,且

    TCPSERVER_BOOTSTRAP.setOption("receiveBufferSizePredictorFactory",new AdaptiveReceiveBufferSizePredictorFactory(64,2048,65535));不起任何作用。

    此处均通过netty做的客户端及TCP调试助手测试。

     

    netty控制了每次接收到的数据包的大小,然而对于发送方,netty框架不可能知道要发送的数据包的大小,此时,数据包是一次传输到netty_server还是分次传输取决于网络链路情况(拥挤或空闲),如果一次性传输(一个包),此时,netty_server可控制将此包按照可接收的包大小进行截取分包,而如果是分次传输(分包),则当netty_server接收到数据包后,会进行组包(此时,组包的顺序问题待定),举例如下:

    假设:TCP_client发送数据包大小为3000 byte;分了四次传输,730,770,680,820;TCP_server设置的接收数据包大小为1024,

    则对于当前连接(TCP不中断,保持连接):服务器端接收到两个数据包(分两次),第一次,接收到1024大小的数据包,第二次接收到1976大小的数据包。

    如果保持连接再次发送,则第一次收到2048大小的数据包,第二次收到952大小的数据包。

    如果继续保持连接再次发送,则server仅收到一个大小为3000的数据包

     

    UDP默认最大768,不设置bootstrap.setOption("receiveBufferSizePredictorFactory", new FixedReceiveBufferSizePredictorFactory(65535));时,超过768后的数据丢弃,设置后,数据包只要不超过设置的值,按照一个包接收

    1. 关于TCP/IP数据分包问题

 

首先,需要知道TCP/IP协议栈的构成,如下图所示:

根据图,可以看到,TCP/IP协议栈共分了四层,由高到低依次是:应用层(Application)、传输层(Transport)、网络层(Network)、链路层(Link)。

当两台机器进行通讯时,会产生如下图所示的交互过程:

 

最上方的应用层是我们实际想要发送的数据,数据是通过最下方的以太网进行交互的,而我们的数据通过TCP/IP协议栈发送到网络上,就要从

应用层à传输层à网络层à链路层

而数据每经过一层,都需要进行数据封装(Encapsulation),数据封装如下图所示:

可以看到,数据每经过一层,都在头部加了一个header。

提示:数据包在不同的层有不同的叫法,在传输层叫段(segment),在网络层叫数据报(datagram),在链路层叫帧(frame),真正在传输介质上传输的是帧

下图是说明数据的实际传输过程:

TCP传输的双方需要首先建立连接,之后由TCP协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的总是可靠的数据流,通讯之后关闭连接。UDP协议不面向连接,也不保证可靠性,也不能保证数据包的顺序性。使用UDP协议的应用程序需要自己完成丢包重发、消息排序等工作。

数据在以太网中名为帧,帧的格式如下:这段只做了解,重要的是数据分包,要知道数据是不是需要分包传输,我们必须先找到数据传输过程中会造成分包的因素。首先我们需要知道数据链路层对数据帧的长度的限制,也就是链路层所能承受的最大数据长度----数据传输单元(MTU),以以太网(Ethernet)为例,这个值通常是1500字节。即上层传过来的数据包大小不能超过MTU值,如果超过了,就需要分片。

而封装好的MTU包含了以太头、IP头、TCP头和真实的数据。

以MTU最小为例,则为46,去掉20个IP头,去掉20个TCP头,剩下的就是应用数据,即应用数据最少为6个字节,而对于ARP和RARP,不再做具体说明,关于IP、ARP、RARP以及ICMP、IGMP、TCP、UDP的关系见下图:

允许的最大的应用数据为1500-20个IP头-20个TCP头=1460,而如果仅仅是ICMP协议的话,则允许的最大字节数为1500-20-8=1472,由于ping命令是调用的ICMP协议,故如果ping网关的话(ping -f -l 1472 192.168.1.1),可以返回信息,而如果ping -f -l 1473 192.168.1.1,则返回如下信息Packet needs to be fragmented but DF set.(数据包需要分片,但设置DF为不允许分片,值为1),DF:Don't Fragment,"不分片"位。也就是在此时,数据产生了分片,即数据需要分包发送。另外,使用UDP很容易导致IP分片,而很难强迫TCP发送一个需要进行分片的报文。

为了更好地研究TCP的分包问题,请看下图:

注意看其中第5—8个字节(第二行),在IP头里面,16位识别号唯一记录了一个IP包的ID,具有同一个ID的IP分片将会重新组装;而13位片偏移则记录了某IP片相对整个包的位置;而这两个表中间的3位标志则标志着该分片后面是否还有新的分片。这三个标志就组成了IP分片的所有信息,接受方就可以利用这些信息对IP数据进行重新组织。

下面分析TCP协议中数据的封装问题,为了更好地研究TCP传往另一端的最大数据的长度,这里引入一个词:MSS

MSS是TCP数据包每次能够传输的最大数据分段。

为了达到最佳的传输效能,TCP协议在建立连接的时候通常要协商双方的MSS值,这个值TCP协议在实现的时候往往用MTU值代替(需要减去IP数据包包头的大小20Bytes和TCP数据段的包头20Bytes)所以一般MSS值1460

  通讯双方会根据双方提供的MSS值得最小值确定为这次连接的最大MSS值。

  注:最大报文段长度MSS这个名词很容易引起误解。MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度,而是:MSS=TCP报文段长度-TCP首部长度.我们假设发送的时候,数据包会以1500来封装,然而,不幸的是,传输中有一段X.25网(一个数据包交换网,X.25协议是CCITT(ITU)建议的一种协议,它定义终端和计算机到分组交换网络的连接,这里不做研究),它的MTU是576,这样的话,我们的数据就会在传输中经路由分片,然而,前面提到,而很难强迫TCP发送一个需要进行分片的报文,如果传输时, DF设置为1,而又经过了一个MTU值比较小的网络,则数据包无法分片,就会丢弃此数据包。

虽然总是希望它能很大(1460),但是大多数BSDUnix的衍生系统)实现,它都是512的倍数,如1024……

 

TCP分片中采用的滑动窗口协议,它跟收发两端的缓存有关,暂不研究。

 

由于UDP数据只负责发送,其他概不负责,所以,这里没有仔细研究UDP的分包机制(UDP很容易产生分包,UDP分包机制待研究),这里只做简单介绍:

Internet上的标准MTU值为576字节,故,实际传输过程中UDP数据包的大小尽量控制在548字节以内(576-8-20),UDP数据报的首部8个字节,IP首部20个字节

  1. 实现方案

通过分析,将网上的一些解决方案总结如下:

 

关于UDP:采用UDP数据传输时,需要考虑的是数据包接收后的确认,可借鉴滑动窗口机制设计来提高效率。

关于TCP:

(1)每次发送相同大小的数据包,数据包过大则拆分,过小则填充固定数据(如0)。

(2)采用一个固定的唯一性字符作为包结束的标识,接收数据时,只要出现此标识,则截取前部分作为一个包(主要用于粘包的区分处理)。

(3)模拟TCP发送数据的分包机制,给出一个头信息,封装包标识,当前是第几个包,是否还有后续包标识等,服务器接收后,根据头信息重新组合成一个包。

 

然而,这是三种方式限制性都比较大,最直接的就是直接去发包,依靠网络中的路由自己处理(坏处就是不确定路由默认设置的DF是0还是1,如果是1,就永远收不到数据包了),在接收端只负责从接收区缓冲池中读取数据包。如果不考虑网络中路由的DF设置问题(默认为0),则netty中pipeline.addLast("decoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));方式还是非常有效的,能够保证接收到的数据是整个包而且不会出现粘包现象,也未发现有包混乱(组包时不同的包内容出现在一个包中)现象。

下面说下netty在处理TCP协议包时的组包:

在设置了LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4);后,FrameDecoder先于Handler接收到数据包,FrameDecoder类中有方法:messageReceived,其中有一个全局变量protected ChannelBuffer cumulation;用于存储分包的数据,当接收到第二个分包时,就会把第二个分包通过调用appendToCumulation方法组合成一个包,然后,将所有子包组合成一个包后,提供给Handler类中的messageReceived方法调用,这样,我们接收到的数据包就是一个整包了。

 

  1. 参考源码包

    org.jboss.netty.handler.codec.frame

    org.jboss.netty.channel

    org.jboss.netty.channel.socket.nio

    1. FrameDecoder

messageReceived方法接收子包

appendToCumulation方法拼装子包

  1. SocketReceiveBufferPool

提供接收数据大小的处理,含有此类的包(nio)相关的接收池具体功用还有待分析

  1. FixedReceiveBufferSizePredictorFactory

构造方法new FixedReceiveBufferSizePredictorFactory(65535)设置可接收包(组合后)最大值,目前测试情况仅针对UDP协议有效(UDP默认768),TCP无效

 

  1. Demo实现

    参考netty数据分包处理机制博客

     
0 0
原创粉丝点击