一、滑动窗口:发送窗口与接收窗口
TCP 的 "滑动窗口" 是实现连续传输(不用发一个等一个) + 流量控制(避免接收方过载) 的核心机制,本质是 "双方约定的可传输字节范围"。
我们用 "快递驿站收发包裹" 类比:
- 发送方 = 快递员(负责发包裹)
- 接收方 = 驿站(负责收包裹,有固定容量的仓库)
- 字节 = 包裹
- 窗口 = 驿站当前能接收的包裹数(接收窗口)+ 快递员能发的包裹数(发送窗口)
1. 接收窗口(Advertised Window)
接收窗口是接收方当前可用的缓冲区大小(告诉发送方 "我还能收多少"),由接收方在 ACK 报文中的 "窗口大小" 字段告知发送方。
公式:接收窗口 = 接收方TCP缓冲区大小 - 已收到但未被应用层读取的字节数
类比:驿站仓库容量是 10 个包裹(缓冲区),已收了 3 个但还没被取走→接收窗口 = 10-3=7→驿站告诉快递员 "我还能收 7 个包裹"。
2. 发送窗口(Send Window)
发送窗口是发送方当前可以发送的字节范围,由 "接收窗口" 和 "发送方缓冲区" 共同决定(取最小值,避免发送方自己缓冲区不够)。
发送窗口包含 3 个部分:
- 已发送且已确认:不用管;
- 已发送但未确认:发出去了,等接收方确认;
- 可发送但未发送:在窗口内,还没发。
公式:发送窗口 ≤ 接收窗口(接收方能力) & 发送方TCP缓冲区可用空间(发送方能力)
类比:快递员的 "可发包裹数"= 驿站说的 "还能收 7 个" → 发送窗口 = 7→快递员可以连续发 7 个包裹,不用发 1 个等 1 个。
二、窗口的滑动与流量控制
"窗口滑动" 是窗口范围随数据传输 / 确认动态变化的过程,而 "流量控制" 是通过窗口大小限制发送速率,避免接收方缓冲区溢出。
1. 窗口的滑动过程(以发送方为例)
发送方的窗口以 "已确认的字节" 为起点,随接收方的 ACK 动态前移:
- 初始状态:发送窗口 = 7,发送方发了 7 个包裹(字节 1-7);
- 接收方收到前 3 个包裹,应用层读取了这 3 个→接收窗口更新为 7-3=4,回复 ACK(确认号 = 4,窗口大小 = 4);
- 发送方窗口滑动:已确认到字节 3→窗口起点从 1 移到 4,可发送范围变为 4-10(共 7 个,但接收窗口更新为 4→实际发送窗口 = 4);
- 发送方继续发字节 4-7,循环上述过程。
2. 流量控制的核心逻辑
流量控制的目标是让发送方速率不超过接收方的处理能力:
- 当接收方缓冲区快满时(接收窗口→0),发送方会停止发送(进入 "零窗口探测");
- 当接收方处理完数据、缓冲区空出后,会发送 "窗口更新报文",告知新的接收窗口,发送方恢复传输。
反例:如果没有流量控制,发送方一直发,接收方缓冲区满后会丢弃报文→发送方重传→恶性循环,浪费带宽。
三、操作系统缓冲区与滑动窗口的关系
滑动窗口的 "物理载体" 是操作系统的 TCP 缓冲区(发送缓冲区、接收缓冲区),窗口大小直接受缓冲区配置影响。
1. 接收缓冲区与接收窗口
接收窗口的最大值由操作系统的tcp_rmem参数决定(以 Linux 为例):
tcp_rmem是接收缓冲区的 "最小 / 默认 / 最大" 大小(比如默认 87380 字节);- 接收窗口不会超过接收缓冲区的当前可用空间(即
接收缓冲区 - 未读字节数)。
2. 发送缓冲区与发送窗口
发送窗口的上限由两个因素决定:
- 接收窗口(接收方能力);
- 发送缓冲区的可用空间(由
tcp_wmem参数决定,发送缓冲区的 "最小 / 默认 / 最大" 大小)。
发送方实际发送窗口 = min(接收窗口, 发送缓冲区可用空间)
3. 配置影响示例
如果将接收方的tcp_rmem默认值从 87380 调大到 174760→接收窗口的上限翻倍→发送方可以一次发更多数据,提升大文件传输速率。
四、如何减少小报文提高网络效率
"小报文" 是指数据量远小于 MSS 的 TCP 报文(比如 1 字节数据 + 40 字节首部),会导致 "带宽利用率极低"(首部占比 98%)。常见优化手段如下:
1. Nagle 算法:合并小报文
- 原理 :发送方会缓存小报文,直到满足以下任一条件再发送:
- 缓存的数据达到 MSS;
- 收到上一批数据的 ACK;
- 触发紧急数据(URG=1)。
- 适用场景:适合 "频繁发小数据" 的场景(比如键盘输入),避免一次发 1 个字节。
- 注意 :若和 "延迟确认" 配合,可能导致延迟(比如小数据等 ACK),可通过
socket选项TCP_NODELAY禁用 Nagle(适合低延迟场景,如游戏)。
2. 延迟确认(Delayed ACK)
- 原理:接收方收到报文后,不立即回复 ACK,而是等待 200ms(或有数据要发给发送方时),合并多个 ACK 为一个。
- 作用:减少 ACK 报文的数量(比如连续收 3 个报文,只发 1 个 ACK)。
3. 避免 "糊涂窗口综合征(SWS)"
- 问题:接收方的窗口很小(比如只剩 1 字节),仍通知发送方→发送方发 1 字节的小报文,循环往复。
- 解决方法 :
- 接收方:窗口小于 MSS 的一半时,不通知发送方;
- 发送方:收到的窗口小于 MSS 时,等窗口足够大再发。
五、拥塞控制:避免网络 "堵车"
流量控制是 "管接收方",而拥塞控制是 "管网络"------ 避免多个 TCP 连接同时占用带宽,导致网络拥塞(丢包、延迟飙升)。
拥塞控制的核心是动态调整 "拥塞窗口(cwnd)" :发送方实际发送速率由min(发送窗口, 拥塞窗口)决定。
1. 慢启动(Slow Start):"轻踩油门"
- 原理 :初始拥塞窗口(cwnd)很小(比如 1 个 MSS),每次收到 ACK,cwnd指数级增长(1→2→4→8...)。
- 终止条件:cwnd 增长到 "慢启动阈值(ssthresh)"(默认 65535 字节),进入 "拥塞避免" 阶段。
- 类比:开车起步时慢慢加速,避免一脚油门踩到底导致追尾。
2. 拥塞避免(Congestion Avoidance):"平稳加速"
- 原理 :cwnd 不再指数增长,而是每轮 RTT 线性增长 1 个 MSS(比如 8→9→10...)。
- 终止条件:出现丢包(超时重传或收到 3 个重复 ACK),触发拥塞处理。
- 类比:车速到一定程度后,慢慢踩油门,保持平稳行驶。
3. 快速重传与快速恢复:"遇堵快速调整"
当收到3 个重复 ACK(说明 "报文丢了,但网络没堵"),触发以下流程:
- 快速重传:立即重传丢失的报文(不等 RTO 超时);
- 快速恢复 :
- 将慢启动阈值(ssthresh)设为当前 cwnd 的一半;
- 将 cwnd 设为新的 ssthresh;
- 直接进入 "拥塞避免" 阶段(不用回退到慢启动)。
- 优势:避免 RTO 超时导致的 cwnd 骤降,提升拥塞后的恢复速度。
拥塞控制完整示例(数值化)
假设:初始 cwnd=1MSS,ssthresh=16MSS
- 慢启动阶段:cwnd 从 1→2→4→8→16(到达 ssthresh);
- 拥塞避免阶段:cwnd 从 16→17→18→...→20;
- 收到 3 个重复 ACK:
- ssthresh=20/2=10;
- cwnd=10;
- 进入拥塞避免,cwnd 从 10→11→12...
六、SACK 与选择性重传算法
在传统的累计确认机制 中,若中间报文段丢失,发送方会重传 "丢失段及之后的所有段"(冗余重传),而 SACK(Selective Acknowledgment,选择性确认)是 TCP 的扩展选项,通过 "明确告知已收字节范围" 实现选择性重传,大幅减少冗余数据。
1. SACK 的核心作用:解决累计确认的冗余问题
累计确认的缺陷:假设发送方发送 3 个报文段:
- 段 A:字节 1-1000
- 段 B:字节 1001-2000(丢失)
- 段 C:字节 2001-3000
接收方收到 A、C 后,只能回复确认号 1001(期望段 B)→ 发送方会重传 B、C(C 已被接收,属于冗余重传)。
SACK 的改进:接收方通过SACK选项,在 ACK 报文中附加 "已接收的字节段范围"(称为 SACK 块)→ 发送方仅重传丢失的段 B,无需重传 C。
2. SACK 的工作流程
- 步骤 1:接收方标记已收字节段 接收方维护 "已接收字节的不连续区间",将这些区间封装为 SACK 块(每个块包含
起始序号和结束序号)。 - 步骤 2:发送方解析 SACK 块发送方根据 SACK 块,识别 "未被包含的字节段"(即丢失段),仅重传该段。
- 步骤 3:完成重传与确认发送方重传丢失段后,接收方回复累计确认(确认号覆盖所有已收字节)。
3. SACK 选项的格式
SACK 选项在 TCP 首部的 "选项字段" 中,格式为:
+--------+--------+--------+-----------+
| 类型(1)| 长度(1)| 块1起始(4)| 块1结束(4)|
+--------+--------+--------+-----------+
| 块2起始(4)| 块2结束(4)| ... |
+--------+--------+--------+-----------+
- 类型 = 5(标识 SACK 选项);
- 每个 SACK 块表示 "已接收的字节段"(如块 1 起始 = 1,块 1 结束 = 1001 → 表示 1-1000 字节已收到)。
4. 选择性重传示例
延续上述场景:
- 接收方收到 A(1-1000)、C(2001-3000)→ 发送 ACK(确认号 = 1001),附加 SACK 块:
[1-1001, 2001-3001]; - 发送方解析 SACK 块,发现 "1001-2001" 未被包含→ 仅重传段 B(1001-2000);
- 接收方收到 B→ 回复确认号 = 3001(覆盖所有字节),无 SACK 块。
七、从丢包到测量驱动的拥塞控制
传统拥塞控制(如慢启动、拥塞避免)的核心假设是 "丢包 = 网络拥塞 ",但这个假设在现代网络中存在明显缺陷 ------ 丢包可能是无线信号差、链路波动导致的,并非真拥塞。因此,拥塞控制逐渐从 "丢包驱动 " 转向 "测量驱动"。
1. 传统丢包驱动的问题
- 误判拥塞:无线环境中,丢包率可能高达 5% 但网络并未拥塞,传统算法会误将 cwnd 减半,降低传输效率;
- 不适应长肥管道(高带宽、高延迟):若网络带宽大、RTT 长,丢包驱动的算法会长期处于 "保守发送" 状态,无法占满带宽。
2. 测量驱动的拥塞控制核心思路
测量驱动的核心是主动测量网络的真实能力,而非被动等待丢包,关键测量指标包括:
- 瓶颈带宽:网络链路能稳定承载的最大速率(单位:字节 / 秒);
- 最小 RTT:网络无拥塞时的往返时间(反映链路的 "真实延迟");
- 队列延迟:实际 RTT - 最小 RTT(反映网络中的排队长度,队列延迟高说明拥塞)。
通过持续测量这些指标,动态调整发送速率:
- 当队列延迟低时,提高速率以占满带宽;
- 当队列延迟高时,降低速率以缓解拥塞。
八、Google BBR 拥塞控制算法原理
BBR(Bottleneck Bandwidth and RTT)是 Google 在 2016 年提出的测量驱动拥塞控制算法,核心是 "基于瓶颈带宽和最小 RTT 调整发送速率",在长肥管道、高延迟网络中表现远超传统算法(如 CUBIC)。
1. BBR 的核心思想
BBR 认为:网络的 "最大安全发送速率"= 瓶颈带宽 × 最小 RTT(即 "带宽延迟积 BDP")。
- 瓶颈带宽:通过 "发送数据量 / 传输时间" 测量;
- 最小 RTT:通过周期性探测(probe_rtt 阶段)获取,排除队列延迟的干扰。
BBR 的目标是:让发送速率稳定在 BDP 附近,同时保持队列延迟最小。
2. BBR 的四个工作阶段
BBR 的生命周期分为 4 个阶段,循环执行:
(1)启动阶段(Startup):快速探测瓶颈带宽
- 核心逻辑:cwnd指数级增长(类似慢启动),同时测量 "发送速率",直到速率不再提升(说明到达瓶颈带宽)。
- 终止条件:连续 3 个 RTT 内,发送速率的增长幅度 < 25%。
(2)排水阶段(Drain):排空网络队列
- 核心逻辑:启动阶段可能让网络队列堆积(延迟升高),因此将发送速率设为 "瓶颈带宽",让队列中的数据逐渐排空。
- 终止条件:测量到的 RTT 下降到 "接近最小 RTT"(队列排空)。
(3)ProbeBW 阶段:周期性探测带宽
- 核心逻辑:这是 BBR 的 "稳态阶段",周期性调整发送速率(在 BDP 基础上 ±25%),探测是否有额外带宽可用,同时保持队列延迟稳定。
- 执行方式:每 8 个 RTT 为一个周期,依次将 cwnd 调整为 1.25×BDP、1×BDP、1×BDP...,平衡 "带宽利用率" 和 "延迟"。
(4)ProbeRTT 阶段:周期性探测最小 RTT
- 核心逻辑:每 10 秒将 cwnd 强制设为 1 个 MSS,持续至少 1 个 RTT,测量此时的 RTT(即最小 RTT,无队列延迟),更新 BDP 的计算基准。
- 作用:避免网络环境变化(如链路切换)导致最小 RTT 过时,保证 BDP 的准确性。
3. BBR 的优势
- 适应长肥管道:在高带宽、高延迟网络(如跨洋链路)中,能占满带宽(传统算法可能仅用 30%);
- 低延迟:通过控制队列长度,大幅降低传输延迟(适合实时业务,如视频会议);
- 抗丢包:不依赖丢包作为拥塞信号,在无线环境中更稳定。
九、TCP 关闭连接优化与辅助机制
(一)关闭连接过程优化
TCP 默认通过 "四次挥手" 关闭连接,但实际场景中可通过双向同时关闭简化流程:
1. 默认四次挥手(单方主动关闭)
主动关闭方发FIN→被动方回ACK(确认 FIN)→被动方发FIN(自身无数据)→主动方回ACK(确认 FIN),主动方进入TIME-WAIT状态。
2. 优化:双方同时关闭(三次挥手)
若客户端和服务器在同一时间发起关闭请求 (都发FIN):
- 客户端发
FIN(状态从ESTABLISHED→FIN_WAIT_1),服务器同时发FIN(状态从ESTABLISHED→FIN_WAIT_1); - 双方收到对方的
FIN后,会将 "ACK(确认对方 FIN)+ 自身FIN" 合并为一个报文回复(状态从FIN_WAIT_1→CLOSING); - 双方收到对方的
ACK后,直接进入TIME-WAIT状态。
此场景下,四次挥手简化为三次交互,减少 1 个 RTT 的延迟。
(二)优化关闭连接时的 TIME-WAIT 状态
TIME-WAIT是主动关闭方在发送最后一个ACK后的状态,会持续2×MSL(报文最大生存时间,默认 1 分钟),作用是:
- 确保最后一个
ACK被对方收到(避免对方重传FIN); - 防止延迟的旧报文干扰新连接(旧报文会在 2MSL 内被网络丢弃)。
但TIME-WAIT会占用 "源 IP + 源端口 + 目的 IP + 目的端口" 的四元组,高并发场景易导致 "端口耗尽",需通过以下方式优化:
1. 端口复用(核心手段)
通过 socket 选项或系统参数允许TIME-WAIT状态的端口被复用:
SO_REUSEADDR:允许同一端口绑定多个连接(需源 IP 不同),或TIME-WAIT状态的端口被新连接复用;SO_REUSEPORT:允许同一端口被多个进程 / 线程同时绑定(需各连接的四元组唯一)。
2. 调整系统参数(以 Linux 为例)
net.ipv4.tcp_fin_timeout:缩短TIME-WAIT时长(默认 60s,可调整为 30s,避免过短导致旧报文干扰);net.ipv4.tcp_max_tw_buckets:增加TIME-WAIT连接的最大数量(默认 180000,防止超过后内核强制关闭新连接);- (已废弃)
net.ipv4.tcp_tw_recycle:快速回收TIME-WAIT连接,但会导致 NAT 环境下的连接异常,Linux 4.12 + 已移除。
(三)keepalive:TCP 保活机制
当连接长期空闲(如客户端崩溃未发FIN),keepalive可检测连接是否存活,避免 "僵死连接" 占用资源。
1. 工作原理
当连接空闲时间超过keepalive_time:
- 发送方发送keepalive 探测报文 (无数据,仅 TCP 首部,
ACK=1); - 若对方回复
ACK,则连接保持; - 若未回复,每隔
keepalive_intvl重试,共重试keepalive_probes次; - 若所有探测无响应,判定连接失效并关闭。
2. 核心系统参数(Linux)
net.ipv4.tcp_keepalive_time:空闲超时时间(默认 7200s=2 小时);net.ipv4.tcp_keepalive_intvl:探测重试间隔(默认 75s);net.ipv4.tcp_keepalive_probes:探测重试次数(默认 9 次)。
注意
keepalive是 TCP 扩展(非标准),可能被防火墙拦截;若需更灵活的保活,建议在应用层实现(如 HTTP 的 Heartbeat)。
(四)校验和:TCP 报文完整性保障
TCP 首部的 "校验和(16 位)" 是保障报文完整性与正确性的机制,校验范围包括:
- TCP 首部 + TCP 数据部分;
- 伪首部(源 IP、目的 IP、协议类型(6=TCP)、TCP 报文长度)。
计算与验证流程
- 发送方:将上述数据按 16 位分组,累加所有分组(溢出则进位),最终取反得到校验和;
- 接收方:重复上述计算,若结果为全 1(校验和匹配),则报文有效;否则丢弃报文。
作用
检测报文在传输中是否因网络噪声、设备故障等损坏,避免错误数据被应用层处理。
(五)带外数据:TCP 紧急数据传输
带外数据(Out-of-Band Data)是 TCP 传输紧急数据的机制(如中断、异常通知),通过以下字段实现:
URG标志位:置 1 表示报文包含带外数据;紧急指针:16 位字段,指向 TCP 报文中 "紧急数据的最后一个字节" 的位置。