TCP协议基础回顾:三次握手、四次挥手、连接状态与端口套接字
说实话,很多搞了几年运维的朋友,对TCP的理解还停留在「知道有三次握手」这个层面。但一旦涉及到端口复用、高并发部署,这些基础就成了绕不过去的坎。我见过太多线上事故,根源就是对TCP状态机理解不透彻。
这一节,咱们把TCP协议的核心骨架捋一遍。不扯太深的理论,重点讲清楚跟端口复用直接相关的几个概念。
三次握手:建立连接的过程
TCP是面向连接的协议。什么叫「面向连接」?说白了,就是通信前双方先打个招呼,确认彼此都在线,再开始传数据。
这个打招呼的过程,就是三次握手:
- 客户端发送SYN:客户端说「我要连你」,同时带上自己的初始序列号(比如1000)。
- 服务端回复SYN+ACK:服务端说「收到,我也准备好了」,带上自己的初始序列号(比如2000),同时确认客户端的序列号(ack=1001)。
- 客户端发送ACK:客户端说「好的,开始传数据吧」,确认服务端的序列号(ack=2001)。
为什么非得三次?两次不行吗?我刚开始学的时候也有这个疑问。你想想看,如果只有两次握手,服务端无法确认客户端是否收到了自己的SYN+ACK。万一丢了,服务端就傻等着,浪费资源。三次握手保证了双方都有能力收发数据。
关键点:三次握手完成后,连接状态变为ESTABLISHED,双方才能开始传输数据。端口复用场景下,我们经常要关注这个状态是否正常建立。
四次挥手:断开连接的过程
断开连接比建立连接复杂一些。因为TCP是全双工的,双方都可以主动发起关闭。四次挥手的过程是这样的:
- 主动方发送FIN:主动方说「我这边数据发完了,准备关闭」。此时主动方不能再发数据,但还能收数据。
- 被动方回复ACK:被动方说「知道了,但我还有数据要发,你先等着」。
- 被动方发送FIN:被动方数据发完了,说「我也准备好了,关吧」。
- 主动方回复ACK:主动方说「收到,关闭」。然后主动方进入TIME_WAIT状态。
这里有个坑,我踩过好几次。主动关闭的一方在发送最后一个ACK后,不会立即释放连接,而是要等2MSL(最大报文段生存时间)才真正关闭。为什么?因为要确保被动方收到了ACK。如果被动方没收到,会重发FIN,主动方还能再回应一次。
注意:TIME_WAIT状态持续60秒左右(Linux默认)。在高并发短连接场景下,大量TIME_WAIT会占用端口资源,导致端口耗尽。这是端口复用要解决的核心问题之一。
TCP连接状态:LISTEN、ESTABLISHED、TIME_WAIT
TCP连接在整个生命周期中会经历多个状态。跟端口复用最相关的三个状态,我重点说一下:
| 状态 | 含义 | 与端口复用的关系 |
|---|---|---|
| LISTEN | 服务端正在监听某个端口,等待客户端连接 | 端口复用时,多个进程可以监听同一个端口(通过SO_REUSEADDR) |
| ESTABLISHED | 连接已建立,双方可以传输数据 | 端口复用时,同一个端口上可以有多个ESTABLISHED连接 |
| TIME_WAIT | 主动关闭方等待2MSL,确保被动方收到最后的ACK | 大量TIME_WAIT会占用端口,影响新连接建立 |
我个人习惯用 ss -tlnp 或者 netstat -tlnp 查看这些状态。比如:
# 查看所有监听状态的端口
ss -tlnp
# 查看ESTABLISHED状态的连接
ss -tan | grep ESTAB
# 统计TIME_WAIT数量
ss -tan | grep TIME_WAIT | wc -l
我在项目中遇到过一个问题:某台服务器上TIME_WAIT数量飙到了3万多,导致新连接建立失败。排查后发现是短连接太多,端口来不及释放。后来通过端口复用和调整内核参数解决了。
端口与套接字的关系
很多人把「端口」和「套接字」混为一谈,其实它们不是一回事。
- 端口:是一个16位的数字(0-65535),用来标识主机上的某个服务。比如80端口通常给HTTP用,443给HTTPS用。
- 套接字(Socket):是IP地址和端口号的组合。一个完整的套接字由四元组唯一标识:源IP、源端口、目标IP、目标端口。
举个例子:
客户端A(192.168.1.10:54321) → 服务端(10.0.0.1:80)
客户端B(192.168.1.11:54322) → 服务端(10.0.0.1:80)
这两个连接的目标端口都是80,但它们是不同的套接字,因为源IP和源端口不同。所以,同一个端口可以同时承载成千上万个连接,只要四元组不重复就行。
经验之谈:端口复用就是利用这个原理。多个进程监听同一个端口时,内核会根据四元组把连接分发给不同的进程。我曾经用这个特性做过灰度发布——新老版本同时监听80端口,流量逐步切换。
核心逻辑图:TCP连接生命周期
下面这张图把三次握手、四次挥手和关键状态串起来了。建议你仔细看看,尤其是TIME_WAIT的位置。
这张图里,我特意把TIME_WAIT标成了紫色。为什么?因为它是端口复用中最需要关注的状态。你想想看,如果服务端主动关闭连接,TIME_WAIT就会出现在服务端,那服务端的端口就被占用了。但如果是客户端主动关闭,TIME_WAIT在客户端,服务端端口就能快速释放。
嗯,这里要注意:在HTTP短连接场景下,通常是客户端主动关闭。所以TIME_WAIT一般出现在客户端,服务端相对安全。但如果你用了一些特殊的反向代理或负载均衡器,情况可能反过来。
避坑指南:我曾经在Nginx反向代理场景下,发现后端服务器上TIME_WAIT堆积严重。排查后发现是Nginx配置了keepalive_timeout为0,导致Nginx主动关闭连接。改成客户端主动关闭后,问题解决。