📘 TCP 可靠传输
2022年6月20日
- 计算机网络
📘 TCP 可靠传输
1. 重传机制
1) 超时重传
- 时间驱动,在发送数据时设置个定时器,如果期限内没收到接受者的ACK就会重新发送数据
- 发生场景:
- 数据包未送达对方手中
- 数据包对方发出后,未到自己手中
- 时间相关参数:
- RTT 数据从发出到接收到对方回应的时间,包的往返时间
- RTO 超时重传时间
- RTO 时长设置:
- 若 RTO 过长,则需要过很久才能发现丢包问题,影响效率
- 若 RTO 过短,则可能数据没丢失,却认为数据发生了丢失,从而导致冗余的重发,增加网络堵塞,进而导致更多的超时 & 更多的重发,连锁反应
- 理论上,RTO 应略大于 RTT 最合适
- RTT & RTO 动态变化
- 受到网络波动的影响,RTT & RTO 并非是固定值
- Linux RTT 值取决于 该段时间内采样的 RTT 时间 & RTT 的波动范围,进行平滑处理得到
- RTO 翻倍
- 每当遇到一次超时重传后,系统会将下一次的超时时间间隔设置为之前的 2 倍
- 遇到 2 次超时,则说明网络环境较差,不适合频繁发送数据
- 因此可能会导致超时周期越来越长
- 相关参数:
net.ipv4.tcp_retries2
在 TCP 连接建立的情况下,超时重传的最大次数- 内核还会结合
最大超时时间
来判定
2) 快速重传
- 数据驱动
- 发生场景:
- 发送端接收到 3 个相同的 ACK 时,无需等待超时重传,直接返回接收端需要的 报文段
- 为什么设置为 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
- 假设现在要发送 [x - 1, x, x + 1, x + 2] 这 4 个报文段,[x - 1] 已经成功发送,那么会出现以下几种情况:
- 存在的问题:
- 当发生丢包时,是发送丢的这一个包,还是发送丢的这个包后面的所有包?
3) SACK 选择性确认
- 原理:
- 在 TCP 头部的选项中添加字段
SACK
SACK
中存放接收端已经收到了发送端传来的哪些数据- 通过这些信息,发送端在重传时,就可以有选择性的重传丢失的包
- 若 ACK = 3000, SACK = 3200-3400,那么就表示接收方希望收到 3000 开始的数据包,SACK > 3000 就表示后面的部分 3200-3400 已经接收,需要重传 3000-3199 的数据
- 在 TCP 头部的选项中添加字段
- 所需环境:
- 要支持 SACK,需要双方均支持才可以
- Linux 中可通过参数
net.ipv4.tcp_sack
打开 | 关闭该功能 - Linux 2.4 默认打开
4) D-SACK
- 作用:
- 告知发送方,哪些数据是重复发送的
- 可以判断,数据是发送方发送到接收端时丢失,还是接收端回复给发送端时丢失
- 原理:
- 使用与 SACK 一样的报文格式
- 接收端发送 ACK 时,ACK 为期望收到的发送方的序列号
- 若 ACK = 3000, SACK = 1000-1200 那么就表示接收方 3000 之前的均已经收到,而 SACK < 3000 就表示 1000-1200 是多余的数据段,而非希望重传的数据
- 相关参数:
- Linux 中可通过参数
net.ipv4.tcp_dsack
打开 | 关闭该功能 - Linux 2.4 默认打开
- Linux 中可通过参数
2. 滑动窗口
- 含义:
- 只要的滑动窗口内的数据,无需等待一个数据发送完,收到 ACK 确认,在这段时间内还可以发送窗口内的其他数据
- 窗口的本质是:
- 操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据
- 如果按期收到确认应答,此时数据就可以从缓存区清除
- 作用:
- 提高传输效率
- 即使中间有部分报文段的 ACK 丢失,也无影响,只要最后的 ACK 还在,这种模式叫做 “累计应答”
- 窗口大小如何确定?
- 窗口的大小由接收方决定
- 接收方会告知发送方自己的缓冲区还可以接收多少数据,超过这个范围接收方就无法接收了
- 发送窗口 & 接收窗口大小关系?
- 接收窗口大小 ≈ 发送窗口大小
3. 流量控制
- 含义:
- 让发送方可以根据接收方的接受能力控制发送的数据量
- 发送方根据接收方的接收窗口大小进行流量控制
- 窗口关闭情况:
- 当接收方窗口 == 0 时,会阻止发送发继续发送数据给接收方,直到窗口大小非零
- 潜在问题:
- 造成死锁
- 分析:
- 接收方回 ACK 时,接收窗口 == 0
- 当接收方处理完数据后,此时接收窗口非零,将这个 ACK 报文传送给发送方,让其继续发数据过来
- 而这个 ACK 报文,正好丢失
- 导致接收方一直等待发送方的新数据,而发送方没有收到 ACK,一直等待接收方的 ACK 报文,从而造成互相等待
- TCP 如何解决?
- TCP 为每个连接设有一个定时器
- 当 TCP 任何一方收到另一方的零窗口通知时,该定时器启动
- 定时发送窗口探测报文给另一方,而对方收到消息后,会回复自己目前能接收的窗口大小
- 若仍 == 0 则计时器重新定时,定时时间到后,继续发送探测报文
- 若 != 0 则死锁局面打破,本方不用继续等待,可以根据对方回复的窗口大小继续发送数据
- 探测次数一般 == 3 次,每次 30 - 60 s,若 3 次过后,窗口仍 == 0,有的 TCP 会发 RST 报文中断连接
- 窗口糊涂综合征:
- 接收方只能接受很小的窗口长度数据,而发送方就发送了这么短的数据
- 如何解决?
- 避免接收方回复小窗口大小
- 当接收方窗口长度 < min(MSS, 缓存空间 / 2) 时,直接回复 == 0,阻止发送发继续发送数据过来
- 避免发送短数据
- 使用
Nagle
算法进行延时处理,只有满足以下 2 个条件之一才可以:- 窗口大小 >= MSS && 可发送的数据 >= MSS
- 收到之前发送数据的 ACK 确认
- 默认打开
Nagle
算法 VS 延迟确认- 由于
Nagle
算法 在等待对方的 ACK 确认 - 而 延迟确认 会导致 ACK 确认延迟发送,例如累积多少个 ACK 才发送 ACK,因此可能出现双方互相等待的问题
- 因此 两者不可同时开启
- 由于
- 使用
- 避免接收方回复小窗口大小
4. 拥塞控制
1) 作用
- 流量控制只能确定接收方的接受能力,但不能知道当前网络的相关情况
- 如果接收方接受窗口很大,但是网络很堵,那么
- 发送方继续发送大量数据,而网络堵塞,导致数据传不过去
- 此时触发重传机制,继续发送数据
- 会导致网络更加阻塞,
- 进而导致数据更加传不过去,
- 形成恶性循环
- 拥塞控制的作用就是为了避免发送方发送的数据填满整个网络
2) 拥塞窗口
- 拥塞窗口 [
cwnd
] - 由发送方维护,根据网络的拥塞状况进行动态变化
- 发送窗口 [
swnd
] =Math.min(cwnd, rwnd)
cwnd
变化规则:- 网络没有拥塞时,
cwnd
单调递增 - 网络出现拥塞时,
cwnd
会减少
- 网络没有拥塞时,
- 如何判断网络是否拥塞?
- 根据发送方是否在指定时间内接收到 ACK 应答情况判断,如果发生了超时重传,那么说明网络可能出现了拥塞
3) 拥塞控制算法
① 慢开始
- TCP 连接刚建立后,数据包的数量一点点增大,呈指数级增长
- 发送方每收到一个 ACK,
cwnd ++
- 慢启动门限 [
ssthresh
]:cwnd < ssthresh
时,使用慢启动算法cwnd >= ssthresh
时,使用拥塞避免算法ssthresh
default == 65536 Byte
② 拥塞避免
- 发生场景:
- 慢启动后,
cwnd >= ssthresh
- 慢启动后,
- 规则:
- 每收到一个 ACK,
cwnd += 1/cwnd
,使得cwnd
保持线性增大
- 每收到一个 ACK,
- 拥塞避免阶段
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
窗口变小了
- 之前