tcp - basics
06. TCP 基础
0. 本章先解决什么问题
IP 层尽力把包送到目标主机,但它不保证包一定到、不保证顺序、不保证只到一次,也不关心目标进程处理得过来没有。
如果应用直接建立在 IP 之上,就要自己处理:
包丢了怎么办?
包乱序了怎么办?
重复包怎么办?
一大段数据怎样切分?
对方突然断开怎么办?
网络中间设备丢包怎么办?
TCP 在 IP 之上提供一个更适合通用应用的抽象:
面向连接、可靠、有序、全双工的字节流。
本章先讲 TCP 的基础机制:连接、字节流、序号、ACK、重传、三次握手、四次挥手、常见状态。下一章再专门展开流量控制、拥塞控制和性能。
这张图怎么读
TCP 排障时,状态比一句错误信息更有用。ESTABLISHED 说明连接已建立,TIME_WAIT 常见于主动关闭后的保护等待,CLOSE_WAIT 大量堆积更常指向本端应用没有正确关闭 socket。
1. TCP 给应用的抽象是什么
应用使用 TCP 时,看到的通常不是一个个 IP 包,而是一条可以读写的连接。
应用 A write bytes -> TCP -> IP -> 网络 -> IP -> TCP -> 应用 B read bytes
TCP 试图让应用相信:
| TCP 提供 | 含义 |
|---|---|
| 可靠 | 发出的字节只要连接正常,就会被确认或重传 |
| 有序 | 接收方按发送顺序交付字节 |
| 字节流 | 应用看到连续字节,不是固定消息块 |
| 面向连接 | 通信前先建立双方状态 |
| 全双工 | 两端可以同时发送和接收 |
但 TCP 并不保证:
| 不保证 | 解释 |
|---|---|
| 消息边界 | 多次 write 不等于多次 read |
| 永远不断 | 网络、进程、中间设备都可能让连接失败 |
| 低延迟 | 可靠性、排队、拥塞控制可能增加等待 |
| 应用语义正确 | TCP 不知道业务数据代表什么 |
所以 TCP 是通用传输能力,不是应用协议。
2. TCP 连接由什么标识
一个 TCP 连接通常由四元组标识:
源 IP
源端口
目标 IP
目标端口
例如:
198.51.100.10:51520 -> 203.0.113.8:443
同一台客户端可以和同一个目标端口建立多条连接,因为源端口不同:
代码块收起展开
198.51.100.10:51520 -> 203.0.113.8:443
198.51.100.10:51521 -> 203.0.113.8:443
198.51.100.10:51522 -> 203.0.113.8:443这也是为什么短连接特别多时,源端口、连接状态、TIME_WAIT 都可能成为资源问题。
3. TCP 是字节流,不是消息队列
TCP 不保留应用 write 的边界。
发送方:
write(“hello”)
write(“world”)
接收方可能读到:
“helloworld”
或 “hel” + “loworld”
或 “hello” + “world”
原因是 TCP 只负责按顺序传输字节,不理解应用层“消息”的概念。
应用协议必须自己定义边界:
| 边界方案 | 思路 | 例子 |
|---|---|---|
| 固定长度 | 每条消息长度固定 | 简单二进制协议 |
| 分隔符 | 遇到特殊字符表示结束 | 行协议、文本命令 |
| 长度字段 | 头部声明 body 长度 | 很多二进制协议 |
| 协议字段 | 由应用协议定义 | HTTP 的 Content-Length / chunked |
如果应用忘记处理粘包/拆包,本质不是 TCP 出错,而是应用层没有定义清楚消息边界。
反例:一次 read 读到半条消息不是 TCP 异常
很多网络 bug 来自一个错误假设:
发送方 write 一条消息
接收方 read 一次就能拿到完整消息
TCP 从来没有承诺这个。它只承诺按顺序交付字节。假设应用协议定义:
[4 byte length][body bytes]
发送方发送一条长度为 1000 byte 的消息,接收方可能这样收到:
第 1 次 read: 2 byte
第 2 次 read: 100 byte
第 3 次 read: 902 byte
也可能这样:
第 1 次 read: 第一条完整消息 + 第二条消息前 20 byte
正确的接收逻辑应该维护一个应用层缓冲区:
- 不断把 read 到的字节追加到 buffer
- buffer 不足 4 byte 时,继续读
- 读出 length
- buffer 不足 length 时,继续读
- buffer 足够时,取出一条完整消息
- 剩余字节留给下一条消息
这也是“粘包/拆包”这个说法容易误导的地方:TCP 没有把消息粘错,也没有把消息拆坏。消息边界本来就是应用协议自己的责任。
4. 序号:可靠传输的地基
TCP 给字节流中的每个字节编号。序号不是“第几个包”,而是“字节流中的位置”。
假设初始序号是 1000,发送 5 个字节:
代码块收起展开
字节: A B C D E
序号: 1000 1001 1002 1003 1004接收方回复 ACK 1005,意思是:
1005 之前的字节我都按顺序收到了,下一步希望收到 1005。
这叫累计确认。
发送: 1000-1004
收到 ACK 1005
含义: 1000 到 1004 都已连续到达
如果中间缺一段,接收方不能把后面的字节交给应用,只能等待缺口补齐。
5. ACK 与重传
TCP 通过 ACK 确认数据,通过重传修复丢包。
基本逻辑:
发送方发送数据
-> 保存未确认数据
-> 等待 ACK
-> 收到 ACK 后释放对应数据
-> 超时或检测到丢包时重传
简化图:
如果 ACK 没回来,发送方不能立刻知道原因:
数据包可能丢了
ACK 可能丢了
网络只是很慢
对方可能还没处理
所以 TCP 需要根据 RTT 估算超时时间。超时太短会误判正常延迟,超时太长会让恢复慢。
6. 三次握手:建立双方状态
TCP 通信前要先建立连接。三次握手不是仪式,而是为了确认双方收发能力和初始序号。
每一步的意义:
| 步骤 | 含义 |
|---|---|
| SYN | 客户端提出建立连接,并声明自己的初始序号 |
| SYN+ACK | 服务端确认客户端序号,同时声明自己的初始序号 |
| ACK | 客户端确认服务端序号,双方进入可传输状态 |
三次握手后,双方都知道:
我能发出去
我能收到对方的回复
对方能发出来
对方能收到我的回复
双方初始序号都已同步
6.1 为什么不是两次
如果只有两次,服务端发出 SYN+ACK 后无法确认客户端是否真的收到了自己的初始序号。旧的延迟 SYN 也可能让服务端误建连接。
第三次 ACK 用来完成双向确认,让服务端知道客户端确实收到了服务端的 SYN。
手推:三次握手到底同步了哪两件事
三次握手至少同步两类状态:
- 双方的初始序号
- 双方的收发能力
逐步看:
| 步骤 | 客户端确认了什么 | 服务端确认了什么 |
|---|---|---|
| C -> S: SYN x | 客户端能发出 SYN | 服务端知道客户端想建立连接,知道 x |
| S -> C: SYN y + ACK x+1 | 客户端知道服务端收到了 x,也知道 y | 服务端能发出 SYN+ACK |
| C -> S: ACK y+1 | 客户端确认自己收到 y | 服务端知道客户端收到了 y |
两次握手的问题是服务端少一个证据:
客户端是否真的收到了服务端的初始序号 y?
没有这个证据,服务端可能以为连接已建立,但客户端并没有进入相同状态。TCP 是两端状态机协作,连接建立不是“发一个开始信号”,而是让双方对后续字节流的位置达成一致。
7. 数据传输阶段:连接不是一根真实的线
TCP 连接是两端内核维护的一组状态,不是一根物理线。
它包含:
| 状态 | 作用 |
|---|---|
| 发送缓冲区 | 保存已写入但未完全确认的数据 |
| 接收缓冲区 | 保存已到达但应用尚未读取的数据 |
| 序号和 ACK | 记录字节流位置 |
| 窗口 | 控制可发送的数据量 |
| 定时器 | 处理重传、保活、等待状态 |
| 状态机 | 管理 LISTEN、ESTABLISHED、FIN 等状态 |
应用调用 write 后,数据通常先进入发送缓冲区。write 返回不等于对方应用已经 read 到,只能说明本端把数据交给了系统缓冲或发送路径。
应用 write 成功
不等于对端已收到
不等于对端已处理
不等于业务成功
这点对理解超时、重试、幂等性很重要。
8. 四次挥手:两个方向分别关闭
TCP 是全双工。A 不想再发送,不代表 B 也不想再发送。
常见关闭过程:
含义:
| 步骤 | 含义 |
|---|---|
| A 发 FIN | A 的发送方向关闭:我不再发数据 |
| B 回 ACK | B 确认 A 的关闭请求 |
| B 发 FIN | B 的发送方向也关闭 |
| A 回 ACK | A 确认 B 的关闭请求 |
如果 B 收到 A 的 FIN 后还有数据要发,第二个 FIN 就会晚一点出现,所以看起来通常是四次。
9. TIME_WAIT 与 CLOSE_WAIT
9.1 TIME_WAIT
主动关闭方发送最后一个 ACK 后,进入 TIME_WAIT。
它的作用:
- 如果最后 ACK 丢了,对方重发 FIN 时,本端还能再回 ACK。
- 等待旧连接中的延迟包自然消失,避免污染同四元组的新连接。
看到 TIME_WAIT 不要立刻认定是 bug。它是 TCP 为了安全关闭连接付出的等待成本。
真正要看:
数量是否持续暴涨?
是否导致本机端口耗尽?
是否有大量短连接?
是否应该复用连接?
主动关闭方是否符合预期?
9.2 CLOSE_WAIT
CLOSE_WAIT 表示对方已经发 FIN,本端也 ACK 了,但本端应用还没有关闭 socket。
如果 CLOSE_WAIT 大量堆积,常见原因是:
应用代码没有 close
异常路径忘记释放连接
读到 EOF 后没有结束资源
连接对象被泄漏
相比 TIME_WAIT,CLOSE_WAIT 更常指向本端应用资源释放问题。
10. TCP 常见状态
| 状态 | 常见位置 | 含义 |
|---|---|---|
| LISTEN | 服务提供方 | 正在监听端口 |
| SYN_SENT | 主动连接方 | SYN 已发出,等待 SYN+ACK |
| SYN_RECV | 被连接方 | 收到 SYN,已回复 SYN+ACK |
| ESTABLISHED | 双方 | 连接已建立,可传输数据 |
| FIN_WAIT_1 | 主动关闭方 | FIN 已发出,等待确认 |
| FIN_WAIT_2 | 主动关闭方 | 对方已 ACK,本端等对方 FIN |
| CLOSE_WAIT | 被动关闭方 | 收到对方 FIN,但本端应用未关闭 |
| LAST_ACK | 被动关闭方 | 本端 FIN 已发,等待最后 ACK |
| TIME_WAIT | 主动关闭方 | 等待旧包消失和处理重发 FIN |
状态是排障线索。它告诉你连接卡在生命周期的哪一步。
11. 常见错误现象怎么映射
| 现象 | 更可能发生在 | 解释 |
|---|---|---|
| Connection refused | 建连阶段 | 目标端口无监听或被明确拒绝 |
| connect timeout | 建连阶段 | SYN 没有完成握手,可能被丢弃或路径不通 |
| read timeout | 数据阶段 | 连接已建立,但等待响应数据超时 |
| connection reset | 任意阶段 | 对端或中间设备发送 RST 强制断开 |
| broken pipe | 写入阶段 | 本端继续写入一个已经失效的连接 |
| 大量 SYN_RECV | 握手阶段 | 半连接堆积,可能是攻击、丢包或处理慢 |
| 大量 CLOSE_WAIT | 关闭阶段 | 本端应用没有及时关闭 socket |
| 大量 TIME_WAIT | 关闭阶段 | 主动关闭频繁,短连接多 |
排障时要先判断:失败发生在连接建立前、连接建立中、数据传输中,还是关闭阶段。
12. 联系实际:一个请求失败时,TCP 帮你切分问题
假设某个程序访问远程服务失败。不要直接说“网络坏了”,先按 TCP 生命周期拆:
- DNS 是否解析到了目标 IP?
- SYN 是否发出?
- 是否收到 SYN+ACK?
- 连接是否进入 ESTABLISHED?
- 请求字节是否写入?
- 是否收到响应字节?
- 连接是正常 FIN 关闭,还是 RST/超时?
不同答案对应不同方向:
| 卡点 | 方向 |
|---|---|
| SYN 发不出 | 本机路由、防火墙、目标地址 |
| SYN 发出无响应 | 中间网络、目标安全策略、目标不可达 |
| 收到拒绝 | 目标端口无监听或拒绝连接 |
| 建立后读不到 | 应用处理慢、响应丢失、读取超时 |
| 写入时报错 | 对端已关闭、中间设备断开、连接状态失效 |
| 关闭阶段堆积 | 资源释放、连接复用、短连接策略 |
TCP 的价值之一,是让你能把“访问失败”拆成更精确的生命周期证据。
排障卡:用 TCP 状态定位连接失败
TCP 状态不是背诵题,它是一张连接排障地图。
| 现象 | 可能状态/阶段 | 先看什么 |
|---|---|---|
| 连接超时 | SYN 已发但没有有效回应 | 目标地址、路由、防火墙、SYN 重传 |
| 连接被拒绝 | 对端返回拒绝或无人监听 | 端口监听、服务启动、访问地址 |
| 能连但读不到完整消息 | 字节流边界没有处理好 | 应用协议的长度、分隔符、读取循环 |
| 大量 TIME_WAIT | 主动关闭连接的一方较多 | 连接复用、短连接比例、关闭策略 |
| 大量 CLOSE_WAIT | 本端收到关闭但应用未关闭 | 资源释放、读写循环、异常路径 |
练习方式:画出一次连接从 SYN 到 ESTABLISHED 再到关闭的状态迁移,把每个状态旁边标注“谁在等谁”。这样你看到状态名时,脑子里出现的是等待关系,而不是孤立术语。
13. 学完本章你能解决什么问题
学完本章,你应该能:
- 解释 TCP 为什么要在 IP 之上提供可靠有序字节流。
- 正确区分字节流和消息边界,知道应用协议为什么需要长度字段或分隔符。
- 用序号和 ACK 理解可靠传输、重传和累计确认。
- 解释三次握手和四次挥手各自解决的问题。
- 判断 TIME_WAIT 与 CLOSE_WAIT 分别意味着什么。
- 把 Connection refused、connect timeout、read timeout、reset、broken pipe 等现象映射到 TCP 生命周期。
- 在排查网络请求失败时,按“解析 -> 握手 -> 传输 -> 关闭”的顺序定位。