滑动窗口

TCP 流量控制基于 滑动窗口 (连续 ARQ 协议) 实现。滑动窗口既保证了分组无差错、有序接收,也实现了流量控制。

发送方滑动窗口示意图

发送方的滑动窗口由接收方控制,实现方法为:在接收方传递给发送方的报文中加入 Window Size 信息,从而告知发送方自己的窗口大小,且发送方必须服从接收方的管理 。在 TCP 实现中,两端的窗口大小相同 。另外需要注意的是,发送方不一定会一次性发送整个窗口的数据,这根据网络情况而定。

窗口本身是一种抽象 ,发送窗口有三个变量实现,分别为:SfS_f (第一个未完成分组)SnSn (下一个待发送分组)SsizeS{size} (窗口大小)。

窗口的抽象

滑动窗口示意图

一般而言,流量控制分为两种:1)发送方输出的流量过多,接收方来不及接收。2)发送方发得太少,导致浪费带宽(数据只有一两个字节,而报头就占了几十个字节)。对于第一种则很好控制,只要发送方将每次输出的流量控制在接收方告知的窗口大小内即可;对于第二种情况(被称之为 糊涂窗口综合征 ),则相对复杂。

糊涂窗口综合征 :当发送端进程产生数据缓慢或接收端进程数据拉取缓慢时,会导致发送方以很小的段发送,这会降低信道的利用率。这个问题称之为糊涂窗口综合征。此问题可以从发送方和接收方解决:

  • 发送方: Nagle 算法,Cork 算法
  • 接收方:Clark Solution,延迟确认

延迟确认

延迟确认(Delayed Acknowledgements)用于接收方 ,其工作原理:当包到达接收方时,不立即确认,而是等待一定时间再发送确认。等待一段时间有两个好处:

  1. 此时间内接收方进程可以拉取数据,清空部分缓冲区以提供更大窗口(这样就可以使发送方一次发送更多的字节)。
  2. 接收方不必确认每一个段,减少了 pure ACK ,提高了通信效率。这基于发送方的 累积确认

需要注意,延迟确认最多延迟一次,即每两个报文就必须发送 ACK 报文;最多延迟 0.5 秒,若未等到下一个包,则直接发送 ACK 。同时,延迟确认必须同时满足以下几点才可使用

  1. 当前已经收到的但是还没有回复 ACK 的报文长度小于接收 MSS。

    这很好理解,TCP 是基于字节流的协议 ,数据没有边界,所以发送方填充数据时应该会将每个段填充到最大长度( MSS )时才会继续填充下一个。若数据长度未达 MSS,我们就可以合理猜测对方数据已经发送完(也可能是因为对方进程产生数据太慢)。

  2. 当前没有处于 quick ACK模式

  3. 当前接收窗口中没有先前接收的乱序报文

仅有延迟确认机制,不会导致请求延迟(初以为是必须等到 ACK 包发出去,recv系统调用才会返回)。一般来说,只有当该机制与Nagle算法或拥塞控制(慢启动或拥塞避免)混合作用时,才可能会导致时耗增长。

quick ACK:

与延迟 ACK 对应,Linux 还有一个 quick ACK 模式,这种 quick ack 模式下就会对每个数据包都回复一个 ACK。在连接初始建立时候收包间隔大于 RTO 时收到不在接收窗内的报文的时候 等场景下就会进入 quck ack 模式,进入 quick ac k模式的时候,会把 quick ack 计数器初始化为 16 (也有可能是小于16的某个值),这就是说随后的 16 个数据包都不采用延迟 ACK。

Nagle 算法

如果发送方 TCP 正在为一个创建数据很缓慢的进程服务,则可能引起糊涂窗口综合征。解决方法是防止发送方一个一个字节地发送数据。发送方被迫等待到将数据集成较大的数据块时再发送。那么需要等待多久?太长,延误进程;太短,数据不够长,效率低。Nagle 提供了一个简单的方法解决此问题。

Nagle 算法用于发送方 ,其方法非常简单:

  1. 即使从发送方进程接收来的数据只有 1 字节,也直接将其封装并发送。
  2. 发送第一个段后,发送方在缓冲区积累数据并等待,直到发送方接收到 ACK 或数据积累到 MMS 或包中有 FIN 再发送。

Nagle与延迟ACK引发死锁

当 Nagle 算法和延迟 ACK 同时使能的时候,可能会造成如下情况:

  1. 客户端发送第一个小包,并被服务端接收。
  2. 客户端等待 ACK 或数据长度达到 MMS。
  3. 服务端延迟确认等待下一个包直到超时(0.5s)。

这使得两端相互等待,引发 死锁(deadlock) 。这种情况下就会提升平均时延,降低网络性能,因此可能需要禁用Nagle算法或者延迟ACK算法。另外,对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性比较强的程序 ,你需要关闭这个算法。在 Socket 设置 TCP_NODELAY 选项来关闭这个算法

Cork 算法

Nagle 算法用于发送方 。Cork 算法与 Nagle 算法类似,但 Cork 算法则更为激进,一旦打开 Cork 算法,TCP 不关注是否有收到 ACK 报文,只要当前缓存中累积的数据量不满 MMS 时,就不会将数据包发出,直到一个 RTO 超时后才会把不满 MMS 的数据包发出去。

linux 中可以通过 TCP_CORK 选项来设置 Socket 打开 Cork 算法。TCP_NODELAY 选项和 TCP_CORK 选项在 Linux 早期版本是互斥的,但目前最新的 linux版本已经可以同时打开这两个选项了,但是 TCP_CORK 选项的优先级要比 TCP_NODELAY 选项的优先级要高。

CORK 与 Nagle 的区别是,Nagle 算法是尽量避免大量的小包发送,而 CORK 算法是期望完全避免发送小包(无论是大量还是少量的小包)。

Clark Solution

Clark Solution 用于接收方 。要避免接收方的糊涂窗口综合征,简单的办法就是不让接收方窗口每次只更新少量可用缓冲区,直到有较大空间时,才通知发送方窗口大小。Clark 解决方法是 只要有数据到达就发送确认,但宣布的窗口大小为零,直到窗口足够放入具有最大长度的报文段,或者至少半个缓存空间是空的

参考:CorkNagle延迟ACKTCP简介,《计算机网络自顶向下》