TCP 可靠性保证:确认、重传、流量控制、拥塞控制
TCP 为什么能保证「可靠传输」?
面试时很多人会说「TCP 是可靠协议」,但追问「怎么保证可靠的」,就答不上来了。
TCP 的可靠性靠四把「锁」:确认机制、重传机制、流量控制、拥塞控制。
TCP 可靠性的四大机制
┌─────────────────────────────────────────────────────────────┐
│ TCP 可靠性保证体系 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 确认机制 │ │ 重传机制 │ │ 流量控制 │ │
│ │ (ACK/NACK) │ │(超时/快速) │ │ (滑动窗口) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────┐ │
│ │ 拥塞控制 │ │
│ │(慢启动/避免) │ │
│ └─────────────┘ │
│ │
│ 确认机制 + 重传机制 → 保证数据不丢失 │
│ 流量控制 → 保证接收方不会溢出 │
│ 拥塞控制 → 保证网络不会拥塞 │
│ │
└─────────────────────────────────────────────────────────────┘确认机制(Acknowledgment)
基本原理
发送方每发送一个数据段,接收方都要回复一个 ACK(确认)。
发送方 接收方
│ │
│ ──── 数据段 (seq=100, len=100) ────────> │
│ │
│ <────── ACK (ack=200) ─────────────────── │
│ │
│ 确认收到 seq=100 开始的数据,共 100 字节 │
│ 下次期望收到 seq=200 │累积确认 vs 选择确认
累积确认(TCP 默认):
ACK=200 表示 seq=0-199 都已收到
即使中间丢了 seq=50-99,ACK 还是 200
发送方会重传 seq=50-199选择确认(SACK,Selective Acknowledgment):
ACK=200, SACK=[500-600, 700-800]
告诉发送方:seq=0-199 收到了,seq=500-600 和 700-800 也收到了
发送方只需要重传 seq=200-499java
// Java 启用 SACK
Socket socket = new Socket();
socket.setPerformancePreferences(0, 2, 1); // 低延迟配置捎带确认(Piggybacking)
正常情况下,ACK 是独立发送的。但如果此时接收方也要发送数据,可以把 ACK 捎带到数据段里:
发送方 ──── 数据 ────────────────────────────────────> 接收方
(带上 ACK=200)
接收方 ──── 数据 (ack=200) ─────────────────────────> 发送方
(数据带上对 seq=100 的确认)重传机制(Retransmission)
超时重传
发送方发送数据后,启动一个计时器(RTO,Retransmission Timeout)。如果在 RTO 内没收到 ACK,就重传。
发送方 ──── 数据 (seq=100) ───────────> 接收方
启动 RTO 计时器
...
RTO 超时,没收到 ACK
...
重传 ──── 数据 (seq=100) ───────────> 接收方RTO 的计算是 TCP 的精髓之一。RFC 6298 定义了标准算法:
java
// RTT 和 RTO 计算(简化版)
public class RtoCalculator {
private double srtt; // 平滑往返时间
private double rttvar; // RTT 变化
private double rto;
public RtoCalculator() {
this.srtt = 0;
this.rttvar = 0;
this.rto = 1; // 初始值 1 秒
}
// 更新 RTT
public void updateRtt(double rtt) {
if (srtt == 0) {
// 首次采样
srtt = rtt;
rttvar = rtt / 2;
} else {
// RFC 6298 公式
rttvar = 0.875 * rttvar + 0.125 * Math.abs(srtt - rtt);
srtt = 0.875 * srtt + 0.125 * rtt;
}
// RTO = 1 秒(最小值)到 60 秒(最大值)
rto = Math.max(1, Math.min(60, srtt + 4 * rttvar));
}
public double getRto() {
return rto;
}
}快速重传
超时重传等待时间太长。快速重传利用重复 ACK 来快速检测丢包:
发送方 ──── 数据 (seq=100) ────────> 收到
发送方 ──── 数据 (seq=200) ────────> 收到
发送方 ──── 数据 (seq=300) ────────> 丢失!
发送方 ──── 数据 (seq=400) ────────> 收到
接收方 ──── ACK (ack=200) ────────>
接收方 ──── ACK (ack=200) ────────> 3 个重复 ACK
接收方 ──── ACK (ack=200) ────────>
收到 3 个重复 ACK,立即重传 seq=300触发条件:收到 3 个或更多重复 ACK(TCP Reno 及之后版本)。
重传机制的配合
丢包场景下的重传策略:
1. 超时重传:最慢的兜底方案
- RTO 到达后重传
- 适用于严重丢包
2. 快速重传:快速的丢包检测
- 3 个重复 ACK 触发
- 优于超时重传
3. SACK:精准的重传
- 告诉发送方哪些收到了
- 只重传真正丢的部分流量控制(Flow Control)
滑动窗口的引入
如果发送方发得太快,接收方处理不过来,会导致数据丢失。
TCP 通过滑动窗口来控制发送方的发送速度:
┌─────────────────────────────────────────────────────────────────┐
│ 发送方滑动窗口 │
├──────────┬─────────────────────────────────────────┬────────────┤
│ 已发送 │ 可发送区域 │ 未分配 │
│ 已确认 │ 已发送未确认 │ 可发送 │ 不可发送 │ 窗口外 │
├──────────┼─────────────────────────────────────────┼────────────┤
│ seq=100 │ 100-199 │ 200-299 │ 300-399 │ seq>=400 │
│ │ 已发送 ACK │ 允许发送 │ 窗口外 │ │
│ │ 未收到 ACK │ │ │ │
└──────────┴─────────────────────────────────────────┴────────────┘
↑ ↑
左边收到 ACK 后 右边收到 ACK 后
向右滑 向右滑接收方的窗口通告
接收方在 ACK 中告诉发送方自己的窗口大小:
TCP 头部中的窗口字段:
┌──────────────────────────────────────────────────────────────┐
│ ... │ 窗口大小 (16 位) │ ... │
└──────────────────────────────────────────────────────────────┘
│
▼
接收方告诉发送方:
「我还有多少接收缓冲区空间」零窗口与窗口探测
如果接收方的缓冲区满了,会通告窗口为 0:
发送方 ──── 数据 ────────────────────────────────> 接收方
...
接收方缓冲区满,无法接收新数据
...
接收方 ──── ACK (win=0) ───────────────────────> 发送方
窗口大小为 0,暂停发送
发送方收到零窗口后,停止发送数据但发送方不能一直等,要定期探测窗口是否恢复:
发送方 ──── 窗口探测 (1 字节) ────────────────> 接收方
接收方 ──── ACK (win=?) ───────────────────────> 发送方
如果 win > 0,恢复发送拥塞控制(Congestion Control)
为什么需要拥塞控制?
流量控制解决的是「接收方处理不过来」的问题。
拥塞控制解决的是「网络处理不过来」的问题。
场景:
- 路由器缓存队列满了
- 丢包激增
- 延迟飙升
- 整个网络陷入拥塞状态
拥塞控制的目标:
- 避免往已经拥塞的网络里塞更多数据
- 在拥塞时减少发送速率
- 在空闲时逐步增加发送速率核心算法:慢启动 + 拥塞避免
┌─────────────────────────────────────────────────────────────┐
│ 拥塞控制算法 │
├─────────────────────────────────────────────────────────────┤
│ │
│ cwnd (拥塞窗口) │
│ │ │
│ │ 慢启动阶段:指数增长 │
│ │ │ │
│ 24 │ │ │
│ │ │ 拥塞避免:线性增长 │
│ 16 │ │ │ │
│ │ │ │ │
│ 12 │ │ │ 发生拥塞 │
│ │ │ │ │ │
│ 8 │ │ │ ▼ │
│ │ │ │ cwnd = ssthresh │
│ 4 │ │ │ │ │
│ │ │ │ ▼ │
│ 2 │ │ │ 减半 │
│ │ │ │ │ │
│ 1 │ │ │ ▼ │
│ └─────┴───────────────────┴────────────┴──────────────► │
│ 1 2 4 8 12 16 20 24 时间(RTT) │
│ │
│ ssthresh:慢启动阈值,到达后进入拥塞避免 │
│ │
└─────────────────────────────────────────────────────────────┘慢启动(Slow Start)
连接建立初期,cwnd 很小,不知道网络能承受多大流量。
初始 cwnd = 1 MSS(最大报文段大小)
每个 RTT 后,cwnd 加倍
cwnd = 1
↓ (1 RTT)
cwnd = 2
↓ (1 RTT)
cwnd = 4
↓ (1 RTT)
cwnd = 8
↓ ...
直到 cwnd 达到 ssthresh,进入拥塞避免拥塞避免(Congestion Avoidance)
每个 ACK,cwnd 增加 1 MSS(而不是翻倍)
相当于每个 RTT,cwnd 增加 1 MSS
增长速率:线性快速恢复(Fast Recovery)
快速重传后,不是完全从头开始,而是进入快速恢复:
Reno 算法:
1. 收到 3 个重复 ACK
2. ssthresh = cwnd / 2
3. cwnd = ssthresh + 3 * MSS(跳过 3 个报文)
4. 进入拥塞避免
Tahoe 算法(旧版):
1. 收到 3 个重复 ACK
2. ssthresh = cwnd / 2
3. cwnd = 1 MSS
4. 回到慢启动阶段拥塞检测:丢包怎么发现?
丢包的两种表现:
1. 超时:最严重的拥塞,重新慢启动
2. 快速重传:较轻的拥塞,快速恢复综合示例:TCP 可靠性在代码中的体现
java
import java.io.*;
import java.net.*;
public class ReliableTCPClient {
public static void main(String[] args) {
String host = "example.com";
int port = 80;
try (Socket socket = new Socket(host, port)) {
// 设置 socket 选项(影响 TCP 行为)
socket.setTcpNoDelay(true); // 禁用 Nagle 算法,低延迟
socket.setReceiveBufferSize(64 * 1024); // 接收缓冲区
socket.setSendBufferSize(64 * 1024); // 发送缓冲区
socket.setKeepAlive(true); // TCP 保活
socket.setSoTimeout(30000); // 读超时
// 发送 HTTP 请求
String request = "GET / HTTP/1.1\r\n" +
"Host: example.com\r\n" +
"Connection: close\r\n" +
"\r\n";
OutputStream out = socket.getOutputStream();
out.write(request.getBytes());
out.flush();
// 读取响应
InputStream in = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println("收到 " + baos.size() + " 字节");
System.out.println(new String(baos.toByteArray()));
} catch (IOException e) {
e.printStackTrace();
}
}
}查看 TCP 参数
bash
# Linux 查看 TCP 参数
sysctl -a | grep tcp
# 常用参数
net.ipv4.tcp_rmem # 接收缓冲区大小
net.ipv4.tcp_wmem # 发送缓冲区大小
net.ipv4.tcp_congestion_control # 拥塞控制算法
net.ipv4.tcp_slow_start_after_idle # 空闲后是否重置 cwnd面试追问方向
- TCP 如何保证可靠传输?有哪些机制?
- 确认机制是什么?什么是累积确认和选择确认?
- 什么是 RTO?如何计算?
- 什么是快速重传?和超时重传有什么区别?
- 什么是滑动窗口?窗口大小如何协商?
- 什么是零窗口?如何处理?
- 什么是拥塞控制?和流量控制的区别是什么?
- 慢启动、拥塞避免、快速恢复分别是怎样工作的?
- 拥塞控制算法有哪些?BBR 和 CUBIC 的区别是什么?
