通过研究发现,约一半的TCP报文段包含成块数据(如FTP、电子邮件),另一半包含交互数据(如telnet、rlogin),如果按字节计算,成块数据与交互数据的比例约为9:1,这是因为成块数据的报文段基本上都是满长度的(通常为512字节的用户数据),而交互数据小得多,telnet和rlogin分组中通常约90%左右的用户数据小于10个字节。
rlogin程序通常每一次按键都会产生一个数据分组,即每次从客户传到服务器的是一个字节的按键,而不是每次一行,而且rlogin需要服务器回显客户键入的字符,这样就会产生四个报文段:
我们一般可以将报文段2和3进行合并,称其为经受时延的确认。
telnet有一个选项允许客户发送一行到服务器,通过使用这个选项可以减少网络的负载。
键入5个字符date\n
时的数据流:
第一行客户发送字符d到服务器,第二行服务器发送该字符的回显和确认,第三行客户发送该字符回显的确认。
对于\n字符,客户只发送了一个字符,但服务器的回显是两个字符,分别是回车\r和换行\n,\r可用CR表示,\n可用LF表示。
第十六行是来自服务器的date命令的输出,这30个字符由28字符的date命令输出和2字节的CR/LF组成。
第十八行是服务器发往客户的7个字符,这是服务器主机上的客户提示符svr4 %
(百分号后边有个空格)。
上图报文段的时间序列:
通常TCP在接收到数据时并不立即发送ACK,它推迟发送以便将ACK与需要沿该方向发送的数据一起发送,有时称其为数据捎带ACK,绝大多数实现采用的时延为200ms。
如果观察上图客户端接收到服务器的回显数据到发送ACK的时间差(16.5ms、16.3ms、16.5ms等),会发现它们似乎是随机的,但全都在200ms内,因为在经受时延的定时器溢出前总是有数据要发送(如果在产生要发送的数据前(上例中是16ms左右)定时器溢出,我们会看到一个经受时延的ACK单独不带数据发出,但上例中我们没有看到)。对于服务器的回显报文的ACK,由于回显报文是随机到达的,TCP会在内核的200ms定时器的下一次溢出时得到通知,这将是1~200ms中的任一刻。
Host Requirements RFC声明TCP需要实现一个经受时延的ACK,但时延必须小于500ms。
在一个rlogin连接上,客户每发送一个字节到服务器,就产生了一个41字节长的分组:20字节的IP首部、20字节的TCP首部、1字节的数据,局域网上这些小分组通常不会引起麻烦,但在广域网上,会增加拥塞出现的可能,采用Nagle算法可解决此问题。
Nagle算法要求一个TCP连接上最多只能有一个未被确认的小分组,在该小分组的确认到达之前不能发送其他小分组,这些未被发送的小分组在确认到来时以一个分组的方式发出去,该算法的优越之处在于确认到达得越快,数据发送得也越快。在希望减少小分组数目的低速广域网上,该算法会发送更少的分组。
在往返时间增加时,在rlogin客户端快速键入字符时的tcpdump输出:
可见左边客户每次发送数据的长度是不同的,这是因为客户只有接收到前一个数据的确认后才发送已经收集的数据。
上图中报文段14和15看似不符合Nagle算法,其实这是由于服务端的回显数据和对客户发过来数据的确认没在一个报文段中一起发送到客户端导致的(确认报文段12是一个经受时延的ACK),在客户发送下一个小分组报文段14时,已经收到了对上一个小分组的确认,之后客户又发送了报文段15以对服务器发回的回显进行确认。
上图中我们看到slip通告窗口大小为4096字节,而服务器通告自己的窗口大小为8192字节,而报文段5的窗口大小为4095字节而非4096字节,这是由于客户程序还未读取传到的数据,这表明确认只是确保TCP收到了数据,不能确保进程也受到了数据。服务器一直通告自己的窗口大小为8192字节,这是因为在服务器回显接收到的数据前,已经读取了收到的数据。然而在服务端的ACK到来时,客户的TCP总是有数据需要发送,这是它在等待ACK的过程中(Nagle算法)缓存的要发送的字符,发送字符时,客户进程还没有读取刚收到的服务器回显,因此客户的窗口大小会小于4096。
有时需要关闭Nagle算法,如X窗口系统服务器,鼠标移动的消息必须无时延地发送;还有在一个交互过程中键入终端的一个特殊功能键,该键产生多个字符序列,这个字符序列通常从ASCII码的转义字符开始,如果TCP每次得到一个字符,它可能会先发送第一个ASCII码转义字符,然后收到转义字符的确认后再发送剩余字符,但服务器在发送确认时,会触发服务器的经受时延的确认算法,最多等500ms才会发送转义字符的确认,对交互用户会产生明显的时延。
socket API可通过TCP_NODELAY选项关闭Nagle算法。
产生多个字符的按键和Nagle算法情况下的报文段交互:
以下是以上过程的时间序列:
上图显示,当rlogin客户读取到输入的第1个字节并向TCP写入时,该字节作为报文段1被发送,它的回显和确认在报文段2中被返回,此时剩余的2个字节才被发送。
上图中第一个字节的回显是2个字节,这是因为ASCII码中转义符的回显是2个字节。
使用关闭了Nagle算法的rlogin重复以上过程:
三个字节准备好时全部被发送,没有时延发生,Nagle算法被禁止。
报文段4中带有来自服务器的第5个字节和一个确认序号为4的ACK,但客户端需要的是服务端的第2个字节,因此客户立即发了一个确认序号为2的响应,看起来是一个报文段丢失了。之后服务端重新发送的报文段6中包含了丢失的报文段中的数据和报文段4,这称为重新分组化。
以上过程的时间序列: