1. TCP协议基础:从协议栈到状态机
TCP 这东西,说白了就是网络通信的「定海神针」。我做了十几年网络编程,见过无数问题最后都追溯到对 TCP 理解不够深。今天咱们就从最底层开始,把 TCP 的骨架搭起来。
1.1 TCP/IP 协议栈:数据包的奇幻漂流
你想想看,你在浏览器里敲个网址,数据是怎么跑到服务器上的?这背后就是 TCP/IP 协议栈在干活。它分四层,每层各司其职:
| 层级 | 核心协议 | 一句话说明 |
|---|---|---|
| 应用层 | HTTP、FTP、SSH | 你直接打交道的协议,定义数据格式 |
| 传输层 | TCP、UDP | 负责端到端通信,TCP 保证可靠 |
| 网络层 | IP | 寻址和路由,把数据包送到目标机器 |
| 网络接口层 | 以太网、Wi-Fi | 物理传输,比特流在网线上跑 |
数据从应用层一路往下,每经过一层就加个头部——这叫「封装」。接收端再一层层拆开。我记得刚入行时调一个 HTTP 接口死活连不上,抓包一看,IP 层地址写对了,但 TCP 端口写反了。嗯,这种低级错误,犯过一次就记住了。
核心要点:TCP 工作在传输层,它不关心你的数据是 HTTP 还是 FTP,它只保证「我把你给我的字节流,原封不动地送到对面」。
1.2 TCP 报文格式:头部里的秘密
TCP 报文头部最少 20 字节,最多 60 字节。别看它小,里面每个字段都有讲究。我挑几个最重要的说:
- 源端口 / 目的端口(各 2 字节): 标识通信的进程。IP 地址找机器,端口找进程。
- 序列号(4 字节): 这是 TCP 可靠传输的基石。每个字节都有编号,接收方靠它排序、去重。
- 确认号(4 字节): 告诉对方「我收到哪了」,值是期望收到的下一个序列号。
- 标志位(9 位): 常用的有 SYN、ACK、FIN、RST。握手挥手全靠它们。
- 窗口大小(2 字节): 流量控制用的,告诉对方「你慢点发,我快撑不住了」。
我的习惯: 调试网络问题时,我第一件事就是抓包看 TCP 头部。序列号是否连续?窗口是不是被填满了?这些信息比看业务日志管用多了。
1.3 三次握手:建立连接的艺术
为什么是三次?不是两次,也不是四次?我当年面试时也被问过这个问题。咱们走一遍流程就明白了:
- 客户端 → 服务端:SYN — 「嘿,我想跟你说话,这是我的初始序列号 x」
- 服务端 → 客户端:SYN + ACK — 「收到,这是我的初始序列号 y,我确认收到了你的 x」
- 客户端 → 服务端:ACK — 「好,我确认收到了你的 y,咱们开聊」
两次不够,因为服务端没法确认客户端收到了自己的 SYN。四次浪费,第三次握手时已经可以捎带数据了。三次刚刚好。
避坑指南: 我曾经遇到过一个线上问题,客户端频繁断连重连,导致服务端有大量半连接(SYN_RCVD 状态)。后来发现是客户端没做连接复用,每次请求都新建连接。解决方案很简单:用连接池。但排查过程花了一整天。
1.4 四次挥手:好聚好散
断开连接比建立连接复杂,因为 TCP 是「全双工」的——两边可以同时收发。所以每一端都要单独关闭自己的写通道:
- 主动方 → 被动方:FIN — 「我这边没数据要发了」
- 被动方 → 主动方:ACK — 「知道了,但我还有数据要发」
- 被动方 → 主动方:FIN — 「我也发完了,拜拜」
- 主动方 → 被动方:ACK — 「收到,我关闭了」
注意第二步和第三步之间,被动方可能还在传输数据。这就是为什么挥手要四次,不能合并。
关键点: 主动关闭方在发送最后一个 ACK 后会进入 TIME_WAIT 状态,持续 2MSL(约 2 分钟)。很多人觉得这个状态烦人,但它其实在保护你——防止最后一个 ACK 丢失导致被动方重发 FIN。
1.5 TCP 状态转换图:一张图看懂所有状态
下面这张图是我自己整理的 TCP 状态机。说实话,刚学的时候觉得这图好复杂,后来在项目中排查过几次连接异常后,发现它其实很直观。
我的经验: 排查网络问题时,我习惯先看 netstat -an | grep 端口号。如果看到大量 TIME_WAIT,说明客户端频繁创建短连接;如果看到大量 CLOSE_WAIT,说明服务端代码忘了调用 close()。这两种情况我都遇到过,解决方案完全不同。
1.6 常见状态解读:面试和实战都用的上
| 状态 | 含义 | 常见场景 |
|---|---|---|
| LISTEN | 服务端在监听端口 | 服务启动正常 |
| SYN_SENT | 客户端发了 SYN,等回复 | 连接超时可能卡在这 |
| ESTABLISHED | 连接已建立 | 正常通信中 |
| CLOSE_WAIT | 被动方等应用程序关闭 | 代码没调 close() |
| TIME_WAIT | 主动方等待 2MSL | 短连接过多时出现 |
避坑指南: 我曾经在生产环境遇到过一个问题:服务端 CLOSE_WAIT 状态堆积到几千个,导致文件描述符耗尽,新连接全部被拒绝。查了半天发现是某个第三方库在读取完数据后没有关闭 socket。从那以后,我写网络代码都会在 finally 块里确保 close() 被执行。
好了,TCP 协议基础就讲到这里。这些概念是后面所有内容的地基——Socket 编程、粘包拆包、心跳机制,全都建立在这些状态和流程之上。你先把这张状态转换图记熟,后面实战时会发现,很多问题看一眼状态就猜到了七八分。