tcp - basics

06. TCP 基础

0. 本章先解决什么问题

IP 层尽力把包送到目标主机,但它不保证包一定到、不保证顺序、不保证只到一次,也不关心目标进程处理得过来没有。

如果应用直接建立在 IP 之上,就要自己处理:

包丢了怎么办?
包乱序了怎么办?
重复包怎么办?
一大段数据怎样切分?
对方突然断开怎么办?
网络中间设备丢包怎么办?

TCP 在 IP 之上提供一个更适合通用应用的抽象:

面向连接、可靠、有序、全双工的字节流。

本章先讲 TCP 的基础机制:连接、字节流、序号、ACK、重传、三次握手、四次挥手、常见状态。下一章再专门展开流量控制、拥塞控制和性能。

TCP 生命周期状态机

这张图怎么读

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

同一台客户端可以和同一个目标端口建立多条连接,因为源端口不同:

代码块PLAINTEXT · 3 行收起展开
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

正确的接收逻辑应该维护一个应用层缓冲区:

  1. 不断把 read 到的字节追加到 buffer
  2. buffer 不足 4 byte 时,继续读
  3. 读出 length
  4. buffer 不足 length 时,继续读
  5. buffer 足够时,取出一条完整消息
  6. 剩余字节留给下一条消息

这也是“粘包/拆包”这个说法容易误导的地方:TCP 没有把消息粘错,也没有把消息拆坏。消息边界本来就是应用协议自己的责任。

4. 序号:可靠传输的地基

TCP 给字节流中的每个字节编号。序号不是“第几个包”,而是“字节流中的位置”。

假设初始序号是 1000,发送 5 个字节:

代码块PLAINTEXT · 2 行收起展开
字节: 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 没回来,发送方不能立刻知道原因:

数据包可能丢了
ACK 可能丢了
网络只是很慢
对方可能还没处理

所以 TCP 需要根据 RTT 估算超时时间。超时太短会误判正常延迟,超时太长会让恢复慢。

6. 三次握手:建立双方状态

TCP 通信前要先建立连接。三次握手不是仪式,而是为了确认双方收发能力和初始序号。

TCP 三次握手时序

每一步的意义:

步骤含义
SYN客户端提出建立连接,并声明自己的初始序号
SYN+ACK服务端确认客户端序号,同时声明自己的初始序号
ACK客户端确认服务端序号,双方进入可传输状态

三次握手后,双方都知道:

我能发出去
我能收到对方的回复
对方能发出来
对方能收到我的回复
双方初始序号都已同步

6.1 为什么不是两次

如果只有两次,服务端发出 SYN+ACK 后无法确认客户端是否真的收到了自己的初始序号。旧的延迟 SYN 也可能让服务端误建连接。

第三次 ACK 用来完成双向确认,让服务端知道客户端确实收到了服务端的 SYN。

手推:三次握手到底同步了哪两件事

三次握手至少同步两类状态:

  1. 双方的初始序号
  2. 双方的收发能力

逐步看:

步骤客户端确认了什么服务端确认了什么
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 也不想再发送。

常见关闭过程:

TCP 四次挥手时序

含义:

步骤含义
A 发 FINA 的发送方向关闭:我不再发数据
B 回 ACKB 确认 A 的关闭请求
B 发 FINB 的发送方向也关闭
A 回 ACKA 确认 B 的关闭请求

如果 B 收到 A 的 FIN 后还有数据要发,第二个 FIN 就会晚一点出现,所以看起来通常是四次。

9. TIME_WAIT 与 CLOSE_WAIT

9.1 TIME_WAIT

主动关闭方发送最后一个 ACK 后,进入 TIME_WAIT。

它的作用:

  1. 如果最后 ACK 丢了,对方重发 FIN 时,本端还能再回 ACK。
  2. 等待旧连接中的延迟包自然消失,避免污染同四元组的新连接。

看到 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 生命周期拆:

  1. DNS 是否解析到了目标 IP?
  2. SYN 是否发出?
  3. 是否收到 SYN+ACK?
  4. 连接是否进入 ESTABLISHED?
  5. 请求字节是否写入?
  6. 是否收到响应字节?
  7. 连接是正常 FIN 关闭,还是 RST/超时?

不同答案对应不同方向:

卡点方向
SYN 发不出本机路由、防火墙、目标地址
SYN 发出无响应中间网络、目标安全策略、目标不可达
收到拒绝目标端口无监听或拒绝连接
建立后读不到应用处理慢、响应丢失、读取超时
写入时报错对端已关闭、中间设备断开、连接状态失效
关闭阶段堆积资源释放、连接复用、短连接策略

TCP 的价值之一,是让你能把“访问失败”拆成更精确的生命周期证据。

排障卡:用 TCP 状态定位连接失败

TCP 状态不是背诵题,它是一张连接排障地图。

现象可能状态/阶段先看什么
连接超时SYN 已发但没有有效回应目标地址、路由、防火墙、SYN 重传
连接被拒绝对端返回拒绝或无人监听端口监听、服务启动、访问地址
能连但读不到完整消息字节流边界没有处理好应用协议的长度、分隔符、读取循环
大量 TIME_WAIT主动关闭连接的一方较多连接复用、短连接比例、关闭策略
大量 CLOSE_WAIT本端收到关闭但应用未关闭资源释放、读写循环、异常路径

练习方式:画出一次连接从 SYN 到 ESTABLISHED 再到关闭的状态迁移,把每个状态旁边标注“谁在等谁”。这样你看到状态名时,脑子里出现的是等待关系,而不是孤立术语。

13. 学完本章你能解决什么问题

学完本章,你应该能:

  1. 解释 TCP 为什么要在 IP 之上提供可靠有序字节流。
  2. 正确区分字节流和消息边界,知道应用协议为什么需要长度字段或分隔符。
  3. 用序号和 ACK 理解可靠传输、重传和累计确认。
  4. 解释三次握手和四次挥手各自解决的问题。
  5. 判断 TIME_WAIT 与 CLOSE_WAIT 分别意味着什么。
  6. 把 Connection refused、connect timeout、read timeout、reset、broken pipe 等现象映射到 TCP 生命周期。
  7. 在排查网络请求失败时,按“解析 -> 握手 -> 传输 -> 关闭”的顺序定位。

延伸阅读