1. TCP协议基础:从协议栈到状态机
做网络开发这么多年,我始终觉得TCP是绕不开的基石。不管你是处理粘包、分析TLS握手,还是排查连接超时,最终都要回到TCP本身。这一章,我们就从最基础的东西开始捋一遍。
1.1 TCP/IP协议栈回顾
先看这张经典的协议栈分层图。我习惯把它理解成「各司其职」的流水线——每一层只关心自己的事,把剩下的丢给下一层。
你想想看,当你在浏览器里输入一个网址,数据是怎么从应用层一路「穿衣服」到网线上的?每一层都会加上自己的头部,到了接收端再一层层脱掉。这就是所谓的封装与解封装。
我个人习惯把传输层看作「端到端的信使」。它不管你的数据走哪条路,只保证把数据从进程A送到进程B。而网络层负责的是「路由与寻址」——它决定数据包下一跳该往哪走。
1.2 TCP报文段结构
TCP报文段长什么样?我直接给你看最核心的字段。记住,20字节固定头部是必须背下来的。
| 字段 | 长度(位) | 说明 |
|---|---|---|
| 源端口 / 目的端口 | 16 + 16 | 标识通信的进程 |
| 序列号(SEQ) | 32 | 本报文段数据的第一个字节序号 |
| 确认号(ACK) | 32 | 期望收到的下一个字节序号 |
| 数据偏移 | 4 | 头部长度(单位:4字节) |
| 标志位 | 9 | URG/ACK/PSH/RST/SYN/FIN 等 |
| 窗口大小 | 16 | 接收窗口(流量控制用) |
| 校验和 | 16 | 覆盖头部+数据的校验 |
| 紧急指针 | 16 | URG=1时有效 |
我的一点经验:序列号和确认号是TCP最容易被忽视的细节。很多粘包问题追根溯源,都是因为没搞清楚「字节流」的概念——TCP不关心你发的是消息还是文件,它只认字节偏移。
标志位这块,你至少得记住三个:SYN(发起连接)、FIN(关闭连接)、ACK(确认)。RST也很重要,我在排查连接异常断开时经常抓到它。
1.3 三次握手与四次挥手
这个你肯定听过无数遍了。但我还是想用自己的话再讲一遍,因为细节往往藏在最基础的地方。
三次握手:建立连接
- 客户端 → 服务端:SYN(SEQ=x)
客户端说:「我要连你,我的初始序号是x。」 - 服务端 → 客户端:SYN + ACK(SEQ=y, ACK=x+1)
服务端说:「收到,我的初始序号是y,我等你发x+1。」 - 客户端 → 服务端:ACK(SEQ=x+1, ACK=y+1)
客户端说:「好,开始传数据吧。」
为什么非得三次?两次行不行?我遇到过有人问这个问题。说白了,两次握手无法防止历史连接请求的干扰。如果客户端发了一个SYN因为网络延迟很久才到,服务端以为这是新连接,直接分配资源——那就乱套了。第三次ACK就是为了让服务端确认「这个连接请求是有效的」。
四次挥手:关闭连接
- 主动方 → 被动方:FIN(SEQ=u)
主动方说:「我发完了,准备关。」 - 被动方 → 主动方:ACK(ACK=u+1)
被动方说:「知道了,但我还有数据没发完。」 - 被动方 → 主动方:FIN(SEQ=v)
被动方说:「我也发完了,可以关了。」 - 主动方 → 被动方:ACK(ACK=v+1)
主动方说:「收到,开始等待2MSL后关闭。」
我曾经踩过的坑:四次挥手中的TIME_WAIT状态持续2MSL(约1-4分钟)。如果服务端频繁主动关闭连接,端口会被占满,导致新连接无法建立。后来我改用长连接+心跳,才彻底解决这个问题。
1.4 TCP状态转换图
这张图我建议你打印出来贴在工位上。每次排查连接问题,我都是对着它一步步推演的。
这张图里,我最想让你关注的是TIME_WAIT和CLOSE_WAIT。前者是主动关闭方最后停留的状态,后者是被动关闭方收到FIN后等待应用层关闭的状态。
排查技巧:用 netstat -tan | grep TIME_WAIT 看看你的服务器上有多少TIME_WAIT。如果成千上万,说明你的应用频繁创建和关闭短连接。我一般建议用连接池或者调小 tcp_tw_reuse 来缓解。
1.5 小结
这一章我们覆盖了TCP最核心的几个概念:协议栈分层、报文段结构、三次握手、四次挥手、状态转换。这些东西看起来基础,但后面所有的高级话题——粘包、拆包、TLS握手、流量控制——都建立在这之上。
嗯,我个人建议你把状态转换图背下来。下次遇到连接卡住、端口被占、握手失败,你就能快速定位到具体状态,而不是瞎猜。