2.TCP深度解析:握手、挥手、状态机、流量与拥塞控制
文章目录
- TCP深度解析:握手、挥手、状态机、流量与拥塞控制
- 1. TCP三次握手(Three-Way Handshake)
- 握手过程详解
- 为什么需要三次握手?
- 代码实现视角
- 2. TCP四次挥手(Four-Way Handshake)
- 挥手过程详解
- 为什么需要四次挥手?
- TIME_WAIT状态的重要性
- 3. TCP状态转换图
- 完整状态机
- 状态持久化时间
- 4. 流量控制(Flow Control) - 滑动窗口(Sliding Window)
- 窗口机制原理
- 零窗口和糊涂窗口综合征
- 5. 拥塞控制(Congestion Control)
- 拥塞控制状态机
- 5.1 慢启动(Slow Start)
- 5.2 拥塞避免(Congestion Avoidance)
- 5.3 快速重传和快速恢复
- 5.4 现代拥塞控制算法
- BBR(Bottleneck Bandwidth and Round-trip time)
- CUBIC
- 6. 实际参数和调优
- Linux TCP参数调优
- 7. 常见问题
- Q1: 为什么是三次握手而不是两次?
- Q2: TIME_WAIT状态为什么要等待2MSL?
- Q3: 滑动窗口和拥塞窗口的区别?
- Q4: 慢启动为什么是指数增长?
- Q5: 什么是快速重传?
- 总结
TCP深度解析:握手、挥手、状态机、流量与拥塞控制
1. TCP三次握手(Three-Way Handshake)
握手过程详解
目的:同步连接双方的初始序列号(ISN),确认双方的收发能力正常。
过程:
- SYN (Client -> Server):
- 客户端发送一个SYN包(
SYN=1
),并选择一个初始序列号seq = J
。 - 客户端状态变为 SYN_SENT。
- 客户端发送一个SYN包(
- SYN-ACK (Server -> Client):
- 服务器收到SYN包后,分配资源,并发送SYN-ACK包(
SYN=1, ACK=1
)。 - 确认号
ack = J + 1
,并选择自己的初始序列号seq = K
。 - 服务器状态变为 SYN_RCVD。
- 服务器收到SYN包后,分配资源,并发送SYN-ACK包(
- ACK (Client -> Server):
- 客户端收到SYN-ACK后,分配资源,发送ACK包(
ACK=1
)。 - 序列号
seq = J + 1
,确认号ack = K + 1
。 - 客户端状态变为 ESTABLISHED。
- 服务器收到ACK后,状态也变为 ESTABLISHED。连接建立成功。
- 客户端收到SYN-ACK后,分配资源,发送ACK包(
为什么需要三次握手?
核心原因:防止已失效的连接请求报文突然到达,导致服务器资源被浪费。
- 场景:一个SYN报文滞留在网络中长期无效,客户端超时重传并成功建立连接。之后,无效的SYN报文终于到达服务器。
- 两次握手:服务器会认为这是一个新的连接请求,直接回复SYN-ACK并进入连接状态,导致服务器资源空等。
- 三次握手:客户端收到这个陈旧的SYN-ACK后,会发现自己并未请求连接,会发送RST包重置,服务器不会进入连接状态。
代码实现视角
// 客户端视角
void client_connect() {// 发送SYNsend_packet(SYN_FLAG, local_seq++);state = SYN_SENT;// 等待SYN-ACKwhile (state != ESTABLISHED) {packet = receive_packet();if (packet.has(SYN|ACK) && packet.ack == local_seq) {send_packet(ACK_FLAG, local_seq++, packet.seq + 1);state = ESTABLISHED;}}
}// 服务器视角
void server_listen() {while (true) {packet = receive_packet();if (packet.has(SYN)) {send_packet(SYN|ACK, local_seq++, packet.seq + 1);state = SYN_RCVD;// 等待ACKpacket = receive_packet();if (packet.has(ACK) && packet.ack == local_seq) {state = ESTABLISHED;}}}
}
2. TCP四次挥手(Four-Way Handshake)
挥手过程详解
目的:双方都同意关闭全双工连接。
过程:
- FIN (主动关闭方 -> 被动关闭方):
- 主动方发送FIN包(
FIN=1
),序列号为seq = U
。 - 主动方状态由 ESTABLISHED 变为 FIN_WAIT_1。
- 主动方发送FIN包(
- ACK (被动关闭方 -> 主动关闭方):
- 被动方收到FIN后,发送ACK包(
ACK=1
),确认号ack = U + 1
。 - 被动方状态变为 CLOSE_WAIT。
- 主动方收到ACK后,状态变为 FIN_WAIT_2。
- 此时,从主动方到被动方的连接已关闭。但反向连接仍然可用。
- 被动方收到FIN后,发送ACK包(
- FIN (被动关闭方 -> 主动关闭方):
- 被动方处理完所有数据后,发送自己的FIN包(
FIN=1
),序列号seq = V
。 - 被动方状态变为 LAST_ACK。
- 被动方处理完所有数据后,发送自己的FIN包(
- ACK (主动关闭方 -> 被动关闭方):
- 主动方收到FIN后,发送ACK包(
ACK=1
),确认号ack = V + 1
。 - 主动方状态变为 TIME_WAIT。
- 被动方收到ACK后,状态变为 CLOSED。
- 主动方在 TIME_WAIT 状态等待 2MSL(两倍最大报文段生存时间)后,状态变为 CLOSED。
- 主动方收到FIN后,发送ACK包(
为什么需要四次挥手?
// TCP是全双工协议,每个方向都需要单独关闭
void tcp_teardown_reason() {// 第一次挥手:客户端说"我没有数据要发了"// 第二次挥手:服务器说"我知道你要关了"// 第三次挥手:服务器说"我也没有数据要发了" // 第四次挥手:客户端说"我知道你要关了"
}
TIME_WAIT状态的重要性
// 等待2MSL(Maximum Segment Lifetime)的原因:
void time_wait_importance() {// 1. 确保最后一个ACK能够到达对方// - 如果ACK丢失,对方会重传FIN// - 客户端在TIME_WAIT状态能够处理重传的FIN// 2. 让旧连接的重复报文在网络中消失// - 避免影响新的连接(相同四元组)(源IP、源端口、目的IP、目的端口)的连接错误接收。// MSL通常为30秒到2分钟,2MSL就是1到4分钟
}
3. TCP状态转换图
完整状态机
必须理解的核心状态:
- LISTEN:服务器等待SYN的状态。
- SYN_SENT:客户端已发送SYN,等待SYN-ACK。
- SYN_RCVD:服务器已发送SYN-ACK,等待ACK。
- ESTABLISHED:连接已建立,可数据传输。
- FIN_WAIT_1:主动关闭方已发送FIN。
- FIN_WAIT_2:主动关闭方已收到对FIN的ACK。
- CLOSE_WAIT:被动关闭方收到FIN,并已回复ACK。
- LAST_ACK:被动关闭方已发送自己的FIN。
- TIME_WAIT:主动关闭方发送完最后一个ACK,等待2MSL。
- CLOSED:连接完全关闭。
记忆技巧:理解握手和挥手的过程,状态转换自然就记住了。
重要的状态转换:客户端状态流:
CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED服务器状态流:
CLOSED → LISTEN → SYN_RCVD → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED异常情况:
- 同时打开:SYN_SENT → SYN_RCVD → ESTABLISHED
- 同时关闭:FIN_WAIT_1 → CLOSING → TIME_WAIT → CLOSED
状态持久化时间
// 各种状态的超时时间
struct tcp_timeouts {time_t syn_sent_timeout = 75; // SYN_SENT超时(秒)time_t syn_rcvd_timeout = 75; // SYN_RCVD超时(秒)time_t fin_wait_2_timeout = 60; // FIN_WAIT_2超时(秒)time_t time_wait_timeout = 60; // TIME_WAIT超时(2*MSL)time_t close_wait_timeout = 3600; // CLOSE_WAIT超时(应用控制)
};
4. 流量控制(Flow Control) - 滑动窗口(Sliding Window)
窗口机制原理
目的:解决发送方和接收方速率不匹配的问题,防止发送过快导致接收方缓冲区溢出。
机制:滑动窗口协议。
- 接收方在每次发送ACK时,都会通过 TCP首部中的“窗口大小”字段 告知发送方自己当前还能接收多少数据(接收窗口,
rwnd
)。 - 发送方的发送窗口大小不能超过接收方通告的窗口大小。
- 这个窗口是动态“滑动”的:接收方处理完数据,窗口右移,通告新的窗口大小;发送方收到确认,发送窗口也相应右移。
核心:接收方通过控制rwnd
来掌控发送方的发送速率。
零窗口和糊涂窗口综合征
// 零窗口处理
void handle_zero_window() {// 当接收方窗口为0时,发送方停止发送// 定期发送零窗口探测报文send_probe_packet(1); // 只发送1字节探测
}// 避免糊涂窗口综合征(Silly Window Syndrome)
void avoid_silly_window() {// 接收方策略:等待窗口达到一定大小再通告if (free_buffer < MIN_WINDOW_SIZE) {advertise_window(0); // 通告零窗口} else {advertise_window(free_buffer);}// 发送方策略:积累足够数据再发送if (data_to_send < MSS && !urgent) {wait_for_more_data();}
}
5. 拥塞控制(Congestion Control)
拥塞控制状态机
慢启动 → 拥塞避免 → 快速恢复
5.1 慢启动(Slow Start)
- 目的:初始阶段快速探测网络容量。
- 行为:连接刚建立时,
cwnd = 1 MSS
(一个最大报文段长度)。每收到一个ACK,cwnd
就加倍(指数增长)。 - 结束条件:
cwnd
超过慢启动阈值(ssthresh
) 时,进入拥塞避免阶段;或者遇到拥塞(超时)。
// 慢启动算法
void slow_start(struct tcp_connection* conn) {// 初始拥塞窗口(initcwnd):通常为2-10个MSSconn->cwnd = initial_cwnd;conn->ssthresh = 65535; // 初始慢启动阈值// 每个ACK到达时:cwnd = cwnd + 1*MSS// 效果:每个RTT时间窗口翻倍(指数增长)while (conn->cwnd < conn->ssthresh) {// 发送cwnd大小的数据send_data(conn->cwnd);// 等待ACK,每收到一个ACK:conn->cwnd += MSS;}// 进入拥塞避免阶段conn->state = CONGESTION_AVOIDANCE;
}
5.2 拥塞避免(Congestion Avoidance)
- 目的:避免很快再次出现拥塞。
- 行为:当
cwnd >= ssthresh
时,每收到一个ACK,cwnd
增加 1/cwnd(线性增长,每RTT时间cwnd
增加1个MSS)。
// 拥塞避免算法
void congestion_avoidance(struct tcp_connection* conn) {// 每个RTT时间:cwnd = cwnd + 1*MSS// 效果:线性增长// 每个ACK到达时:cwnd = cwnd + MSS*(MSS/cwnd)conn->cwnd += (MSS * MSS) / conn->cwnd;// 检测拥塞:超时或重复ACKif (packet_loss_detected()) {conn->ssthresh = max(2, conn->cwnd / 2);conn->cwnd = 1;conn->state = SLOW_START;}
}
5.3 快速重传和快速恢复
快速重传:
- 目的:在定时器超时之前尽早重传丢失的报文。
- 行为:如果发送方连续收到3个重复的ACK,就推断某个报文段丢失,立即重传该报文,而不必等待超时。
快速恢复:
- 行为:在快重传之后触发。
ssthresh = cwnd / 2
cwnd = ssthresh
(有的实现是cwnd = ssthresh + 3
,因为收到3个重复ACK表明有3个数据包离开了网络)- 然后直接进入拥塞避免阶段(线性增长)。
- 好处:避免了超时后
cwnd
被重置为1,性能更好。
// 快速重传算法
void fast_retransmit(struct tcp_connection* conn) {// 收到3个重复ACK时,认为报文丢失if (dup_ack_count >= 3) {// 立即重传丢失的报文retransmit_lost_packet();// 进入快速恢复conn->ssthresh = max(2, conn->cwnd / 2);conn->cwnd = conn->ssthresh + 3 * MSS;conn->state = FAST_RECOVERY;}
}// 快速恢复算法
void fast_recovery(struct tcp_connection* conn) {// 每收到一个重复ACK:cwnd = cwnd + MSS// 当新数据的ACK到达时:cwnd = ssthreshif (new_ack_received) {conn->cwnd = conn->ssthresh;conn->state = CONGESTION_AVOIDANCE;}
}
5.4 现代拥塞控制算法
BBR(Bottleneck Bandwidth and Round-trip time)
// Google的BBR算法
void bbr_algorithm(struct tcp_connection* conn) {// 基于带宽和延迟的拥塞控制estimate_bandwidth_delay(); // 估计带宽和RTT// 动态调整发送速率conn->pacing_rate = estimated_bandwidth * gain;conn->cwnd = estimated_bandwidth * min_rtt * gain;
}
CUBIC
// Linux默认的CUBIC算法
void cubic_algorithm(struct tcp_connection* conn) {// 使用三次函数调整窗口大小// 更公平,更适合高速网络conn->cwnd = C * (t - K)^3 + W_max;
}
6. 实际参数和调优
Linux TCP参数调优
# 查看当前TCP参数
sysctl -a | grep tcp# 调整拥塞控制算法
sysctl -w net.ipv4.tcp_congestion_control=cubic# 调整初始拥塞窗口
sysctl -w net.ipv4.tcp_initcwnd=10# 调整接收窗口大小
sysctl -w net.core.rmem_max=16777216
sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216'# 调整TIME_WAIT超时(谨慎使用)
sysctl -w net.ipv4.fin_timeout=30
7. 常见问题
Q1: 为什么是三次握手而不是两次?
答:第三次握手确认客户端的接收能力正常,防止已失效的连接请求报文突然传到服务器导致错误连接建立。
Q2: TIME_WAIT状态为什么要等待2MSL?
答:1) 确保最后一个ACK能够到达对方;2) 让旧连接的重复报文在网络中消失,避免影响新连接。
Q3: 滑动窗口和拥塞窗口的区别?
答:滑动窗口是接收方控制的流量控制机制,拥塞窗口是发送方控制的拥塞避免机制。实际发送窗口取两者最小值。
Q4: 慢启动为什么是指数增长?
答:为了快速探测网络可用带宽,在连接初期快速增加发送速率,尽快达到网络容量。
Q5: 什么是快速重传?
答:当收到3个重复ACK时,不等待超时就立即重传丢失的报文,提高重传效率。
-
必知必会:
- 能画图并详细讲解三次握手和四次挥手的每一个步骤、状态变化及原因。
- 能清晰区分流量控制和拥塞控制的目的(谁 vs 谁)。
- 能说出慢启动、拥塞避免、快重传、快恢复的触发条件和窗口变化规则。
-
深度问题:
- SYN Flood攻击的原理是什么?如何防范?(耗尽服务器的半连接队列;可用SYN Cookie防御)
- TIME_WAIT状态过多有什么问题?如何优化?(占用端口资源;可设置
SO_REUSEADDR
套接字选项) - 除了TCP的拥塞控制,还了解哪些其他算法?(如BBR)
总结
TCP的复杂机制确保了可靠的数据传输:
- ✅ 三次握手:可靠建立连接
- ✅ 四次挥手:优雅关闭连接
- ✅ 状态机:管理连接生命周期
- ✅ 滑动窗口:流量控制,避免淹没接收方
- ✅ 拥塞控制:网络保护,避免拥塞崩溃