1. TCP协议基础:从协议栈到状态机

做网络开发这么多年,我始终觉得TCP是绕不开的基石。不管你是处理粘包、分析TLS握手,还是排查连接超时,最终都要回到TCP本身。这一章,我们就从最基础的东西开始捋一遍。

1.1 TCP/IP协议栈回顾

先看这张经典的协议栈分层图。我习惯把它理解成「各司其职」的流水线——每一层只关心自己的事,把剩下的丢给下一层。

TCP/IP 协议栈分层模型 应用层(HTTP / FTP / SMTP / TLS) 传输层(TCP / UDP) 网络层(IP / ICMP / ARP) 网络接口层(以太网 / WiFi / PPP) 数据逐层封装:应用数据 → TCP段 → IP包 → 以太网帧

你想想看,当你在浏览器里输入一个网址,数据是怎么从应用层一路「穿衣服」到网线上的?每一层都会加上自己的头部,到了接收端再一层层脱掉。这就是所谓的封装与解封装。

我个人习惯把传输层看作「端到端的信使」。它不管你的数据走哪条路,只保证把数据从进程A送到进程B。而网络层负责的是「路由与寻址」——它决定数据包下一跳该往哪走。

1.2 TCP报文段结构

TCP报文段长什么样?我直接给你看最核心的字段。记住,20字节固定头部是必须背下来的。

字段 长度(位) 说明
源端口 / 目的端口16 + 16标识通信的进程
序列号(SEQ)32本报文段数据的第一个字节序号
确认号(ACK)32期望收到的下一个字节序号
数据偏移4头部长度(单位:4字节)
标志位9URG/ACK/PSH/RST/SYN/FIN 等
窗口大小16接收窗口(流量控制用)
校验和16覆盖头部+数据的校验
紧急指针16URG=1时有效

我的一点经验:序列号和确认号是TCP最容易被忽视的细节。很多粘包问题追根溯源,都是因为没搞清楚「字节流」的概念——TCP不关心你发的是消息还是文件,它只认字节偏移。

标志位这块,你至少得记住三个:SYN(发起连接)、FIN(关闭连接)、ACK(确认)。RST也很重要,我在排查连接异常断开时经常抓到它。

1.3 三次握手与四次挥手

这个你肯定听过无数遍了。但我还是想用自己的话再讲一遍,因为细节往往藏在最基础的地方。

三次握手:建立连接

  1. 客户端 → 服务端:SYN(SEQ=x)
    客户端说:「我要连你,我的初始序号是x。」
  2. 服务端 → 客户端:SYN + ACK(SEQ=y, ACK=x+1)
    服务端说:「收到,我的初始序号是y,我等你发x+1。」
  3. 客户端 → 服务端:ACK(SEQ=x+1, ACK=y+1)
    客户端说:「好,开始传数据吧。」

为什么非得三次?两次行不行?我遇到过有人问这个问题。说白了,两次握手无法防止历史连接请求的干扰。如果客户端发了一个SYN因为网络延迟很久才到,服务端以为这是新连接,直接分配资源——那就乱套了。第三次ACK就是为了让服务端确认「这个连接请求是有效的」。

四次挥手:关闭连接

  1. 主动方 → 被动方:FIN(SEQ=u)
    主动方说:「我发完了,准备关。」
  2. 被动方 → 主动方:ACK(ACK=u+1)
    被动方说:「知道了,但我还有数据没发完。」
  3. 被动方 → 主动方:FIN(SEQ=v)
    被动方说:「我也发完了,可以关了。」
  4. 主动方 → 被动方:ACK(ACK=v+1)
    主动方说:「收到,开始等待2MSL后关闭。」

我曾经踩过的坑:四次挥手中的TIME_WAIT状态持续2MSL(约1-4分钟)。如果服务端频繁主动关闭连接,端口会被占满,导致新连接无法建立。后来我改用长连接+心跳,才彻底解决这个问题。

1.4 TCP状态转换图

这张图我建议你打印出来贴在工位上。每次排查连接问题,我都是对着它一步步推演的。

TCP 状态转换图(简化版) CLOSED LISTEN SYN_SENT SYN_RCVD ESTABLISHED FIN_WAIT_1 CLOSE_WAIT FIN_WAIT_2 TIME_WAIT 被动打开 收到SYN 主动打开 收到SYN+ACK 收到ACK 主动关闭 收到FIN 收到ACK 收到FIN (被动关闭后发FIN)

这张图里,我最想让你关注的是TIME_WAIT和CLOSE_WAIT。前者是主动关闭方最后停留的状态,后者是被动关闭方收到FIN后等待应用层关闭的状态。

排查技巧:用 netstat -tan | grep TIME_WAIT 看看你的服务器上有多少TIME_WAIT。如果成千上万,说明你的应用频繁创建和关闭短连接。我一般建议用连接池或者调小 tcp_tw_reuse 来缓解。

1.5 小结

这一章我们覆盖了TCP最核心的几个概念:协议栈分层、报文段结构、三次握手、四次挥手、状态转换。这些东西看起来基础,但后面所有的高级话题——粘包、拆包、TLS握手、流量控制——都建立在这之上。

嗯,我个人建议你把状态转换图背下来。下次遇到连接卡住、端口被占、握手失败,你就能快速定位到具体状态,而不是瞎猜。