《TCP/IP详解卷3:TCP事务协议、HTTP、NNTP和UNIX域协议》 —3.6 请求或应答超出报文段最大长度
3.6 请求或应答超出报文段最大长度
到目前为止,在我们所举的所有例子中,无论是客户的请求报文段还是服务器的应答报文段,都没有超过报文段最大长度(MSS)。如果客户要发送超出报文段最大长度的数据,而且也确信对等端支持T/TCP协议,那么它就会发送多个报文段。由于对等端的报文段最大长度存储在TAO高速缓存中(图2-5的tao_mssopt),因而客户的TCP协议能够知道服务器的报文段最大长度,但无法知道服务器的接收窗口宽度(卷1的18.4节和20.4节分别讨论了报文段最大长度和窗口宽度)。对一个特定的主机来说,报文段最大长度一般是一个固定值,而接收窗口的宽度却会随应用程序改变其插口接收缓存的大小而相应地变化。而且,即使对等端告知了一个较大的接收窗口(比如说,32 768字节),但如果报文段最大长度为512字节,那么很可能会有一些中间路由器无法处理客户一下子发给服务器的前64个报文段(即,TCP协议的慢启动是不能跳过的)。T/TCP协议加了两条限制来解决这些问题:
1) T/TCP协议将刚开始时的发送窗口宽度设定为4096字节。在Net/3中,这就是变量snd_wnd的值。该变量控制着TCP输出流可以发出多少数据。当对等端带有窗口通告的第1个报文段到达后,窗口宽度的初始值4096将被改变为所需值。
2) 只有当对等端不在本地时,T/TCP协议才使用慢启动方式开始通信。TCP协议将snd_cwnd变量设置为1个报文段时就是慢启动。图10-14给出了本地/非本地测试程序,以内核的in_localaddr函数为基础。如果与本机拥有相同的网络号和子网号,或者虽然网络号相同子网号不同,但内核的subnetsarelocal变量值非0,这样的对等主机就是本地主机。
Net/3总是用慢启动方式开始每一条连接(卷2第721页),但这样就使客户在启动事务时无法连续发出多个报文段。折中的结果是,允许向本地的对等主机发送多个报文段,但最多4096 字节。
每次调用TCP协议的输出模块,它总是选择snd_wnd和snd_cwnd中较小的一个作为其可发送数据量的上限值。前者的初始值为TCP滑动窗口通告中的最大值,我们假设为65 535字节(如果使用窗口宽度选项,那么这个最大值可以为65 535×214,大约为1GB)。如果对等主机在本地,那么snd_wnd和snd_cwnd的初始值分别为4 096和65 535。TCP协议在连接刚开始时还未收到对方的窗口通告前,可以发出至多4096字节的数据。如果对方通告的窗口宽度为32 768字节,那么TCP协议可以持续发送数据直到对等主机的接收窗口满为止(因为 32 768和65 535的小值是32 768)。这样,TCP协议既可以避开慢启动过程,发送数据量又可以受限于对方通告的窗口宽度。
如果对等主机不在本地,那么snd_wnd的初始值仍为4096,但snd_cwnd的初始值则为1个报文段(假设保存的对等主机报文段最大长度为512)。TCP协议在连接一开始的时候只能发出一个报文段,当收到对等主机的窗口通告后,每收到一个确认,snd_wnd的值就加1。这时慢启动机制在起作用,可以发出的数据量受限于拥塞窗口,直至拥塞窗口宽度超过了对等主机通告的接收窗口。
作为一个例子,我们对第1章中的T/TCP客户和服务器程序加以修改,使请求和应答中的数据量分别为3300字节和3400字节。图3-9给出了分组交换过程。
这个例子要显示T/TCP交换的多个报文段的序列号,恰好暴露了Tcpdump的一个输出bug。第6、第8和第10个报文段的确认号应当输出3302而不是1。
图3-9 3300字节的客户请求和3400字节的服务器应答
由于客户知道服务器支持T/TCP协议,客户可以立即发出4096字节。在前2.6 ms的时间里,客户发出了第1~3个报文段。第1个报文段携带了SYN标志、1448字节数据和12字节TCP选项(MSS和CC)。第2个报文段没有带标志,只有1452字节数据和8字节TCP选项。第3个报文段携带FIN和PSH标志、8字节TCP选项以及剩余的400字节数据。第2个报文段是唯一一个没有设置任何TCP标志(共有6个标志),甚至不带ACK标志的报文段。通常情况下,ACK标志总是要携带的,除非是客户端主动打开,此时的报文段带有SYN标志(在收到服务器的报文段之前,客户是绝不能发出任何确认的)。
第4个报文段是服务器的SYN报文段,它同时也对客户所发来的所有内容做出了确认,包括SYN标志、数据和FIN标志。在第5个报文段中,客户立即确认了服务器的SYN报文段。
第6个报文段晚了40 ms才到达客户端,它携带了服务器应答的第1段数据。客户立即对此给出了确认。第8~11个报文段继续同样的过程。服务器的最后一个报文段(第10行)带有FIN标志,客户发出的最后一个ACK报文段对这最后的数据以及FIN标志做了确认。
一个问题是:为什么客户对3个服务器应答报文中的前两个立即给出了确认?是因为它们在很短的时间(44 ms)内就到达了吗?答案在TCP_REASS宏(卷2第726页)中,客户每收到一个带有数据的报文段就要调用该宏。由于连接的客户端处理完第4个报文段后就进入了FIN_WAIT_2状态,于是在TCP_REASS宏中对连接是否处于ESTABLISHED状态的测试失败,从而使客户端立即发出ACK而不是延迟一会儿再发。这一“特性”并非T/TCP协议所独有,在Net/3的程序中,如果任何一端半关闭了TCP连接而进入FIN_WAIT_1或FIN_WAIT_2状态,都会出现这种情形。从此以后,来自对等主机的每一个数据报文段都立即给予确认。TCP_REASS宏中对是否已进入ESTABLISHED状态的测试使协议无法在三次握手完成之前把数据提交给应用程序。实际上,当连接状态大于ESTABLISHED时,没有必要立刻确认按序收到的每个报文段(即,应当修改这种测试)。
TCP_NOPUSH插口选项
运行该示例程序之前需要对客户程序再做一些修改。下面这段程序打开了TCP_NOPUSH插口选项(T/TCP协议新引入的选项):
这段程序在图1-10中调用socket函数之后执行。设置该选项的目的是告诉TCP协议不要仅仅为了清空发送缓存而发送报文段。
如果要了解设置该插口选项的原因,我们必须跟踪用户进程调用sendto 函数请求发送3 300字节数据并设置MSG_EOF标志后内核所执行的动作。
1) 内核最终要调用sosend函数(卷2的16.7节)来处理输出请求。它把前2 048字节数据放入一个mbuf簇中,并向TCP协议发出一个PRU_SEND请求。
2) 于是内核调用tcp_output函数(图12-4)。由于可以发送一个满长度(full-sized)的报文段,因此发出mbuf簇中的前1448字节数据,并设置SYN标志(该报文段中包含12字节的TCP选项)。
3) 由于mbuf簇中还剩下600字节数据,于是再次循环调用tcp_output函数。我们也许会认为Nagle算法将不会使另一个报文段发送出去,但是注意卷2第681页可以看到,第1次执行tcp_output函数后,idle变量的值为1。当程序发出长为1448字节的第1个报文段后进入again分支时,idle变量没有重新计算。因此,程序在图9-3所示程序段(“发送方的糊涂窗口避免(sender silly window avoidance)”)中结束。如果idle变量为真,待发送的数据将把插口发送缓存清空,因此,决定是否发送报文段的是TF_NOPUSH标志的当前值。
在T/TCP协议引入这个标志以前,如果某个报文段要清空插口的发送缓存,并且Nagle算法允许,这段程序就总是会发送一个不满长的报文段。但是如果应用程序设置了TF_NOPUSH标志(利用新的TF_NOPUSH插口选项),这时TCP协议就不会仅仅为清空发送缓存而强迫发出数据。TCP协议将允许现有的数据与后面写操作补充来的数据结合起来,以期发出较大的报文段。
4) 如果应用程序设置了TCP_NOPUSH标志,那就不会发送报文段,tcp_output函数返回,程序执行的控制权又回到sosend函数。
如果应用程序没有设置TCP_NOPUSH标志,那么协议就发出那个600字节的报文段,并在其中设置PSH标志。
5) sosend函数把剩余的1252字节数据放入一个mbuf簇,并发出一个PRU_SEND_EOF请求(图5-2),该请求再次结束tcp_output函数的调用。然而在这次调用之前,已经调用过tcp_usrclosed函数(图12-4),使连接的状态由SYN_SENT变迁至SYN_SENT*(图12-5)。设置了TF_NOPUSH标志后,当前插口发送缓存***有1852字节的数据,于是协议又发出一个满长度的报文段,该报文段包含1452字节数据和8字节TCP选项(如图3-9所示)。之所以发出该报文段,就是因为它是满长度的(即Nagle算法不起作用)。尽管SYN_SENT*状态的标志中包含了FIN标志(图2-7),但由于发送缓存中还有额外的数据,因此FIN标志被关掉了(卷2第683页)。
6) 程序又执行了一次循环,从而再次调用tcp_output函数发送缓存中剩余的400字节数据。然而这一回FIN标志是打开的,因为发送缓存已经空了。尽管图9-3中的Nagle算法不允许发出数据,但由于设置了FIN标志,400字节的报文段还是发出去了(卷2第688页)。
本例中,设置了TCP_NOPUSH插口选项之后,在报文段最大长度为1460字节的以太网上发出一个3300字节的请求就引发出3个报文段,长度分别为1448、1452和400字节。如果不设置该选项,那么仍然会有3个报文段,但其长度分别为1448、600和1252字节。但如果请求的长度为3600字节,则设置了TCP_NOPUSH选项时产生3个报文段(长度分别为1448、1452和700字节),而不设置该选项就会产生4个报文段(长度分别为1448、600、1452和100字节)。
总之,当客户程序仅调用一次sendto函数发出请求时,通常应该设置TCP_NOPUSH插口选项。这样,当请求长度超过报文段最大长度时,协议就会尽可能发出满长度的报文段。这样可以减少报文段的数量,减少的程度取决于每次发送的数据量。
- 点赞
- 收藏
- 关注作者
评论(0)