TCP协议基础回顾:三次握手、四次挥手、连接状态与端口套接字

说实话,很多搞了几年运维的朋友,对TCP的理解还停留在「知道有三次握手」这个层面。但一旦涉及到端口复用、高并发部署,这些基础就成了绕不过去的坎。我见过太多线上事故,根源就是对TCP状态机理解不透彻。

这一节,咱们把TCP协议的核心骨架捋一遍。不扯太深的理论,重点讲清楚跟端口复用直接相关的几个概念。

三次握手:建立连接的过程

TCP是面向连接的协议。什么叫「面向连接」?说白了,就是通信前双方先打个招呼,确认彼此都在线,再开始传数据。

这个打招呼的过程,就是三次握手:

  1. 客户端发送SYN:客户端说「我要连你」,同时带上自己的初始序列号(比如1000)。
  2. 服务端回复SYN+ACK:服务端说「收到,我也准备好了」,带上自己的初始序列号(比如2000),同时确认客户端的序列号(ack=1001)。
  3. 客户端发送ACK:客户端说「好的,开始传数据吧」,确认服务端的序列号(ack=2001)。

为什么非得三次?两次不行吗?我刚开始学的时候也有这个疑问。你想想看,如果只有两次握手,服务端无法确认客户端是否收到了自己的SYN+ACK。万一丢了,服务端就傻等着,浪费资源。三次握手保证了双方都有能力收发数据。

关键点:三次握手完成后,连接状态变为ESTABLISHED,双方才能开始传输数据。端口复用场景下,我们经常要关注这个状态是否正常建立。

四次挥手:断开连接的过程

断开连接比建立连接复杂一些。因为TCP是全双工的,双方都可以主动发起关闭。四次挥手的过程是这样的:

  1. 主动方发送FIN:主动方说「我这边数据发完了,准备关闭」。此时主动方不能再发数据,但还能收数据。
  2. 被动方回复ACK:被动方说「知道了,但我还有数据要发,你先等着」。
  3. 被动方发送FIN:被动方数据发完了,说「我也准备好了,关吧」。
  4. 主动方回复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的位置。

TCP连接生命周期:三次握手与四次挥手 TCP连接生命周期 客户端 服务端 三次握手 CLOSED LISTEN SYN (seq=1000) SYN_SENT SYN+ACK (seq=2000, ack=1001) SYN_RCVD ACK (ack=2001) ESTABLISHED ESTABLISHED 数据传送 四次挥手 FIN (seq=3000) FIN_WAIT_1 ACK (ack=3001) CLOSE_WAIT FIN_WAIT_2 FIN (seq=4000) LAST_ACK ACK (ack=4001) TIME_WAIT CLOSED CLOSED 注意:TIME_WAIT状态持续2MSL(约60秒),是端口复用的关键优化点

这张图里,我特意把TIME_WAIT标成了紫色。为什么?因为它是端口复用中最需要关注的状态。你想想看,如果服务端主动关闭连接,TIME_WAIT就会出现在服务端,那服务端的端口就被占用了。但如果是客户端主动关闭,TIME_WAIT在客户端,服务端端口就能快速释放。

嗯,这里要注意:在HTTP短连接场景下,通常是客户端主动关闭。所以TIME_WAIT一般出现在客户端,服务端相对安全。但如果你用了一些特殊的反向代理或负载均衡器,情况可能反过来。

避坑指南:我曾经在Nginx反向代理场景下,发现后端服务器上TIME_WAIT堆积严重。排查后发现是Nginx配置了keepalive_timeout为0,导致Nginx主动关闭连接。改成客户端主动关闭后,问题解决。