目前建立在TCP协议上的网络协议特别多,有telnet,ssh,有ftp,有http等等。这些协议又可以根据数据吞吐量来大致分成两大类:
对于交互性要求比较高的应用,TCP给出两个策略来提高发送效率和减低网络负担:
远程交互按键回显会产生4个报文段:
一般可以将报文段 2 和 3 进行合并—按键确认与按键回显一起发送。
这个策略是说,当主机收到远程主机的TCP数据报之后,通常不马上发送ACK数据报,而是等上一个短暂的时间,如果这段时间里面主机还有发送到远程主机的TCP数据报,那么就把这个ACK数据报“捎带”着发送出去,把本来两个TCP数据报整合成一个发送
。一般的,这个时间是200ms。可以明显地看到这个策略可以把TCP数据报的利用率提高很多。
Nagle算法是说,当主机 A 给主机 B 发送了一个 TCP 数据报并进入等待主机 B 的 ACK 数据报的状态时,TCP 的输出缓冲区里面只能有一个 TCP 数据报,并且,这个数据报不断地收集后来的数据,整合成一个大的数据报,等到 B 主机的 ACK 包一到,就把这些数据“一股脑”的发送出去。该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快。
在编写插口程序的时候,可以通过 TCP_NODELAY 来关闭这个算法。并且,使用这个算法看情况的,比如基于 TCP 的 X 窗口协议,如果处理鼠标事件时还是用这个算法,那么“延迟”可就非常大了。
流程:
对于 FTP 这样对于数据吞吐量有较高要求的要求,将总是希望每次尽量多的发送数据到对方主机,就算是有点“延迟”也无所谓。TCP 也提供了一整套的策略来支持这样的需求。TCP 协议中有 16 个 bit 表示“窗口”的大小,这是这些策略的核心。
在解释滑动窗口前,需要看看 ACK 的应答策略,一般来说,发送端发送一个TCP数据报,那么接收端就应该发送一个 ACK 数据报。但是事实上却不是这样,发送端将会连续发送数据尽量填满接受方的缓冲区,而接受方对这些数据只要发送一个 ACK 报文来回应就可以了,这就是 ACK 的累积特性(在 TCP 中,ACK 表示接收方已经正确收到了一直到确认序号减 1 的所有字节),这个特性大大减少了发送端和接收端的负担。
例子:
TCP 提供了流量控制服务以消除发送方使接收方缓存区溢出的可能性,因此可以说流量控制是一个速度匹配服务(匹配发送方的发送速率与接收方的读取速率)。
在通信过程中,接收方根据自己接收缓存的大小,动态地调整发送方的发送窗口大小,这就是接收窗口rwnd,即调整TCP报文段首部中的“窗口”字段值,来限制发送方向网络注入报文的速率。同时,发送方根据其对当前网络拥塞程序的估计而确定的窗口值,称为拥塞窗口cwnd,其大小与网络的带宽和时延密切相关。
滑动窗口本质上是描述接收方的 TCP 数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据。如果发送方收到接收方的窗口大小为 0 的 TCP 数据报,那么发送方将停止发送数据,等到接收方发送窗口大小不为 0 的数据报的到来。
在这个图中,我们将字节从 1 至 11 进行标号。接收方通告的窗口称为提出的窗口( offered window),它覆盖了从第 4 字节到第 9 字节的区域,表明接收方已经确认了包括第 3 字节在内的数据,且通告窗口大小为 6。
我们使用三个术语来描述窗口左右边沿的运动:
左边沿向右边沿
靠近为窗口合拢。这种现象发生在数据被发送和确认时。右边沿向右移动时
将允许发送更多的数据,我们称之为窗口张开。这种现象发生在另一端的接收进程读取已经确认的数据并释放了 TCP 的接收缓存时。右边沿向左移动时
,我们称之为窗口收缩。Host Requirements RFC 强烈建议不要使用这种方式。如果左边沿到达右边沿,则称其为一个零窗口,此时发送方不能够发送任何数据。
发送方不必发送一个全窗口大小的数据
。窗口的大小是相对于确认序号的
。接收方在发送一个 ACK 前不必等待窗口被填满
。在前面我们看到许多实现每收到两个报文段就会发送一个 ACK。发送端窗口随时间滑动图(不考虑重传)例如下所示:
接收端窗口通告
snd_wnd此字段主要由接收端的窗口通告决定,接收端窗口通告由当前接收端剩余多少空闲的剩余缓存决定。如下图所示:
窗口的大小是可以通过 socket 来制定的,4096 并不是最理想的窗口大小,而16384 则可以使吞吐量大大的增加。
发送方使用该标志通知接收方将所收到的数据全部提交给接收进程。
在最初的TCP规范中,一般假定编程接口允许发送进程告诉它的 TCP何时设置PUSH标志。通过允许客户应用程序通知其 TCP 设置 PUSH 标志,客户进程通知 TCP 在向服务器发送一个报文段时不要因等待额外数据而使已提交数据在缓存中滞留。类似地,当服务器的 TCP 接收到一个设置了 PUSH 标志的报文段时,它需要立即将这些数据递交给服务器进程而不能等待判断是否还会有额外的数据到达。
然而,目前大多数的 API 没有向应用程序提供通知其 TCP 设置 PUSH 标志的方法。
滑动窗口协议中,允许发送方发送多个分组(当有多个分组可用时), 而不需等待确认
,但它受限于在流水线中未确认的分组数不能超过某个最大允许数 N。
滑动窗口协议是 TCP 使用的一种控制流量的方法, 此协议能够加速数据的传输。 只有在接收窗口向前滑动时(与此同时也发送了确认), 发送窗口才有可能向前滑动。收发两端的窗口按照以上规律不断地向前滑动, 因此这种协议称为滑动窗口协议。
当发送窗口和接收窗口的大小都等于1时,就是停止等待协议。
我们之前已经简单介绍过重新发送的机制:当发送方送出一个TCP片段后,将开始计时,等待该TCP片段的ACK回复。如果接收方正确接收到符合次序的片段,接收方会利用ACK片段回复发送方。发送方得到ACK回复后,继续移动窗口,发送接下来的TCP片段。如果直到计时完成,发送方还是没有收到ACK回复,那么发送方推断之前发送的TCP片段丢失,因此重新发送之前的TCP片段。这个计时等待的时间叫做重新发送超时时间(RTO, retransmission timeout)。
发送方应该在等待多长时间之后重新发送呢?这是重新发送的核心问题。上述过程实际上有往返两个方向:1. 发送片段从发送方到接收方的传输,2. ACK片段从接收方到发送方的传输。整个过程实际耗费的时间称做往返时间(RTT, round trip time)。如果RTT是固定的,比如1秒,那么我们可以让RTO等于RTT。但实际上,RTT的上下浮动很大。
要求 RTO 精确的原因有两个:(1)如果设置的 RTO 太长会造成网络利用率不高。(2)RTO 太短会造成多次重传,使得网络阻塞。所以,书中给出了一套经验公式,和其他的保证计时器准确的措施。
最早的TCP曾经用了一个非常简单的公式来估计当前网络的状况,如下
α α 是一个推荐值为 0.9 的平滑因子, β β 通常为 2。注意,这是经验,没有推导过程,这个数值是可以被修改的。这个公式是说用旧的 RTT(R) 和新的 RTT(M) 综合到一起来考虑新的 RTT(R) 的大小。但是,我们又看到,这种估计在网络变化很大的情况下完全不能做出“灵敏的反应”,于是就有下面的修正公式:
上式中, A A 是被平滑的 RTT(均值的估计器)而 D D 则是被平滑的均值偏差。 Err E r r 是刚得到的测量结果与当前的 RTT 估计器之差。 A A 和 D D 均被用于计算下一个重传时间(RTO)。增量 g g 起平均作用,取为 1/8(0.125) 1 / 8 ( 0.125 ) 。偏差的增益是 h h ,取值为 0.25。
注意,这两组公式更新,都是在数据成功传输的情况下才进行,在发生数据重新传输的情况下,并不使用上面的公式进行网络估计,理由很简单,因为程序已经不在正常状态下了,估计出来的数据也是没有意义的。
RTO的初始化是由公式决定的,例如最初的公式,初始的值应该是1。而修正公式,初始 RTO 应该是 A+4D A + 4 D 。
当数据正常传输的情况下,我们就会用上面的公式来更新各个数据,并重开定时器,来保证下一个数据被顺利传输。要注意的是:重传的情况下,RTO 不用上面的公式计算,而采用一种叫做“指数退避”的方式。例如:当 RTO 为 1S 的情况下,发生了数据重传,我们就用 RTO=2S 的定时器来重新传输数据,下一次用 4S。一直增加到 64S 为止。
和上面的讨论差不多,就是在正常情况下,用上面的公式计算,在重传的情况下,不更新估计器的各种参数。原因还是因为估计不准确。
这不算是一个算法,这应该是一个策略,说的就是更新RTO和估计器的值的时机选择问题。当一个超时和重传发生时, 在重传数据的确认最后到达之前,不能更新 RTT 估计器,因为我们并不知道 ACK 对应哪次传输。
两句话:
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
TCP用拥塞窗口(cwnd)来进行拥塞控制,主要利用了慢启动、拥塞避免、快速重传和快速恢复这四个算法。
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。
为了便于讨论,做如下假设:
发送的最初执行慢开始,令 cwnd=1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 …
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd/2,然后重新执行慢开始。
算法具体说明:
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口 cwnd 和一个慢启动门限 ssthresh。这样得到的算法的工作过程如下:
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。
算法具体说明: