TCP流量控制机制通过动态调整窗口大小来控制发送端的操作,确保路由器/接收端消息不会溢出。
交互式TCP连接
交互式TCP连接指该连接需要在客户端和服务器之间传输用户输入信息,如按键操作、短消息、操作杆或鼠标操作。对于这些操作,如果用较小的报文段来承载信息,则传输协议需要耗费很高的代价;反之采用较大的报文段则会引入更大的延时,对延迟敏感类应用造成负面影响。因此需要权衡相关因素来找到适合的方法。
以ssh(安全外壳)应用为例,对一个ssh连接,每个交互按键通常都会生成一个单独的数据报(每个按键独立传输)。另外,ssh会在远程系统调用shell,对客户端的输入字符做出回显。客户端的每个输入字符都会生成4个TCP数据段:客户端的交互击键的确认、服务器端对击键的确认、服务器端生成的回显、客户端对该回显的确认。通常服务器端对击键的确认和服务器端生成的回显可以合并成一个信息返回。
(a)是产生4个报文段的过程,(b)是合并击键确认和回显报文后的过程。
一下是针对远程服务器键入date命令并回车的TCP连接信息图:
两张图是一样的操作,1键入d、2确认d并回显d、3对回显d的确认,依次类推至15完成date的输入并键入回车键,16~17是对日期的回显及确认,18~19是换行后对命令提示符的返回和对该数据的确认。
延时确认和Nagle算法
在许多情况下,TCP并不对每个到来的数据包都返回ACK,利用TCP的累积ACK字段可能实现这个功能。累积确认可以允许TCP延迟一段时间发送ACK,以便将ACK和相同方向上需要传的数据结合发送。主机需求RFC[RFC1122]指出,TCP实现ACK延迟的时延应小于500ms,实践中最大取200ms。
采用延时ACK的方法会减少ACK传输数目从而可以一定程度地减轻网络负载。该方法通常用于(大)批量数据传输,但在小数据包传输中,如交互式应用,则需要采取Nagle算法。
Nagle算法[RFC0896]要求,当一个TCP连接中有在传数据(即那些已发送但还未经确认的数据),小的报文段(长度小于SMSS)就不能被发送,直到所有的在传数据都收到ACK。并且在收到ACK后,TCP需要收集这些小数据,将其整合到一个报文段中发送。这种方法迫使TCP遵循停等(stop-and-wait)规程:只有等接收到所有在传数据的ACK后才能继续发送。该算法的精妙之处在于它实现了自时钟控制:ACK返回越快,数据传输越快。在相对高延迟的广域网中,更需要减少微型报的数目,该算法使得单位时间内发送的报文段数目更少,也就是说RTT控制着发包速率。
以下是Nagle算法(以ssh操作为例)禁用和启用的传输过程区别比较:
禁用Nagle:
启用Nagle:
以上两种情况的传输整理成交换过程图如下:
左边是禁用Nagle算法的传输过程,右边是启用Nagle算法后的传输过程。前者19个包,传输持续了0.58s;后者11个包,传输持续了0.8s。
启用Nagle算法后的请求和响应包随时间分布呈一定的规律性,观察每组请求/响应的传输时刻 -- 0.0、0.19、0.38、0.57,遵循一定的模式:每两个间隔为190ms,恰为连接的RTT。每发送一组请求和响应包需要等到一个RTT,这就加长了整个传输过程。Nagle算法做出了一种折中:传输的包数目更少而长度更大,但同时传输时延也更长。
流量控制和窗口管理
回想一下之前在12章介绍TCP数据结构的时候,窗口大小字段(Win,16位,以字节为单位)用于通告一个接收方的窗口大小以达到流量控制。
结合上文的数据传输列表图,可以看到发送方和接收方都带有Win字段,且都有值。发送方请求数据包携带的Win有8320、4220,表示发送且未被确认的数据大小(即发送方窗口大小);接收方响应数据包携带的Win有10800、32900,表示接收方为即将到来的新数据预留的内存存储空间(接收端窗口大小)。
TCP连接的每一端都可以收发数据,连接的收发数据量是通过一组窗口结构来维护的。每个TCP活动连接的两端都维护一个发送窗口结构和接收窗口结构。
如下是TCP发送窗口结构图:
TCP发送端滑动窗口结构记录了已确认、在传以及还未传的数据的序列号。提供窗口的大小是由接收端返回的ACK中的窗口大小字段控制的。
由接收端通告的窗口称为提供窗口,包含4~9字节;接收端已成功确认包括第3个字节在内的之前数据,并通告了一个6字节大小的窗口。窗口大小字段相对ACK号有一个自己的偏移量,发送端计算其可用窗口,即它可以立即发送的数据量,可用窗口计算值为提供窗口大小减去在传且未确认的数据值。变量SND.UNA和SND.WND分别记录窗口左边界和提供窗口值,SND.NXT记录下次发送的数据序列号,可用窗口值为SND.UNA+SND.WND-SND.NXT。
随着传输的进行,当接收到返回的数据ACK时,滑动窗口也随之右移,窗口两端的相对运动使得窗口增大或减小,可用三个术语来描述窗口左右边界的运动:
1. 关闭,窗口左边界右移。当已发送数据得到ACK确认时,窗口会减小。
2. 打开,窗口右边界右移,使得可发送数据量增大。当已确认数据得到处理,接收端可用缓存变大,窗口也随之变大。
3. 收缩,窗口右边界左移。主机需求[RFC1122]并不支持这一做法,但TCP必须能处理这一问题。
窗口左边界不能左移,因为它控制的是已确认的ACK号,具有累积性,不可返回。当得到的ACK号增大而窗口大小保持不变时,称为窗口向前滑动;随着ACK号增大窗口却减小,则左右边界距离减小,当左右边界相等时,称为零窗口。零窗口时,发送端不能再发送新数据,且TCP发送端开始探测对方窗口以伺机增大提供窗口。
接收端也维护一个窗口结构,记录了已接收并确认的数据,以及它能够接收的最大序列号,该窗口可以保证其接收数据的正确性,特别是接收端希望避免存储重复的已接收和确认的数据以及避免存储不应接收的数据。
接收窗口结构如下:
接收窗口相对发送窗口较简单。对接收端来说,到达序列号小于左窗口边界(RCV.NXT)被认为是重复数据而丢弃;超过右边界(RCV.WND+RCV.NXT)的则超出处理范围,也被丢弃;接收窗口(RCV.WND)内会接收并保存。接收窗口只有在接收到数据序列号等于左边界时,才会发生窗口前移。
当通告窗口值变为0时,可以有效的阻止发送端继续发送,直到窗口大小恢复为非零值。当接收端重新获得可用空间时,会给发送端传输一个窗口更新告知其可继续发送数据。窗口更新通常不包含数据,为纯ACK,且不能保证其传输的可靠性。如果一个包含窗口更新的ACK丢失,通信双方就会一直处于等待状态:接收方等待接收数据(已将窗口设为非零值),发送方等待收到窗口更新告知其可继续发送。
针对这个问题,TCP有以下处理方式:发送端会采用一个持续计时器间歇性地查询接收端,看其窗口是否已增长。持续计时器会触发窗口探测的传输,强制要求接收端返回ACK(其中包含了窗口大小字段)。主机需求[RFC1122]建议在一个RTO之后发送第一个窗口探测,随后以指数时间间隔发送。窗口探测包含一个自己的数据,采用TCP可靠传输,因此可以避免由窗口更新丢失导致的死锁。
基于窗口的流量控制机制,尤其是不使用大小固定的报文段的情况下可能会出现称为糊涂窗口综合征(SWS)的缺陷。当出现该问题时,交换数据段大小不是全长而是一些较小的数据段[RFC0813],由于每个报文段中有用的数据相对于头部信息的比例较小,因此耗费的资源也更多,相对应的传输效率也更低。
TCP连接的两端都可能导致SWS的出现:接收端的通告窗口较小、发送端发送的数据段较小。要避免SWS问题,必须在发送端或接收端实现相关规则:
1. 对于接收端来说,不应通告小的窗口值。[RFC1122]描述的接收算法中,在窗口增至一个全长的报文段(接收端MSS)或者接收端缓存空间的一半(取两者中较小值)之前,不能通告比当前窗口(可能为0)更大的窗口值。可能有两种情况会用到该规则:当应用程序处理接收到的数据后使得可用缓存增大;TCP接收端需要强制返回对窗口探测的响应。
2. 对于发送端来说,不应发送小的报文段,而且需要由Nagle算法控制何时发送。为避免SWS问题,只有指示满足以下条件之一时才能传输报文段:
a) 全长(发送MSS字节)的报文段可以发送。
b) 数据段长度 ≥ 接收端通告过的最大窗口值的一半的,可以发送。
c) 满足一下任一条件的都可以发送:(i)某一ACK不是目前期盼的(即没有未经确认的在传数据);(ii)该连接禁用Nagle算法。
在Windows(Vista/7及以后版本)和linux中,支持接收窗口自动调优。自动调优连接的在传输数据值需要不断被估算,通告窗口值不能小于这个值。这种方法使得TCP达到其最大可用吞吐量,而不必提前在发送端或接收端设置过大的缓存。该功能可在系统中通过命令进行配置。
参考:
《TCP IP 详解卷1:协议》