一、概述

TCP 通过三次握手建立两端之间的连接,一旦完成所有设置(如 TCBs、序列号确认等),两端就可以互相传输数据。
通常,我们总是希望数据传输得更快、更多。但是是复杂的网络背景下,需要在网络条件允许的情况下尽可能多的占满网络带宽。TCP 对端到端数据发送使用 流量控制(flow control)维持收发双方数据传输稳定和高效。所谓的 流量控制 就是让发送方的发送速率不要太快,要让接收方来得及接收。
TCP 利用 滑动窗口 实现流量控制。

  • 收发速率匹配,防止接收方被数据流淹没
  • 方法

    • 收发速率匹配 — 滑动窗口协议(它并非TCP特有,而是流量控制的一种实现方式)

      rwnd

  • _**rwnd,Receiver window, RWND,**_接收方窗口大小。

  • 该参数用于表示 接收方现存窗口大小,发送方通过该窗口大小可以调整发送的分组数量。避免出现丢包重传。
  • 单位: 字节

在 TCP 建立连接时,两端需要经过 TCP 三次握手互相交换信息,其中包括 **_rwnd_** 接收窗口大小。

swnd

MSS

  • _**Maximu Segment Size, MSS**_最大报文段长度。表示 Segment 可容纳最大的数据量,不包含 TCP 头部。
  • 单位: 字节。默认 TCP MSS 大小为 536字节
    • 在 IP 层中,数据报大小也有限制,称为 Maximum Transmission Unit, MTU,MTU 最小值为 576字节,因此得 MSS=576-20-20=536字节
  • 属于 TCP 协议定义的一个选项。在 TCP 三次握手时,收发双方协商通信时每一个报文段所能承载的最大数据长度(如果没有确定,则使用默认值 536)。如果设备希望使用更大的 MSS 值,通过 MTU 路径发现 确定两端合适的 MSS 大小。

    二、窗口说明

    image.png
    滑动窗口在概述上将字节划分以下类别:

  • 已发送并收到确认字节。#1 所示。

  • 已发送但未收到确认字节。#2 所示。
  • 未发送但准备好接收字节。#3 所示。
  • 未发送且未准备好接收字节。#4 所示。

    发送端窗口概况

    TCP滑动.png
    连接中的客户端和服务端都必须跟踪它在传输的流和正在接收的流。这是通过一组特殊的指针变量完成的。使用 3 个指针变量将 TCP 字节流切分成 4 个不同部分。指针功能如下:

  • Send Unacknowledged (SND.UNA): 已发送但尚未确认的数据的第一个字节的序列号。这标记了传输类别 #2的第一个字节; 该指针的前面的序号都表示已发送且收到确认,即类别 #1。

  • Send Next (SND.NXT): 该指针指向下一个被发送的字节序号。即类型 #3。
  • Send Window (SND.WND): 窗口的大小。

因此,已发送数据计算如下
SND.UNA+SND.WND-SND.NXT

接收端窗口概况

接收端窗口概况.png
接收窗口只需要分为三部分:

  • 已接收且 ACK 应答成功。即 #1。
  • 未被接收,但允许发送端发送。即 #2。
  • 未被接收且不允许发送端发送。即#3。

需要两个指针就可以完成划分:

  • Receive Next(RCV.NXT): 期望发送端发送的下一个数据字节的序列号。
  • Receive Window(RCV.WIND): 通知对端本连接接收窗口的大小。通常为此连接的而分配的缓冲区的大小。

    三、TCP 滑动窗口数据传输和确认机制

  • 两端都会通过传输控制块(TCP)维护 SNDRCV 指针信息。

  • 随着数据被交换,相关指定得到更新。并且交换用于 TCP 流的控制字段。其中最重要的三点是:
  • Sequence Number: 标识要传输的段中第一个字节的序列号。通常在发送数据时和 SND.UNA 的值相等。
  • Acknowledgment Number: 下一次传输所期望的序列号。此字段通常等于发送端的 RCV.NXT 指针。
  • Window: 窗口大小。对接收端来说,就是发送窗口大小。对发送端来说,就是接收窗口大小。

    TCP 滑动窗口机制示意图

  • 发送窗口大小动态可变

    • 接收方能行当前可用接收缓冲区大小
    • 发送方用该能行值调整发送窗口大小
    • 优点
      • 更加有效的传输,同时还可控制数据流量
  • 极端情况: 接收方能行的可用缓冲区 == 0

    • 发送方停止发送
    • 重新开始发送的条件
      • 收到窗口值不为 0 的通告
      • 试探性发送 — 预防通告丢失造成的通信死锁
      • 带外数据

        服务端发送指针示意图

        发送端发送指针示意图.png
        F-1 服务端发送指针示意图

        客户端发送指针示意图

        客户端发送指针示意图.png
        F-2 客户端发送指针示意图
  • 客户端请求服务端数据。要记住,这是双向传输,因此,两端都需要维护发送指针和接收指针。

    • 对客户端来说,作为请求的一方,需要维护 RCV.WNDRCV.NXT 两个指针。
    • 对服务端来说,作为发送的一方,需要维护 SND.WNDSND.NXTSND.UNA 三个指针。
  • 客户端
    • 第一次发送请求,长度为 40字节,起始序列号为1,更新 SND.NXT=141
    • 收到服务端响应 + 数据。ACK 序列号为 141,表示序列号 141 之前的数据已正常收到,更新 SND.UNA = 141。后续,客户端只响应服务端所发送的数据(即响应 ACK)。
  • 服务端

    • 第一次收到客户端请求,则更新 RCV.NXT=141
    • 第一次回复客户端,数据报中包含 ACK 序列号确认信息和数据。服务端一开始发送 80字节数据,因此,更新 SND.NXT=321,由于还没有收到 ACK 确认,所以 SND.UNA 保持不变。此时,还可以继续发送窗口内的数据,即长度为 120字节,当前数据报起始序列号为 321。
    • 收到客户端的 ACK 确认,因此,更新 SND.UNA=321,由于 SND.WND 窗口大小不变,但是 SND.UNA 增大,因此,以前不可发送的数据现在可以发送了。相当于窗口向右滑动了。
    • 服务端继续收到客户端 ACK=441 的确认报文,更新 SND.NXT=441,继续发送数据,此时长度为 160,序列号为 441。
    • 小结

  • TCP 是全双工,只要发送数据,就必须维护 SND.NXTSND.WNDSND.UNA 这三个指针,只能接收数据,就必须维护 RCV.NXTRCV.WND 这两个指针。

  • TCP 首部报文中存在 窗口字段 用于传输控制。告诉发送方自己接收窗口还有多大空间,发送方根据这个参数控制发送数据量。以实现流量控制。
  • 当然,上述介绍的是最理想的情况下,在现实世界的连接还包括以下复杂性:

    • 传输重叠。实际上,客户端和服务端可能会连续快速地互相发送许多请求和响应,客户端将会用自身包含新请求的段来确认从服务端接收的段。即 数据报中包含对上一次报文的 ACK 确认。
    • 累积确认。
    • 用于流量控制的波动窗口的大小。如果当前接收方处理速度慢,可以调整自己的接收窗口大小,并告知发送方,以便让他减慢发送速度。这就是 TCP 实现流量控制的方式。
    • 报文丢失。需要利用 TCP 重传机制 处理。
    • 避免小窗口问题。我们并不总是尽可能快的发送数据,比如来一个字节就发送,这会导致 糊涂窗口综合症。现阶段,对于 TCP 而言,内部会有一个缓存,一般等到缓存满了之后再发送。当然,你也可以通过其他方式(比如设置 PSH标志)直接发送。
    • 拥塞避免与拥塞管理。虽然滑动窗口机制用来避免 TCP 连接导致的网络拥塞并在它们在检测到拥塞时处理拥塞。

      零窗口

  • 窗口可以理解为缓存,TCP 接收数据并放入缓存并通过上层应用去取,但是出于某种情况,上层应用来不及消费数据。缓存容量大小有限。如果在一段时间内缓存数据都无法被消费,就会造成零窗口现象: 发送方发送窗口为0,接收方接收窗口为0。类似死锁。

  • TCP 使用 ZWP(Zero Window Prob)技术解决这个问题。TCP 为每一个连接设有一个 持续计时器(persistence timer)。只要 TCP 连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计算器设置的时间到期,就发送一个零窗口 探测报文段(仅携带1字节的数据)。而对方就在确认这个探测报文段时给出了现存的窗口值。
  • 一般会经过3次尝试,每次 30~60秒。如果 3 次得到的结果都为0,部分 TCP 实现会发送 RST 重置报文直接把链路切断。
  • 当重新打开关闭的窗口时,还存在一个陷阱: 刚打开的窗口太小。通常,当接收窗口太小时,这会导致生成许多小的报文段,从而大大降低 TCP 的整体效率。

零窗口.png
TCP 窗口大小调整以及流量控制

零窗口攻击

  • 只要有等待的地方都可能出现DDoS攻击,Zero Window也不例外,一些攻击者会在和HTTP建好链发完GET请求后,就把Window设置为0,然后服务端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。

    四、减少小报文发送提高网络效率

    TCP 报文段发送策略

  • 运输层应用层 提供服务,应用进程 只需要把数据传送到 TCP 的 发送缓存 后就可以放手不管了,剩下的发送任何就由 TCP 控制。

  • 可以用不同的机制来控制 TCP 报文段的发送时机。(时间+MSS大小+主动PSH
    1. TCP 维持一个变量,它等于 最大报文段长度 MSS 时,就组装成一个 TCP 报文段 送往 IP 层。
    2. 应用进程 指明要发送的报文段大小。TCP 支持主动的推送(PSH)操作。
      1. 当应用进程需要立即通过互联网发送数据时,利用 TCP 推送功能就能立即将数据推送至网络。无需等待更多数据填满缓冲区才发送。
      2. 接收端收到数据报,发送 PSH 位置为 1,因此,也会直接跳过缓冲区上报至应用进程。
      3. 虽然应用程序可以选择推送操作,但是很少使用。
    3. 发送方的一个定时器期限到了,就把当前已有的缓存数据装入报文段(但长度不能超过 MSS) 发送出去。
  • 如果发送数据量太小,会导致头重脚轻(TCP 首部最少 20 字节)。如果等待 MSS 大小才发送,有些实时交互的应用进程会明显感觉到延迟。
  • 数据优先级传输—Urgent 功能

    • 通过设置 URG=1 告诉发送方此时有紧急数据需要传输,于是发送方 TCP 就将紧急数据插入到本段报文数据段的最前方,这些数据叫做 带外数据。而在紧急数据后面就是正常的普通数据。

      糊涂窗口综合症 Silly Window Syndrome,SWS

  • RFC813

  • 存在原因
    • 通信双方的应用进程以不同速率工作时,会出现严重的性能问题。
    • 接收方: 确认报文通告小窗口。
    • 发送方: 报文段携带少量数据。
    • TCP 接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取 1 个字节,然后向发送方发送确认,并把窗口大小设置为 1 个字节
    • 接着,发送方又发来 1 个字节的数据,接收方发回确认,仍将窗口设置为 1 个字节。
  • 如果按照这样进行,网络效率极低。

糊涂窗口综合证.png
TCP 糊涂窗口综合症示意图

  • 服务端非常繁忙,无法及时清理接收缓存区,客户端每次发送数据时,服务端都会减小其接收窗口。
  • 客户端的发送窗口也会被动减小,直到仅能发送非常小的数据报,或变成零窗口。零窗口并非最坏情况,而是当服务端接收窗口只有接收 1 个字节数据时,发送端也最多只能发送一个字节,这样的网络效率是最低的。
  • 解决思路: 避免对小的滑动窗口大小作出响应,直到有足够大的窗口时再响应。可以用在发送端和接收端。
    • 接收方的 SWS 避免
      • 推迟窗口通告
        • 推迟零窗口之后,在接收窗口显著增加之前,推迟窗口的通告
          • 达到接收缓冲区的一半
          • 或达到最大报文段长度
      • 推迟确认
        • 推迟确认的发送
          • 直到窗口值增大到一定程序,或
          • 有数据要发送,或
          • 超时时限快到
    • 发送方的 SWS 避免
      • 延迟发送
        • 收集应用程序的发送数据,聚集合理的数据量
      • 延迟时间
        • 长 — 反应变慢(如对话应用程序)
        • 短 — 数据量少,吞吐率下降
      • 延迟策略: 根据当前网络性能而定
        • 自定时方式,使用确认的到达来触发报文的发送。
  • 如果 SWS 发生在接收端,则会使用 David D Clark's 方案。
  • 如果 SWS 发生在发送端,则会使用著名的 Nagle算法。该算法的思路也是延时处理

    David D Clark’s

  • 原理: 在接收端,如果收到的数据导致窗口小于某个值,就可以直接发送 ACK(0) 报文,这样就变为 零窗口状态。发送端也停止发送数据。等到接收端上层处理部分数据,此时 窗口大小≥MSS 或者 Receiver Buffer 有一半为空,就可以打开窗口让接收数据。

  • 窗口边界移动值小于 _**Min(MSS,缓存/2)**_时,通知窗口为 0。

    Nagle 算法

  • Nagle 有两个主要的条件

    • 需要等到 窗口大小≥MSS 或是 数据报大小≥ MSS。
    • 收到之前发送数据的 ACK 确认报文。
  • 在 TCP 的实现中广泛使用 Nagle 算法。算法如下:
    • 如果包长度达到 MSS,则允许发送。
    • 如果该包含有 _FIN_ ,则允许发送。
    • 设置了 TCP_NODELAY 选项,则允许发送。
    • 未设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS)均被确认,则允许发送。
    • 上述条件都未满足,但发生了超时(一般为 200ms),则立即发送。
  • TCP_NODELAY 用于关闭 Nagle 算法。

三、如何确定缓存大小

3.1 带宽时延积

image.png
image.png
[1] TCP/IP Guide.com