目录
- TCP 包头格式
- TCP 的三次握手
- TCP 的四次挥手
- TCP 的可靠性与"靠谱"的哲学
- TCP流量控制
- TCP拥塞控制
上一章我们提到,UDP 就像我们小时候一样简单天真,它相信“网之初,性本善,不丢包,不乱序”,因此只提供了传输层必须的最基本字段,例如端口号,用于标识通信的双方。
但随着时间推移,我们逐渐长大,了解了“社会的复杂与残酷”,就像 TCP 协议那样成熟却复杂。相比 UDP 的简单信任,TCP 更加谨慎,它秉承“性恶论”的设计理念,默认认为网络环境是恶劣的,充满了丢包、乱序、重传和拥塞等问题。为了保证数据能够顺利送达,它从算法层面引入了许多机制来保障可靠性。
TCP 包头格式
我们先来看 TCP 头的格式。从这个图上可以看出,它比 UDP 复杂得多。
相比 UDP 的简单,TCP 包头显得复杂得多。这种复杂性正是为了应对网络的不确定性和可靠性问题。让我们逐步解析 TCP 包头,探讨它背后的设计逻辑和价值。
-
端口号:不可或缺的“地址标签”
源端口号 和 目标端口号 是必不可少的字段,与 UDP 一样,用来标识数据的来源和去向。
如果没有端口号,数据就像一封没有写收件人地址的信,完全无法送达应用程序。
这是传输层与应用层沟通的基础。 -
包的序号:解决乱序的“稳重派”
序号 是 TCP 的关键字段,作用是给数据包编号,用于解决乱序问题。
TCP 是“社会老司机”,面对数据乱序,它会一件一件地处理,确保所有数据稳重有序地到达接收方。
为什么需要序号?
如果没有编号,接收方就无法判断哪个包应该先来,哪个应该后到。通过序号,TCP 能将“错乱的数据”还原为有序的字节流。 -
确认序号:保证可靠的“承诺派”
确认序号 是 TCP 的另一大特点,主要用于实现可靠性。
每个发送的数据包都需要收到确认。若未收到确认,TCP 会重传,直到对方确实收到为止。
为什么需要确认?
IP 层不保证可靠性,包可能会丢失,而 TCP 通过“重传机制”尽力弥补丢包的不足。
这种设计体现了 TCP 的“靠谱”:承诺一定送达,即便网络环境差,也会通过努力完成任务。 -
状态位:维护连接的“社交礼仪”
TCP 的状态位是它实现“面向连接”特性的核心。
例如:SYN:发起连接时使用,表示“你好,我要建立连接了”。ACK:确认收到数据包,表示“好的,我知道了”。FIN:结束连接时使用,表示“再见,我们的通信结束了”。RST:异常情况时重置连接。
这些状态位就像“社交礼仪”:
初次见面时礼貌寒暄(SYN),交谈时互相回应(ACK),离开时正式告别(FIN)。
人与人之间的信任需要经过多次交互建立,TCP 连接的维护也同样如此。
- 窗口大小:懂分寸的“流量控制器”
TCP 的 窗口大小 是实现流量控制的重要字段。
通信双方通过窗口字段声明各自当前的处理能力,以避免以下两种情况:
- 发得太快,接收方处理不过来(“撑死”)。
- 发得太慢,浪费带宽资源(“饿死”)。
窗口控制就像老司机的“分寸感”:既能合理提出要求,又不强人所难,确保通信双方的平衡。
- 拥塞控制:知进退的“自我控制”
TCP 的拥塞控制则更进一步:
当网络环境变差(丢包、延迟增加)时,TCP 会主动降低发送速度,避免加剧网络拥堵。
为什么需要拥塞控制?
- 如果网络通路已经堵塞,再大量发包只会导致更多丢包和更高延迟。
- TCP 的拥塞控制机制让它学会“自我克制”,以保护整个网络的稳定性。
这是 TCP 的“成熟之道”:无法改变外部环境时,就通过改变自己适应环境。
重点解析 TCP 协议的五大问题
通过解析 TCP 包头,我们可以总结其核心设计思路:
- 顺序问题:稳重不乱
通过序号字段,确保数据按序到达。
- 丢包问题:承诺靠谱
通过确认序号和重传机制,保证数据可靠送达。
- 连接维护:有始有终
借助状态位(如 SYN、FIN),实现完整的连接管理。
- 流量控制:把握分寸
使用窗口大小,调节发送速率,避免“撑死”或“饿死”。
- 拥塞控制:知进知退
通过算法调整发送行为,适应网络状况,保护通信效率。
TCP 的三次握手
在网络通信中,TCP 的连接建立被称为 三次握手,是确保通信可靠性和序号同步的核心过程。这个过程看似简单,但背后蕴含了丰富的逻辑与机制。以下我们以“请求 -> 应答 -> 应答之应答”的形式逐步解析三次握手的原理、必要性及细节优化。
为什么需要三次握手?
为什么不能两次握手? 在一个不可靠的网络环境中,客户端 A 发送建立连接的请求(SYN 包),但可能发生以下情况:
请求丢失:网络传输中,SYN 包可能未抵达服务器 B。
绕路延迟:即使 SYN 包没有丢失,也可能在网络中绕路或超时。
无响应:目标服务器 B 因忙碌或配置原因未响应。
因此,A 无法直接判断连接是否建立,需要继续重发 SYN 包。假设某次请求最终到达 B,B 知道了 A 的连接请求,若 B 不愿意建立连接,A 的重试最终超时,连接失败即可。如果 B 同意连接,则 B 会回复 A 一个 SYN+ACK 包。然而:
仅两次握手存在风险:如果 B 在发送 SYN+ACK 后,A 未响应(例如 ACK 包丢失),B 将误以为连接已成功,导致连接状态的不一致。甚至,旧的 SYN 包重传还可能诱发无效连接。
因此,两次握手不足以确保双方均能正确确认连接状态。
为什么不采用四次或更多握手? 虽然增加握手次数可以提高可靠性,但网络的天然不确定性(例如丢包、延迟等)决定了握手次数过多也无法完全避免问题。事实上,TCP 设计的目标是在合理的开销内确保双方消息有去有回即可。一旦双方完成三次握手:
A 确认了 B 收到自己的 SYN 并愿意建立连接。
B 确认了 A 收到自己的 SYN+ACK 并发送了 ACK。
从此时起,双方可以安全地通信。
三次握手的过程
以下是 TCP 三次握手的具体流程及每步状态变化:
第一步:A -> B(SYN)
客户端 A 发送 SYN 包,表明希望建立连接,并处于 SYN-SENT 状态。
第二步:B -> A(SYN+ACK)
服务端 B 收到 SYN 包后,同意连接请求,同时发送自己的 SYN 包并确认 A 的 SYN(即 ACK)。此时,B 进入 SYN-RCVD 状态。
第三步:A -> B(ACK)
客户端 A 收到 B 的 SYN+ACK 后,回复 ACK,确认连接成功。此时,A 进入 ESTABLISHED 状态,同时 B 收到 ACK 后也进入 ESTABLISHED 状态,连接正式建立。
序号同步:三次握手的关键任务
三次握手不仅用于建立连接,还用于同步双方的 TCP 包序号。每个连接需要独立的序号以避免数据混淆:
- A 告诉 B:起始序号从 X 开始。
- B 告诉 A:起始序号从 Y 开始。
为什么序号不能从固定值(如 1)开始?
假设序号固定,可能发生以下问题:
A 与 B 建立连接并发送数据 {1,2,3}。因网络问题,包 3 延迟到达。
A 重新连接 B,序号再次从 1 开始,发送数据 {2,4}。此时延迟到达的旧包 3 会被误认为是当前连接的数据,导致错误。
为避免冲突,TCP 序号采用动态生成,通常以系统启动时间为基准,每 4ms 增加 1。32 位序号的重复周期接近 4 小时,而 IP 包的 TTL(生存时间)通常不足此时长,因此不会因序号重复导致数据混淆。
连接建立后的优化
在三次握手完成后,A 和 B 的通信即将开始,但仍需考虑以下问题:
-
后续数据传输解决丢包问题
如果三次握手中的最后一个 ACK 包丢失,B 不会立即关闭连接。A 后续发送的数据包会间接确认连接已建立。
如果 A 长时间不发送数据,B 可通过 KeepAlive 机制 定期发送探测包,检测连接状态。 -
空闲连接的资源释放
如果客户端 A 在建立连接后未发送数据,B 可以主动关闭长时间空闲的连接,以释放资源给其他客户端。
三次握手与状态机
三次握手对应的 TCP 状态变化如下:
-
初始状态:
客户端:CLOSED → 发送 SYN 后进入 SYN-SENT。
服务端:监听端口,处于 LISTEN 状态。 -
握手过程:
服务端收到 SYN 后,进入 SYN-RCVD,并回复 SYN+ACK。
客户端收到 SYN+ACK 后,进入 ESTABLISHED,并回复 ACK。
服务端收到 ACK 后,也进入 ESTABLISHED。
至此,双方均完成状态确认,开始正常通信。
TCP 的四次挥手
在 TCP 协议中,连接的关闭过程被称为 四次挥手。这个过程确保双方优雅断开连接,既支持双方数据的完整传输,又能避免因旧连接导致的新问题。以下以对话形式详细解析四次挥手及异常处理机制。
四次挥手的过程
四次挥手的过程对应客户端 A 和服务端 B 的状态转换,是连接终止的核心步骤:
第一步:A -> B(FIN)
A 表示自己不再发送数据,向 B 发送 FIN 包,进入 FIN_WAIT_1 状态。此时,A 可能还有一些数据需要接收。
第二步:B -> A(ACK)
B 收到 A 的 FIN 包后,回复一个 ACK 表示确认,但 B 可能还有未完成的任务,因此进入 CLOSE_WAIT 状态。此时,A 收到 ACK 后进入 FIN_WAIT_2 状态,等待 B 的 FIN 包。
第三步:B -> A(FIN)
当 B 完成数据传输后,向 A 发送 FIN 包,进入 LAST_ACK 状态,等待 A 的最终确认。
第四步:A -> B(ACK)
A 收到 B 的 FIN 包后,发送 ACK 确认连接关闭,并进入 TIME_WAIT 状态,等待一段时间以确保 B 收到 ACK。B 收到 ACK 后进入 CLOSED 状态,至此连接完全关闭。A 等待超时后也进入 CLOSED 状态。
四次挥手的设计原理
为什么需要四次挥手? 与连接建立的三次握手类似,关闭连接的四次挥手也是为了避免状态不同步问题。TCP 是全双工协议,允许双方独立关闭各自的发送通道:
- A 发送 FIN,表示不再发送数据,但仍然可以接收数据。
- B 在回复 ACK 后,可能仍有数据需要发送,因此不能立即关闭连接。
四次挥手的设计确保双方能独立完成数据的发送与接收,同时避免意外断开连接导致数据丢失。
为什么需要 TIME_WAIT 状态?
- 可靠性保障:A 在发送最后的 ACK 后,可能会因网络问题导致 B 未收到 ACK。B 会重传 FIN,A 需要处于 TIME_WAIT
状态以重新发送 ACK 确保 B 正确关闭连接。 - 防止端口复用冲突:TIME_WAIT 确保旧连接的所有数据包(包括延迟的或重复的)在网络中失效后,端口才可以重新分配,避免干扰新连接。
四次挥手的状态转换详解
四次挥手的状态转换如下:
- A 发送 FIN,进入 FIN_WAIT_1 状态:A 主动发起关闭。
- B 收到 FIN,进入 CLOSE_WAIT 状态:B 确认 A 的 FIN,但继续处理自己的事务。
- B 发送 FIN,进入 LAST_ACK 状态:B 表示自己也完成数据传输。
- A 收到 FIN,发送 ACK,进入 TIME_WAIT 状态:A 确认连接关闭,等待 2MSL 时间。
- B 收到 ACK,进入 CLOSED 状态:B 完全关闭连接。A 等待超时后进入 CLOSED 状态。
异常情况处理
A 发送 FIN 后直接关闭
如果 A 发送 FIN 后直接断开,B 无法确认连接是否已关闭,可能重试发送数据或 FIN 包,导致连接不一致。解决方法:
- A 等待 TIME_WAIT 时间:在 TIME_WAIT 期间,A 可响应 B 的重传 FIN,并重新发送 ACK 确保连接关闭。
B 未发送 FIN 或超时未收到 ACK
如果 B 在发送 FIN 后长时间未收到 A 的 ACK,B 会重传 FIN。但超过 2MSL 后,B 若仍未收到 ACK,会放弃等待并发送 RST 包强制关闭连接。
端口复用问题
如果 A 在 TIME_WAIT 状态期间立即重新使用同一端口,新应用可能收到旧连接中的延迟数据包。通过 TIME_WAIT,确保旧连接的数据包失效后再分配端口。
MSL(Maximum Segment Lifetime)的作用
MSL 是 TCP 报文在网络中生存的最大时间,通常用于确定 TIME_WAIT 的等待时间:
- 确保 ACK 能被成功接收:TIME_WAIT 持续 2 倍 MSL,足够时间覆盖 FIN 重传及 ACK 的传递。
- 避免端口复用导致的数据混淆:等待 2MSL 后,旧连接中的所有数据包都已失效,端口可安全复用。
实际应用中的 MSL:
- 协议标准规定 MSL 为 2 分钟,但实际实现中常设置为 30 秒或 1 分钟以加快端口回收。
TCP 状态机
状态机简介
TCP 状态机包含两大核心流程:
-
连接建立(握手):通过三次握手完成通信通道的初始化,确保双方对连接的状态和序号达成一致。
-
连接断开(挥手):通过四次挥手终止通信,保障数据完整传输,并防止连接复用冲突。
加粗的实线表示客户端 A 的状态变迁,加粗的虚线表示服务端 B 的状态变迁。时序以 阿拉伯数字(连接) 和 大写中文数字(断开) 表示,清晰标记了各步骤的顺序。
连接建立
- CLOSED → LISTEN(服务端启动监听) 服务端 B 启动监听端口,进入 LISTEN 状态,等待客户端连接。
- CLOSED → SYN_SENT(客户端发起连接) 客户端 A 主动发起连接,发送 SYN 包,进入 SYN_SENT 状态。
- LISTEN → SYN_RCVD(服务端收到 SYN) 服务端 B 收到 SYN 包,发送 SYN+ACK,同时进入 SYN_RCVD
状态。 - SYN_SENT → ESTABLISHED(客户端收到 SYN+ACK,回复 ACK) 客户端 A 收到 SYN+ACK 后,发送
ACK,进入 ESTABLISHED 状态。 - SYN_RCVD → ESTABLISHED(服务端收到 ACK) 服务端 B 收到客户端的 ACK 后,进入 ESTABLISHED
状态,连接建立完成。
连接断开
四次挥手的核心流程:
(一)ESTABLISHED → FIN_WAIT_1(客户端主动断开)
客户端 A 发送 FIN 包,进入 FIN_WAIT_1 状态,表明不再发送数据。
(二)ESTABLISHED → CLOSE_WAIT(服务端确认 FIN)
服务端 B 收到 FIN 包,发送 ACK 确认后进入 CLOSE_WAIT 状态。
(三)FIN_WAIT_1 → FIN_WAIT_2(客户端确认 ACK)
客户端 A 收到 ACK 后,进入 FIN_WAIT_2 状态,等待服务端的 FIN。
(四)CLOSE_WAIT → LAST_ACK(服务端主动断开)
服务端 B 完成最后的数据传输后,发送 FIN 包,进入 LAST_ACK 状态。
(五)FIN_WAIT_2 → TIME_WAIT(客户端确认 FIN)
客户端 A 收到服务端的 FIN 包后,发送 ACK 确认,进入 TIME_WAIT 状态。
(六)LAST_ACK → CLOSED(服务端关闭)
服务端 B 收到 ACK 后,进入 CLOSED 状态,连接完全关闭。
(七)TIME_WAIT → CLOSED(客户端关闭)
客户端 A 在等待 2MSL 后,确保所有残留包失效,关闭连接。
TCP 的可靠性与"靠谱"的哲学
TCP 协议以其严谨的设计和稳健的可靠性,成为互联网通信的基石。如果将 TCP 比喻成一个想要成为 “靠谱” 的人的话,那么它的每一个机制都蕴含着深刻的人生智慧。以下从现实职场管理和 TCP 的传输原理出发,优化并阐释 TCP 是如何成为一个“靠谱的人”。
玄奘出网关:公网中的挑战
玄奘西行,离开国境进入陌生的土地,需要面对未知的风险和挑战。同样,TCP 在公网传输中也需要面对网络的 不可靠性:
- 数据可能丢失:网络拥堵导致的数据包丢失。
- 数据可能延迟:数据绕远路或者等待资源。
- 数据可能重复:网络中的残留包导致重复接收。
为了在这种环境下做到 可靠传输,TCP 必须具备恒心(坚持重试)和智慧(巧妙的算法设计)。
"靠谱"的定义:TCP 的可靠传输哲学
如何成为一个靠谱的人?TCP 用自己的方式给出了答案:
- 及时响应:任务交代后,必须有应答,不让事情“石沉大海”。
- 主动重试:对未完成的任务持续跟进,直到明确结果。
- 有序管理:任务有明确的编号和顺序,防止遗漏和混乱。
- 高效协作:支持多任务并行,不浪费彼此时间。
TCP 的核心机制正是基于这些理念设计的。以下我们结合职场场景和 TCP 的工作原理,具体展开。
发送端和接收端的缓存管理及流量控制
发送端的缓存管理
发送端需要维护缓存以跟踪数据包的状态,缓存划分为四个部分:
-
已发送且确认的部分(可靠完成)
这些是发送端交代的任务,并已经收到接收端的确认,标志着任务完成,可从缓存中移除。
数据结构:LastByteAcked 记录这一部分的最后一个字节。 -
已发送但未确认的部分(待确认任务)
已发送给接收端,但尚未收到确认。TCP 会等待确认,如果超时,则重新发送。
数据结构:LastByteAcked 和 LastByteSent 之间的部分。 -
未发送但可发送的部分(计划任务)
这些任务尚未分配,但可随时发送。受限于接收端的 AdvertisedWindow,只有接收端表示可以接受时,才能发送。
数据结构:LastByteSent 和 LastByteAcked + AdvertisedWindow 之间的部分。 -
未发送且暂不可发送的部分(未来任务)
超出了接收端的处理能力,暂时无法发送的任务。
数据结构:LastByteAcked + AdvertisedWindow 之后的部分。
关键数据结构:
LastByteAcked:表示已确认部分的最后一个字节。
LastByteSent:表示已发送但未确认部分的最后一个字节。
AdvertisedWindow:接收端通告的可接收窗口,决定发送端可发送数据的范围。
接收端的缓存管理
接收端的缓存相对简单,但仍需要维护以下三部分:
-
已接收且确认的部分
接收端已成功接收并确认的数据,可供应用层使用。
数据结构:LastByteRead 记录这一部分的最后一个字节。 -
未接收但可接收的部分
接收端能够接收的最大数据量,表示其最大工作能力。
数据结构:由 NextByteExpected 和 MaxRcvBuffer 决定。 -
未接收且不可接收的部分
超出接收端缓存能力的部分,接收端无法处理。
关键数据结构:
LastByteRead:应用层已读取的最后一个字节。
NextByteExpected:下一个期望接收的字节,用于标识数据包的顺序。
MaxRcvBuffer:接收端的总缓存容量。
窗口大小计算:
AdvertisedWindow = MaxRcvBuffer - ((NextByteExpected - 1) - LastByteRead)
NextByteExpected + AdvertisedWindow 定义了可接收窗口的边界。
流量控制与流畅协作
TCP 的流量控制机制类似于项目管理中的“把握分寸”原则,通过接收端的 AdvertisedWindow 限制发送端的任务量,避免资源过载:
-
动态调整窗口大小:接收端根据自己的处理能力动态调整
-
AdvertisedWindow,通知发送端实时更新。
-
空闲与过载防控:
窗口过小:发送端过于保守,导致接收端空闲。
窗口过大:接收端超载,可能导致数据丢失。
通过窗口的动态调整,TCP 实现了任务分配的“适量性”,既不让资源闲置,也不超负荷。
有序性与累计确认
TCP 的可靠性不仅体现在数据的传输成功,还体现在数据的顺序正确性:
- 序列号:每个数据包都带有唯一的序列号,发送端和接收端通过序列号确保数据按顺序处理。
- 累计确认:接收端发送的 ACK 表示所有小于当前序列号的数据包均已收到,未确认的部分将被重传。
- 乱序处理:即使数据包乱序到达,接收端仍能根据序列号重新排序,确保数据连续性。
累计确认的优势:
- 减少 ACK 数量,提高效率。
- 允许部分乱序,提高容错能力。
顺序问题与丢包问题的总结
TCP 协议在不可靠的网络环境中,通过精细的设计解决了数据包顺序问题与丢包问题。以下从确认与重传机制的核心逻辑出发,结合具体例子,总结 TCP 的应对策略及优化设计。
顺序问题与丢包问题的例子
假设发送端和接收端的状态如下:
发送端:
1、2、3:已发送并确认,无问题。
4、5、6、7、8、9:已发送但未确认。
10、11、12:未发送但可发送。
13、14、15:接收端没有空间,不可发送。
接收端:
1、2、3、4、5:已接收并确认,但尚未被应用层读取。
6、7:未接收但可接收。
8、9:已接收但未确认(因为前面的 6、7 尚未到达)。
问题分析:
顺序问题:发送端的包 6、7 丢失,8、9 虽然到达,但因数据乱序,接收端无法确认。
丢包问题:包 5 的 ACK 丢失,导致发送端以为包 5 未被接收,可能会触发不必要的重传。
确认与重传机制
TCP 为解决顺序问题与丢包问题,设计了多种重传机制,其中包含超时重传、快速重传和选择性确认(SACK)。
1. 超时重传
当发送端发送的数据包未收到 ACK,TCP 会通过超时定时器触发重传。以下是其核心逻辑:
-
设定定时器:对每个未确认的数据包,启动定时器。
-
超时重传:如果超过预设时间未收到 ACK,重新发送该数据包。
-
自适应超时(RTT 估算):
TCP 通过采样 RTT(往返时间),动态调整超时时间。
超时时间公式:EstimatedRTT=(1−α)×EstimatedRTT+α×SampleRTT TimeoutInterval=EstimatedRTT+4×DevRTT
其中 DevRTT 表示 RTT 的波动范围,用于适配网络环境的变化。
指数退避(Exponential Backoff):
- 当网络状况恶劣时(连续超时),每次超时时间间隔加倍,避免频繁重传导致拥堵。
优点:适应网络延迟变化,确保丢失的数据最终被重传。
缺点:超时触发的周期可能较长,影响传输效率。
2. 快速重传
为了缩短等待超时的时间,TCP 提供了快速重传机制。核心逻辑:
-
冗余 ACK:
当接收端检测到乱序数据包时,发现数据中断(如丢失包 7),会重复发送 ACK。
例如,接收端收到包 8 和 9,未收到包 7,则会连续发送三个 “ACK6”,表明期望收到的是包 7。 -
触发重传:
当发送端收到 3 个相同的冗余 ACK 后,认为对应的数据包丢失,立即重传该包,而无需等待超时。
示例:
- 接收端收到 6、8、9,但未收到 7,则发送三个 ACK6。
- 发送端收到 3 个冗余 ACK6,立即重传包 7。
优点:显著缩短丢包后的恢复时间,提高网络传输效率。
缺点:只适用于乱序或轻度丢包的情况,不能标识多个不连续丢失的数据包。
TCP流量控制
流量控制机制,在对于包的确认中,同时会携带一个窗口的大小。
假设窗口不变的情况,窗口始终为 9。4 的确认来的时候,会右移一个,这个时候第 13 个包也可以发送了。
这个时候,假设发送端发送过猛,会将第三部分的 10、11、12、13 全部发送完毕,之后就停止发送了,未发送可发送部分为 0。
当对于包 5 的确认到达的时候,在客户端相当于窗口再滑动了一格,这个时候,才可以有更多的包可以发送了,例如第 14 个包才可以发送。
如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。
我们假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。
这个新的窗口 8 通过 6 的确认消息到达发送端的时候,你会发现窗口没有平行右移,而是仅仅左面的边右移了,窗口的大小从 9 改成了 8。
如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为 0。
当这个窗口通过包 14 的确认到达发送端的时候,发送端的窗口也调整为 0,停止发送。
如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。
这就是我们常说的流量控制。
TCP拥塞控制
TCP 的拥塞控制通过 拥塞窗口(cwnd) 和 接收窗口(rwnd) 协同工作,确保网络利用率的最大化,同时避免拥塞的发生。
基本概念:滑动窗口与拥塞窗口
- 滑动窗口(rwnd): 防止接收端缓存溢出,由接收端通告的可接收窗口大小。
- 拥塞窗口(cwnd): 防止网络拥堵,由发送端动态调整。
- 核心公式:
发送方已发送但未确认的字节数必须小于两个窗口中较小的那个。LastByteSent−LastByteAcked≤min(cwnd,rwnd)
TCP 如何判断网络是否满?
TCP 对网络的状态一无所知,只能通过以下反馈间接推测:
-
ACK 的返回速度: 如果 ACK 返回缓慢,可能说明网络传输延迟增加。
-
丢包或超时: 网络设备的缓存溢出会导致数据丢失。
-
发送速率与接收速率的平衡: 当发送速度大于网络或接收端处理能力时,会导致拥塞。
TCP 类比为往水管中灌水:
-
水管粗细(带宽): 每秒能传输的最大数据量。
-
水管长度(延迟): 数据从一端到另一端的往返时间(RTT)。
-
通道容量:
通道容量=带宽×RTT
如图所示,假设往返时间为 8s,去 4s,回 4s,每秒发送一个包,每个包 1024byte。已经过去了 8s,则 8 个包都发出去了,其中前 4 个包已经到达接收端,但是 ACK 还没有返回,不能算发送成功。5-8 后四个包还在路上,还没被接收。这个时候,整个管道正好撑满,在发送端,已发送未确认的为 8 个包,正好等于带宽,也即每秒发送 1 个包,乘以来回时间 8s。
拥塞控制的核心机制
-
慢启动(Slow Start)
工作原理:
初始设置:cwnd = 1。
每次收到一个 ACK,cwnd 增加 1,形成指数性增长:
第 1 轮:cwnd = 1,发送 1 个包;
第 2 轮:cwnd = 2,发送 2 个包;
第 3 轮:cwnd = 4,发送 4 个包;
第 4 轮:cwnd = 8,发送 8 个包。
指数增长直到达到慢启动阈值(ssthresh)。
优点:能够快速填满通道,适应较大的网络带宽。
结束条件:cwnd 达到 ssthresh。
检测到拥塞(例如包丢失)。 -
拥塞避免(Congestion Avoidance)
进入条件:
cwnd 超过 ssthresh。
增长方式:
从指数增长转为线性增长:
每次收到 ACK,cwnd 增加 1/cwnd
假设 cwnd = 8,收到 8 个 ACK,则 cwnd 增加 1。
下次 cwnd = 9。
目的:
缓慢提升窗口,避免网络过载。 -
拥塞检测与恢复
快速重传算法
超时重传:
触发条件: 数据包超时未收到 ACK。
处理策略:
ssthresh = cwnd / 2。
cwnd = 1,重新进入慢启动。
缺点:
重置为 1 的策略过于保守,导致传输速率骤降。
快速恢复(Fast Recovery):
触发条件: 接收端发送 3 个冗余 ACK(快速重传机制)。处理策略:
cwnd 减半:cwnd = cwnd / 2。
ssthresh 设置为当前 cwnd。
从 cwnd = ssthresh 开始线性增长,而不是重新慢启动。
优点:
避免“一夜回到解放前”,更适合高吞吐量网络。
TCP 拥塞控制的综合过程
-
阶段 1:慢启动
cwnd 从 1 开始指数增长。
达到 ssthresh 或检测到拥塞时结束。 -
阶段 2:拥塞避免
cwnd 开始线性增长。
若检测到拥塞:
超时重传: cwnd 重置为 1,重新慢启动。
快速恢复: cwnd 减半,从 ssthresh 开始线性增长。 -
阶段 3:拥塞检测
包丢失或超时:
轻微拥塞(快速重传): 减小 cwnd,但保持较高速率。
严重拥塞(超时重传): cwnd 重置为 1,重新慢启动。
示例应用
假设:
RTT = 8s,带宽 = 1 个包/s,每个包 1024 字节。
通道容量 = 8 个包。
过程:
-
第 1 秒:
发送包 1,cwnd = 1。
-
第 2 秒:
收到包 1 的 ACK,cwnd = 2,发送包 2 和 3。
-
第 3 秒:
收到包 2 和 3 的 ACK,cwnd = 4,发送包 4、5、6、7。
-
第 4 秒:
cwnd 达到 8,通道满载。
-
拥塞发生:
包丢失或超时触发快速恢复或超时重传。
TCP 的拥塞控制是动态且自适应的,通过 慢启动 和 拥塞避免 保持传输效率,通过 快速恢复 降低丢包的冲击。这种机制保证了 TCP 在复杂网络条件下的可靠性与高效性,同时最大程度地利用了网络资源
TCP 的问题与 BBR 拥塞控制算法优化总结
TCP 的传统拥塞控制机制旨在避免两个主要问题:丢包 和 缓存填满导致的超时重传。然而,传统 TCP 拥塞控制也存在以下局限性:
第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。
为了优化这两个问题,后来有了TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。