4. TCP 协议栈回顾:TCP 状态机、三次握手、四次挥手、滑动窗口、拥塞控制
聊端口复用之前,咱们得先把 TCP 协议栈的老底翻出来看看。说实话,很多搞了几年网络的人,对 TCP 的理解还停留在「三次握手、四次挥手」的顺口溜上。但真到排查问题的时候,你会发现——嗯,光背口诀是不够的。
我个人习惯把 TCP 看作一个「有状态的信使」。它不光帮你传数据,还得记住每一段数据的状态。今天咱们就把这个信使的底牌一张张翻开。
4.1 TCP 状态机:信使的内心戏
TCP 状态机,说白了就是一条连接从生到死的所有状态变化。我见过不少同事,抓包看到 SYN_RECV 就慌了,其实你理解了状态机,这些都不是事儿。
下面这张图,是我自己画的一个简化版 TCP 状态机。你重点关注客户端和服务器的状态流转路径。
你想想看,一个连接从 CLOSED 到 ESTABLISHED,中间经历了三次握手。但真正让我头疼的,往往是 TIME_WAIT 和 CLOSE_WAIT 这两个状态。
TIME_WAIT,通常是短连接场景下主动关闭连接的一方。我曾经在一个高并发网关项目里,看到单机 TIME_WAIT 飙到 5 万+,端口直接耗尽。解决方案?要么改长连接,要么调 tcp_tw_reuse。
4.2 三次握手:不只是「你好你好你好」
三次握手的本质是什么?是双方协商初始序列号。很多人以为握手就是为了确认对方在线,其实不然。
我习惯用「对暗号」来理解:
- 客户端发 SYN:我告诉你我的初始序列号是 X,你准备好了吗?
- 服务器回 SYN+ACK:收到你的 X,我的初始序列号是 Y,确认你的 X+1。
- 客户端发 ACK:收到你的 Y,确认 Y+1。咱们开始传数据吧。
为什么是三次,不是两次?你想想看,如果只有两次握手,服务器无法确认客户端是否收到了自己的 SYN+ACK。万一丢了,服务器就傻等了。
tcp_syn_backlog 满了。半连接队列溢出,新连接直接丢弃。所以,高并发场景下记得调大 net.ipv4.tcp_max_syn_backlog。
4.3 四次挥手:优雅地说再见
四次挥手比握手复杂。为什么?因为 TCP 是「全双工」的,每一方向都要单独关闭。
流程是这样的:
- 第一次挥手:客户端发 FIN,告诉服务器「我没数据要发了」。
- 第二次挥手:服务器回 ACK,说「知道了,但我还有数据要发」。
- 第三次挥手:服务器发完数据,发 FIN,说「我也发完了」。
- 第四次挥手:客户端回 ACK,然后进入
TIME_WAIT状态。
这里有个关键点——TIME_WAIT 持续 2MSL(约 1-4 分钟)。为什么?两个原因:
- 保证最后一个 ACK 能到达服务器,万一丢了,服务器会重发 FIN。
- 让网络中残留的报文段都过期,避免影响新连接。
我踩过的坑: 有一次线上服务频繁重启,每次重启后端口都被 TIME_WAIT 占着,新进程起不来。后来加了 SO_REUSEADDR 选项才解决。嗯,这个选项在端口复用里也会用到,后面章节会细讲。
4.4 滑动窗口:流量控制的阀门
滑动窗口,说白了就是接收方告诉发送方:「你一次最多能发多少,别撑着我。」
窗口大小由接收方的缓冲区剩余空间决定。发送方根据这个窗口大小,决定一次发多少数据。
我画个简单的示意图:
你想想看,如果接收方处理不过来,窗口就会缩小。极端情况下窗口变成 0,发送方就得停等。这就是所谓的「零窗口」场景。
tcp_rmem 和 tcp_wmem 这两个内核参数。它们控制着接收和发送缓冲区的大小,直接影响滑动窗口的上限。
4.5 拥塞控制:别把网络堵死了
滑动窗口管的是「点对点」的流量,拥塞控制管的是「整个网络」的负载。说白了,就是怕你发得太快,把中间路由器搞崩了。
TCP 拥塞控制有四个阶段:
| 阶段 | 触发条件 | 行为 |
|---|---|---|
| 慢启动 | 连接刚建立 | cwnd 从 1 开始,每收到一个 ACK 翻倍 |
| 拥塞避免 | cwnd 达到 ssthresh | cwnd 线性增长,每 RTT 加 1 |
| 快速重传 | 收到 3 个重复 ACK | 立即重传丢失的报文,不等超时 |
| 快速恢复 | 快速重传后 | ssthresh 减半,cwnd 设为 ssthresh + 3 |
我记得有一次线上服务出现周期性「卡顿」,抓包一看,发现是拥塞窗口频繁降到 1。原因是网络中存在少量丢包,触发了快速重传和快速恢复。后来调整了 tcp_congestion_control 为 BBR 算法,问题就解决了。
4.6 小结:这些知识跟端口复用有什么关系?
你可能会问,讲这么多 TCP 基础,跟端口复用有啥关系?
关系大了去了。端口复用本质上是在同一个端口上处理多个连接。而 TCP 状态机决定了连接的生命周期,滑动窗口和拥塞控制决定了数据传输的效率。
举个例子:如果你不理解 TIME_WAIT,你就不知道为什么端口复用时要设置 SO_REUSEADDR。如果你不理解滑动窗口,你就无法解释为什么端口复用后吞吐量反而下降了。
所以,别急。这些基础打牢了,后面讲端口复用的实现时,你才能一眼看穿本质。