TCP 连接的本质:为什么需要三次握手?
从数学证明到工程实践,理解 TCP 状态机设计哲学
为什么是三次,不是两次?
很多人知道"三次握手"这个事实,但不知道为什么不能是两次。
两次握手的致命缺陷:客户端可以确认服务端能收发数据,但服务端无法确认客户端能接收自己的数据。更根本的问题是——无法区分"新 SYN"和"网络中延迟的旧 SYN"。
设想这个场景:客户端发出 SYN-A,因网络拥塞延迟;客户端超时重传 SYN-B,建立连接后完成通信,连接关闭;此时延迟的 SYN-A 姗姗来迟,服务端只有两次握手的话,会直接建立一个无效连接,白白占用资源。
第三次握手的作用就是让客户端有机会发送 RST 拒绝历史连接:
客户端 → SYN(seq=100)
服务端 ← SYN-ACK(seq=300, ack=101)
客户端 → ACK(ack=301) ← 这一步让服务端确认"你收到我的确认了"
SYN 洪水攻击与 SYN Cookie
三次握手带来了一个安全窗口:服务端发出 SYN-ACK 后,需要在半连接队列(backlog)中维护状态,等待第三次 ACK。攻击者可以用伪造 IP 的 SYN 包打满这个队列。
Linux 的解法是 SYN Cookie:把连接状态编码进序列号,完全不存储半连接状态。
seq = timestamp | hash(src_ip, src_port, dst_ip, dst_port, timestamp)
收到 ACK 时重新计算验证即可。代价是 SYN Cookie 期间无法协商 TCP 扩展选项(window scaling、SACK),但对合法连接是透明的。
TIME_WAIT 为什么等 2MSL?
四次挥手后,主动关闭方进入 TIME_WAIT,等待 2×MSL(通常 60 秒,总计 2 分钟)。
原因一:确保最后一个 ACK 能到达对端。若对端未收到,会重发 FIN,TIME_WAIT 状态下可以重新应答。
原因二:让网络中所有属于该连接的数据包自然消亡。MSL 是数据包在网络中能存活的最长时间,等待 2MSL 保证旧数据包在新连接建立前全部失效。
高并发服务器的常见问题:大量 TIME_WAIT 占满端口。工程解法:
- 服务端开启
net.ipv4.tcp_tw_reuse = 1 SO_REUSEPORT让多个进程监听同一端口- 反向代理(Nginx)复用长连接,减少连接频繁建立
Nagle 算法与 TCP_NODELAY
Nagle 算法减少小包网络占用:只有缓冲数据达到 MSS,或上一个数据的 ACK 已收到,才发送。
对延迟敏感的场景(游戏、WebSocket、gRPC),这会引入 40ms 级别的额外延迟。设置 TCP_NODELAY 禁用之:
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
WebSocket 和 HTTP/2 的实现通常默认开启 TCP_NODELAY。