📘 TCP 可靠传输

吞佛童子2022年6月20日
  • 计算机网络
  • 传输层
  • TCP
大约 8 分钟

📘 TCP 可靠传输

1. 重传机制

1) 超时重传

  1. 时间驱动,在发送数据时设置个定时器,如果期限内没收到接受者的ACK就会重新发送数据
  2. 发生场景:
    • 数据包未送达对方手中
    • 数据包对方发出后,未到自己手中
  3. 时间相关参数:
    • RTT 数据从发出到接收到对方回应的时间,包的往返时间
    • RTO 超时重传时间
  4. RTO 时长设置:
    • 若 RTO 过长,则需要过很久才能发现丢包问题,影响效率
    • 若 RTO 过短,则可能数据没丢失,却认为数据发生了丢失,从而导致冗余的重发,增加网络堵塞,进而导致更多的超时 & 更多的重发,连锁反应
    • 理论上,RTO 应略大于 RTT 最合适
  5. RTT & RTO 动态变化
    • 受到网络波动的影响,RTT & RTO 并非是固定值
    • Linux RTT 值取决于 该段时间内采样的 RTT 时间 & RTT 的波动范围,进行平滑处理得到
  6. RTO 翻倍
    • 每当遇到一次超时重传后,系统会将下一次的超时时间间隔设置为之前的 2 倍
    • 遇到 2 次超时,则说明网络环境较差,不适合频繁发送数据
    • 因此可能会导致超时周期越来越长
  7. 相关参数:
    • net.ipv4.tcp_retries2 在 TCP 连接建立的情况下,超时重传的最大次数
    • 内核还会结合 最大超时时间 来判定

2) 快速重传

  1. 数据驱动
  2. 发生场景:
    • 发送端接收到 3 个相同的 ACK 时,无需等待超时重传,直接返回接收端需要的 报文段
  3. 为什么设置为 3 ?
    • 假设现在要发送 [x - 1, x, x + 1, x + 2] 这 4 个报文段,[x - 1] 已经成功发送,那么会出现以下几种情况:
      • [x- 1, x, x + 1, x + 2] ACK[1] = x
      • [x - 1, x, x + 2, x + 1] ACK[1] = x
      • [x - 1, x + 1, x, x + 2] ACK[2] = x
      • [x - 1, x + 1, x + 2, x] ACK[3] = x
      • [x - 1, x + 2, x, x + 1] ACK[2] = x
      • [x - 1, x + 2, x + 1, x] ACK[3] = x
      • [x - 1, x + 1, x + 2] ACK[3] = x
      • [x - 1, x + 2, x + 1] ACK[3] = x
    • 什么情况下会发生收到 3 个相同的 ACK,可以看出,一共有 4 种情况,4 种情况中有 2 种是发生了丢包,而一旦丢包,ACK 相同的次数必定 == 3
  4. 存在的问题:
    • 当发生丢包时,是发送丢的这一个包,还是发送丢的这个包后面的所有包?

3) SACK 选择性确认

  1. 原理:
    • 在 TCP 头部的选项中添加字段 SACK
    • SACK 中存放接收端已经收到了发送端传来的哪些数据
    • 通过这些信息,发送端在重传时,就可以有选择性的重传丢失的包
    • 若 ACK = 3000, SACK = 3200-3400,那么就表示接收方希望收到 3000 开始的数据包,SACK > 3000 就表示后面的部分 3200-3400 已经接收,需要重传 3000-3199 的数据
  2. 所需环境:
    • 要支持 SACK,需要双方均支持才可以
    • Linux 中可通过参数 net.ipv4.tcp_sack 打开 | 关闭该功能
    • Linux 2.4 默认打开

4) D-SACK

  1. 作用:
    • 告知发送方,哪些数据是重复发送的
    • 可以判断,数据是发送方发送到接收端时丢失,还是接收端回复给发送端时丢失
  2. 原理:
    • 使用与 SACK 一样的报文格式
    • 接收端发送 ACK 时,ACK 为期望收到的发送方的序列号
    • 若 ACK = 3000, SACK = 1000-1200 那么就表示接收方 3000 之前的均已经收到,而 SACK < 3000 就表示 1000-1200 是多余的数据段,而非希望重传的数据
  3. 相关参数:
    • Linux 中可通过参数 net.ipv4.tcp_dsack 打开 | 关闭该功能
    • Linux 2.4 默认打开

2. 滑动窗口

  1. 含义:
    • 只要的滑动窗口内的数据,无需等待一个数据发送完,收到 ACK 确认,在这段时间内还可以发送窗口内的其他数据
    • 窗口的本质是:
      • 操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据
      • 如果按期收到确认应答,此时数据就可以从缓存区清除
  2. 作用:
    • 提高传输效率
    • 即使中间有部分报文段的 ACK 丢失,也无影响,只要最后的 ACK 还在,这种模式叫做 “累计应答”
  3. 窗口大小如何确定?
    • 窗口的大小由接收方决定
    • 接收方会告知发送方自己的缓冲区还可以接收多少数据,超过这个范围接收方就无法接收了
  4. 发送窗口 & 接收窗口大小关系?
    • 接收窗口大小 ≈ 发送窗口大小

3. 流量控制

  1. 含义:
    • 让发送方可以根据接收方的接受能力控制发送的数据量
    • 发送方根据接收方的接收窗口大小进行流量控制
  2. 窗口关闭情况
    • 当接收方窗口 == 0 时,会阻止发送发继续发送数据给接收方,直到窗口大小非零
    • 潜在问题:
      • 造成死锁
      • 分析:
        • 接收方回 ACK 时,接收窗口 == 0
        • 当接收方处理完数据后,此时接收窗口非零,将这个 ACK 报文传送给发送方,让其继续发数据过来
        • 而这个 ACK 报文,正好丢失
        • 导致接收方一直等待发送方的新数据,而发送方没有收到 ACK,一直等待接收方的 ACK 报文,从而造成互相等待
    • TCP 如何解决?
      • TCP 为每个连接设有一个定时器
      • 当 TCP 任何一方收到另一方的零窗口通知时,该定时器启动
      • 定时发送窗口探测报文给另一方,而对方收到消息后,会回复自己目前能接收的窗口大小
      • 若仍 == 0 则计时器重新定时,定时时间到后,继续发送探测报文
      • 若 != 0 则死锁局面打破,本方不用继续等待,可以根据对方回复的窗口大小继续发送数据
      • 探测次数一般 == 3 次,每次 30 - 60 s,若 3 次过后,窗口仍 == 0,有的 TCP 会发 RST 报文中断连接
  3. 窗口糊涂综合征
    • 接收方只能接受很小的窗口长度数据,而发送方就发送了这么短的数据
    • 如何解决?
      • 避免接收方回复小窗口大小
        • 当接收方窗口长度 < min(MSS, 缓存空间 / 2) 时,直接回复 == 0,阻止发送发继续发送数据过来
      • 避免发送短数据
        • 使用 Nagle 算法进行延时处理,只有满足以下 2 个条件之一才可以:
          • 窗口大小 >= MSS && 可发送的数据 >= MSS
          • 收到之前发送数据的 ACK 确认
          • 默认打开
        • Nagle 算法 VS 延迟确认
          • 由于 Nagle 算法 在等待对方的 ACK 确认
          • 而 延迟确认 会导致 ACK 确认延迟发送,例如累积多少个 ACK 才发送 ACK,因此可能出现双方互相等待的问题
          • 因此 两者不可同时开启

4. 拥塞控制

1) 作用

  1. 流量控制只能确定接收方的接受能力,但不能知道当前网络的相关情况
  2. 如果接收方接受窗口很大,但是网络很堵,那么
    • 发送方继续发送大量数据,而网络堵塞,导致数据传不过去
    • 此时触发重传机制,继续发送数据
    • 会导致网络更加阻塞,
    • 进而导致数据更加传不过去,
    • 形成恶性循环
  3. 拥塞控制的作用就是为了避免发送方发送的数据填满整个网络

2) 拥塞窗口

  • 拥塞窗口 [cwnd]
  • 由发送方维护,根据网络的拥塞状况进行动态变化
  • 发送窗口 [swnd] = Math.min(cwnd, rwnd)
  • cwnd 变化规则:
    • 网络没有拥塞时,cwnd 单调递增
    • 网络出现拥塞时,cwnd 会减少
  • 如何判断网络是否拥塞?
    • 根据发送方是否在指定时间内接收到 ACK 应答情况判断,如果发生了超时重传,那么说明网络可能出现了拥塞

3) 拥塞控制算法

img.png

① 慢开始

  • TCP 连接刚建立后,数据包的数量一点点增大,呈指数级增长
  • 发送方每收到一个 ACK,cwnd ++
  • 慢启动门限 [ssthresh]:
    • cwnd < ssthresh 时,使用慢启动算法
    • cwnd >= ssthresh 时,使用拥塞避免算法
    • ssthresh default == 65536 Byte

② 拥塞避免

  • 发生场景:
    • 慢启动后, cwnd >= ssthresh
  • 规则:
    • 每收到一个 ACK,cwnd += 1/cwnd,使得 cwnd 保持线性增大
  • 拥塞避免阶段 cwnd 虽然增长速度变慢,但也是增长,因此增长到一定阶段后,就会造成网络拥塞

③ 快重传

  • 发生场景:
    • 网络出现拥塞,就会发生数据包的丢包 | 延时,进而触发重传机制
  • 超时重传
    • ssthresh = cwnd / 2
    • cwnd 变为初始值,假设 = 1,Linux 的 cwnd 初始值 = 10
    • 这时就会触发慢启动过程
  • 快速重传
    • cwnd = cwnd / 2
    • ssthresh = cwnd
    • 这时就会触发快恢复过程

④ 快恢复

  • cwnd = ssthresh + 3
  • 重传丢失的数据包
  • 若再收到重复的 ACK,cwnd ++
  • 若收到新的 ACK,cwnd = ssthresh,再次进入拥堵避免阶段
    • 之前 cwnd = ssthresh + 3,现在 cwnd = ssthresh 可以看出 cwnd 窗口变小了

5. 序列号 & 确认号 & 校验和

上次编辑于: 2022/10/10 下午8:43:48
贡献者: liuxianzhishou