大家好,又见面了,我是全栈君。
首先要看TCP/IP协议,涉及到四层:链路层,网络层。传输层,应用层。
当中以太网(Ethernet)的数据帧在链路层
IP包在网络层
TCP或UDP包在传输层
TCP或UDP中的数据(Data)在应用层
它们的关系是 数据帧{IP包{TCP或UDP包{Data}}}
———————————————————————————
在应用程序中我们用到的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字节以内。
———————————————————————————
MTU对我们的UDP编程非常重要。那怎样查看路由的MTU值呢?
对于windows OS: ping -f -l 如:ping -f -l 1472 192.168.0.1
假设提示:Packets needs to be fragmented but DF set. 则表明MTU小于1500,不断改小data_length值,能够终于測算出gateway的MTU值;
对于linux OS: ping -c -M do -s 如: ping -c 1 -M do -s 1472 192.168.0.1
假设提示 Frag needed and DF set…… 则表明MTU小于1500。能够再測以推算gateway的MTU。
———————————————————————————
IP数据包的最大长度是64K字节(65535),由于在IP包头中用2个字节描写叙述报文长度,2个字节所能表达的最大数字就是65535。
由于IP协议提供为上层协议切割和重组报文的功能,因此传输层协议的数据包长度原则上来说没有限制。
实际上限制还是有的,由于IP包的标识字段终究不可能无限长,依照IPv4。好像上限应该是4G(64K*64K)。
依靠这样的机制。TCP包头中就没有“包长度”字段。而全然依靠IP层去处理分帧。
这就是为什么TCP经常被称作一种“流协议”的原因。开发人员在使用TCP服务的时候,不必去关心数据包的大小。仅仅需讲SOCKET看作一条数据流的入口。往里面放数据就是了,TCP协议本身会进行拥塞/流量控制。
UDP则与TCP不同,UDP包头内有总长度字段。相同为两个字节,因此UDP数据包的总长度被限制为65535,这样恰好能够放进一个IP包内,使得UDP/IP协议栈的实现很easy和高效。65535再减去UDP头本身所占领的8个字节。UDP服务中的最大有效载荷长度仅为65527。
这个值也就是你在调用getsockopt()时指定SO_MAX_MSG_SIZE所得到返回值,不论什么使用SOCK_DGRAM属性的socket,一次send的数据都不能超过这个值,否则必定得到一个错误。
那么,IP包提交给下层协议时将会得到如何的处理呢?这就取决于数据链路层协议了,一般的数据链路层协议都会负责将IP包切割成更小的帧,然后在目的端重组它。在EtherNet上,数据链路帧的大小如以上几位大侠所言。而假设是IP over ATM,则IP包将被切分成一个一个的ATM Cell,大小为53字节。
一些典型的MTU值:
网络: MTU字节
超通道 65535
16Mb/s信息令牌环(IBM) 17914
4Mb/s令牌环(IEEE802.5) 4464
FDDI 4352
以太网 1500
IEEE802.3/802.2 1492
X.25 576
点对点(低时延) 296
路径MTU:假设两台主机之间的通信要通过多个网络,那么每一个网络的链路层就可能有不同的MTU。重要的不是两台主机所在网络的MTU的值,重要的是两台通信主机路径中的最小MTU。它被称作路径MTU。
Tcp传输中的nagle算法
TCP/IP协议中。不管发送多少数据。总是要在数据前面加上协议头,同一时候,对方接收到数据。也须要发送ACK表示确认。为了尽可能的利用网络带宽。TCP总是希望尽可能的发送足够大的数据。
(一个连接会设置MSS參数,因此。TCP/IP希望每次都可以以MSS尺寸的数据块来发送数据)。
Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着很多小数据块。
Nagle算法的基本定义是随意时刻,最多仅仅能有一个未被确认的小段。
所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”。是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
1. Nagle算法的规则:
(1)假设包长度达到MSS,则同意发送。
(2)假设该包括有FIN。则同意发送。
(3)设置了TCP_NODELAY选项,则同意发送。
(4)未设置TCP_CORK选项时,若全部发出去的小数据包(包长度小于MSS)均被确认,则同意发送;
(5)上述条件都未满足,但发生了超时(一般为200ms),则马上发送。
Nagle算法仅仅同意一个未被ACK的包存在于网络,它并无论包的大小,因此它其实就是一个扩展的停-等协议。仅仅只是它是基于包停-等的,而不是基于字节停-等的。Nagle算法全然由TCP协议的ACK机制决定,这会带来一些问题,比方假设对端ACK回复非常快的话,Nagle其实不会拼接太多的数据包,尽管避免了网络拥塞。网络整体的利用率依旧非常低。
Nagle算法是silly window syndrome(SWS)预防算法的一个半集。SWS算法预防发送少量的数据,Nagle算法是其在发送方的实现。而接收方要做的时不要通告缓冲空间的非常小增长。不通知小窗体,除非缓冲区空间有显著的增长。这里显著的增长定义为全然大小的段(MSS)或增长到大于最大窗体的一半。
注意:BSD的实现是同意在空暇链接上发送大的写操作剩下的最后的小段,也就是说,当超过1个MSS数据发送时,内核先依次发送完n个MSS的数据包,然后再发送尾部的小数据包,其间不再延时等待。
(如果网络不堵塞且接收窗体足够大)。
举个样例,一開始client端调用socket的write操作将一个int型数据(称为A块)写入到网络中,因为此时连接是空暇的(也就是说还没有未被确认的小段),因此这个int型数据会被立即发送到server端,接着,client端又调用write操作写入‘\r\n’(简称B块)。这个时候。A块的ACK没有返回。所以能够觉得已经存在了一个未被确认的小段,所以B块没有立即被发送,一直等待A块的ACK收到(大概40ms之后),B块才被发送。整个过程如图所看到的:
这里还隐藏了一个问题,就是A块数据的ACK为什么40ms之后才收到?这是由于TCP/IP中不唯独nagle算法。另一个TCP确认延迟机制 。当Server端收到数据之后,它并不会立即向client端发送ACK,而是会将ACK的发送延迟一段时间(如果为t)。它希望在t时间内server端会向client端发送应答数据,这样ACK就行和应答数据一起发送,就像是应答数据捎带着ACK过去。在我之前的时间中,t大概就是40ms。这就解释了为什么’\r\n’(B块)总是在A块之后40ms才发出。
当然。TCP确认延迟40ms并非一直不变的。TCP连接的延迟确认时间一般初始化为最小值40ms,随后依据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等參数进行不断调整。
另外能够通过设置TCP_QUICKACK选项来取消确认延迟。
关于TCP确认延迟的具体介绍可參考:http://blog.csdn.net/turkeyzhou/article/details/6764389
2. TCP_NODELAY 选项
默认情况下,发送数据採用Negale 算法。这样尽管提高了网络吞吐量,可是实时性却减少了。在一些交互性非常强的应用程序来说是不同意的,使用TCP_NODELAY选项能够禁止Negale 算法。
此时。应用程序向内核递交的每一个数据包都会马上发送出去。
须要注意的是,尽管禁止了Negale 算法。但网络的传输仍然受到TCP确认延迟机制的影响。
3. TCP_CORK 选项
所谓的CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置该选项后。内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据(不可能让数据一直等待吧)。
然而。TCP_CORK的实现可能并不像你想象的那么完美,CORK并不会将连接全然塞住。
内核事实上并不知道应用层究竟什么时候会发送第二批数据用于和第一批数据拼接以达到MTU的大小。因此内核会给出一个时间限制,在该时间内没有拼接成一个大包(努力接近MTU)的话,内核就会无条件发送。也就是说若应用层程序发送小包数据的间隔不够短时,TCP_CORK就没有一点作用,反而失去了数据的实时性(每一个小包数据都会延时一定时间再发送)。
4. Nagle算法与CORK算法差别
Nagle算法和CORK算法非常类似。可是它们的着眼点不一样,Nagle算法主要避免网络由于太多的小包(协议头的比例非常之大)而拥塞,而CORK算法则是为了提高网络的利用率,使得整体上协议头占用的比例尽可能的小。如此看来这二者在避免发送小包上是一致的,在用户控制的层面上。Nagle算法全然不受用户socket的控制,你仅仅能简单的设置TCP_NODELAY而禁用它,CORK算法相同也是通过设置或者清除TCP_CORK使能或者禁用之,然而Nagle算法关心的是网络拥塞问题。仅仅要全部的ACK回来则发包。而CORK算法却能够关心内容,在前后数据包发送间隔非常短的前提下(非常重要。否则内核会帮你将分散的包发出),即使你是分散发送多个小数据包,你也能够通过使能CORK算法将这些内容拼接在一个包内,假设此时用Nagle算法的话,则可能做不到这一点。
实际上Nagle算法并非非常复杂。他的主要职责是数据的累积,实际上有两个门槛:一个就是缓 冲区中的字节数达到了一定量,还有一个就是等待了一定的时间(一般的Nagle算法都是等待200ms)。这两个门槛的不论什么一个达到都必须发送数据了。一般 情况下。假设数据流量非常大,第二个条件是永远不会起作用的,但当发送小的数据包时,第二个门槛就发挥作用了。防止数据被无限的缓存在缓冲区不是好事情哦。 了解了TCP的Nagle算法的原理之后我们能够自己动手来实现一个类似的算法了,在动手之前我们还要记住一个重要的事情,也是我们动手实现Nagle算 法的主要动机就是我想要紧急发送数据的时候就要发送了,所以对于上面的两个门槛之外还的添加一个门槛就是紧急数据发送。
对于我如今每秒钟10次数据发送。每次数据发送量固定在85~100字节的应用而言。假设採用默认的开启Nagle算法。我在发送端,固定每帧数据85个,间隔100ms发送一次,我在接受端(堵塞方式使用)接受的数据是43 138交替出现,可能就是这个算法的时间阈值问题,假设关闭Nagle算法,在接收端就能够保证数据每次接收到的都是85帧。
Nagle算法适用于小包、高延迟的场合,而对于要求交互速度的b/s或c/s就不合适了。
socket在创建的时候。默认都是使用Nagle算法的,这会导致交互速度严重下降,所以须要setsockopt函数来设置TCP_NODELAY为1.只是取消了Nagle算法,就会导致TCP碎片增多。效率可能会减少。
关闭nagle算法,以免影响性能。由于控制时控制端要发送非常多数据量非常小的数据包,须要立即发送。
const char chOpt = 1;
int nErr = setsockopt(pContext->m_Socket, IPPROTO_TCP, TCP_NODELAY, &chOpt, sizeof(char));
if (nErr == -1)
{
TRACE(_T(“setsockopt() error\n”),WSAGetLastError());
return;
}
setsockopt(sockfd, SOL_TCP, TCP_CORK, &on, sizeof(on)); //set TCP_CORK
TCP传输小数据包效率问题
摘要:当使用TCP传输小型数据包时。程序的设计是相当重要的。假设在设计方案中不正确TCP数据包的
延迟应答,Nagle算法。Winsock缓冲作用引起重视,将会严重影响程序的性能。这篇文章讨论了这些
问题,列举了两个案例。给出了一些传输小数据包的优化设计方案。
背景:当Microsoft TCP栈接收到一个数据包时,会启动一个200毫秒的计时器。当ACK确认数据包
发出之后,计时器会复位,接收到下一个数据包时。会再次启动200毫秒的计时器。为了提升应用程序
在内部网和Internet上的传输性能,Microsoft TCP栈使用了以下的策略来决定在接收到数据包后
什么时候发送ACK确认数据包:
1、假设在200毫秒的计时器超时之前。接收到下一个数据包。则马上发送ACK确认数据包。
2、假设当前恰好有数据包须要发给ACK确认信息的接收端,则把ACK确认信息附带在数据包上马上发送。
3、当计时器超时,ACK确认信息马上发送。
为了避免小数据包拥塞网络。Microsoft TCP栈默认启用了Nagle算法,这个算法可以将应用程序多次
调用Send发送的数据拼接起来,当收到前一个数据包的ACK确认信息时,一起发送出去。以下是Nagle
算法的例外情况:
1、假设Microsoft TCP栈拼接起来的数据包超过了MTU值,这个数据会马上发送,而不等待前一个数据
包的ACK确认信息。在以太网中,TCP的MTU(Maximum Transmission Unit)值是1460字节。
2、假设设置了TCP_NODELAY选项。就会禁用Nagle算法。应用程序调用Send发送的数据包会马上被
投递到网络,而没有延迟。
为了在应用层优化性能,Winsock把应用程序调用Send发送的数据从应用程序的缓冲区拷贝到Winsock
内核缓冲区。Microsoft TCP栈利用类似Nagle算法的方法,决定什么时候才实际地把数据投递到网络。
内核缓冲区的默认大小是8K,使用SO_SNDBUF选项,能够改变Winsock内核缓冲区的大小。假设有必要的话。
Winsock能缓冲大于SO_SNDBUF缓冲区大小的数据。在绝大多数情况下,应用程序完毕Send调用只表明数据
被拷贝到了Winsock内核缓冲区,并不能说明数据就实际地被投递到了网络上。
唯一一种例外的情况是:
通过设置SO_SNDBUT为0禁用了Winsock内核缓冲区。
Winsock使用以下的规则来向应用程序表明一个Send调用的完毕:
1、假设socket仍然在SO_SNDBUF限额内,Winsock复制应用程序要发送的数据到内核缓冲区。完毕Send调用。
2、假设Socket超过了SO_SNDBUF限额而且先前仅仅有一个被缓冲的发送数据在内核缓冲区,Winsock复制要发送
的数据到内核缓冲区,完毕Send调用。
3、假设Socket超过了SO_SNDBUF限额而且内核缓冲区有不仅仅一个被缓冲的发送数据,Winsock复制要发送的数据
到内核缓冲区,然后投递数据到网络。直到Socket降到SO_SNDBUF限额内或者仅仅剩余一个要发送的数据,才
完毕Send调用。
案例1
一个Winsock TCPclient须要发送10000个记录到Winsock TCP服务端,保存到数据库。记录大小从20字节到100
字节不等。
对于简单的应用程序逻辑,可能的设计方案例如以下:
1、client以堵塞方式发送。服务端以堵塞方式接收。
2、client设置SO_SNDBUF为0。禁用Nagle算法,让每一个数据包单独的发送。
3、服务端在一个循环中调用Recv接收数据包。给Recv传递200字节的缓冲区以便让每一个记录在一次Recv调用中
被获取到。
性能:
在測试中发现。client每秒仅仅能发送5条数据到服务段。总共10000条记录,976K字节左右。用了半个多小时
才所有传到server。
分析:
由于client没有设置TCP_NODELAY选项,Nagle算法强制TCP栈在发送数据包之前等待前一个数据包的ACK确认
信息。
然而,client设置SO_SNDBUF为0,禁用了内核缓冲区。因此,10000个Send调用仅仅能一个数据包一个数据
包的发送和确认,因为下列原因,每一个ACK确认信息被延迟200毫秒:
1、当server获取到一个数据包,启动一个200毫秒的计时器。
2、服务端不须要向client发送不论什么数据,所以。ACK确认信息不能被发回的数据包顺路携带。
3、client在没有收到前一个数据包的确认信息前,不能发送数据包。
4、服务端的计时器超时后。ACK确认信息被发送到client。
怎样提高性能:
在这个设计中存在两个问题。第一,存在延时问题。client须要可以在200毫秒内发送两个数据包到服务端。
由于client默认情况下使用Nagle算法,应该使用默认的内核缓冲区,不应该设置SO_SNDBUF为0。一旦TCP
栈拼接起来的数据包超过MTU值。这个数据包会马上被发送,不用等待前一个ACK确认信息。第二,这个设计
方案对每个如此小的的数据包都调用一次Send。
发送这么小的数据包是不非常有效率的。在这样的情况下。应该
把每一个记录补充到100字节而且每次调用Send发送80个记录。为了让服务端知道一次总共发送了多少个记录,
client能够在记录前面带一个头信息。
案例二:
一个Winsock TCPclient程序打开两个连接和一个提供股票报价服务的Winsock TCP服务端通信。
第一个连接
作为命令通道用来传输股票编号到服务端。第二个连接作为数据通道用来接收股票报价。两个连接被建立后,
client通过命令通道发送股票编号到服务端。然后在数据通道上等待返回的股票报价信息。
client在接收到第一
个股票报价信息后发送下一个股票编号请求到服务端。client和服务端都没有设置SO_SNDBUF和TCP_NODELAY
选项。
性能:
測试中发现,client每秒仅仅能获取到5条报价信息。
分析:
这个设计方案一次仅仅同意获取一条股票信息。
第一个股票编号信息通过命令通道发送到服务端。马上接收到
服务端通过数据通道返回的股票报价信息。然后。client马上发送第二条请求信息。send调用马上返回,
发送的数据被拷贝到内核缓冲区。然而,TCP栈不能马上投递这个数据包到网络。由于没有收到前一个数据包的
ACK确认信息。200毫秒后。服务端的计时器超时,第一个请求数据包的ACK确认信息被发送回client。client
的第二个请求包才被投递到网络。第二个请求的报价信息马上从数据通道返回到client,由于此时。client的
计时器已经超时,第一个报价信息的ACK确认信息已经被发送到服务端。
这个过程循环发生。
怎样提高性能:
在这里。两个连接的设计是没有必要的。
假设使用一个连接来请求和接收报价信息,股票请求的ACK确认信息会
被返回的报价信息马上顺路携带回来。
要进一步的提高性能,client应该一次调用Send发送多个股票请求,服务端
一次返回多个报价信息。假设因为某些特殊原因必需要使用两个单向的连接,client和服务端都应该设置TCP_NODELAY
选项,让小数据包马上发送而不用等待前一个数据包的ACK确认信息。
提高性能的建议:
上面两个案例说明了一些最坏的情况。当设计一个方案解决大量的小数据包发送和接收时,应该遵循下面的建议:
1、假设数据片段不须要紧急传输的话。应用程序应该将他们拼接成更大的数据块,再调用Send。由于发送缓冲区
非常可能被拷贝到内核缓冲区,所以缓冲区不应该太大,通常比8K小一点点是非常有效率的。
仅仅要Winsock内核缓冲区
得到一个大于MTU值的数据块,就会发送若干个数据包。剩下最后一个数据包。
发送方除了最后一个数据包,都不会
被200毫秒的计时器触发。
2、假设可能的话,避免单向的Socket数据流接连。
3、不要设置SO_SNDBUF为0。除非想确保数据包在调用Send完毕之后马上被投递到网络。其实,8K的缓冲区适合大多数
情况。不须要又一次改变。除非新设置的缓冲区经过測试的确比默认大小更高效。
4、假设传输数据不用保证可靠性,使用UDP。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/115336.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...