跳转到主内容TCP | 物联网民工

TCP

页面打开慢、接口超时、抓包里出现重传和重复确认时,问题往往不在 HTTP 语义本身,而在它下面的传输连接。TCP 常被概括成“可靠传输协议”,但真正决定工程判断的,是它如何通过连接、确认、重传、窗口和按序交付,把不可靠的网络路径变成可用的字节流。

TCP 的代价也在这里。它解决了开放互联网里的基本可靠传输问题,也把后来的 Web 性能、弱网体验和多路复用限制一起带到应用层之下。HTTP/2 为什么会被丢包拖住,QUIC 为什么要重做一层传输,答案都在 TCP 的行为模型里。

TCP 的核心,不是“保证数据一定到达”,而是在不可靠 IP 之上,用连接状态、确认重传、窗口控制和按序交付换出一条可持续传输的字节流

它为什么会出现

IP 解决的是“把一个个数据包尽力送到目标地址”,不是“替应用可靠地交付一段完整数据”。在更早的网络现实里,这会直接带来几类问题:

  • 包可能丢
  • 包可能乱序到达
  • 包可能重复到达
  • 发送方可能远快于接收方
  • 整个网络可能因为过量发送而进入拥塞

如果每个应用都自己处理这些问题,代价很高,而且互操作性很差。TCP 的价值就在于把这些高频共性问题收束为一层通用机制,让上层应用拿到的是连续字节流,而不是一组容易失序的数据报。

它是谁在什么背景下做出来的

TCP 出自早期互联网协议体系,由 DARPA 资助推动,后来成为 TCP/IP 协议族的关键组成部分。它面对的不是今天单一厂商可控的数据中心,而是异构主机、长距离链路和不稳定公网。

这决定了 TCP 的设计重心:

  • 默认底层网络并不可靠
  • 默认通信双方能力和处理速度并不一致
  • 默认连接可能持续一段时间,而不是只发一个数据报就结束
  • 默认协议一旦大规模部署,就必须长期兼容

所以 TCP 不是一个只为“局域网内高速传文件”优化的协议,而是一种在开放互联网里尽量保守、尽量稳妥的通用传输层方案。

先理解它的主模型

TCP 可以先看成四个关键词:

  • 面向连接
  • 面向字节流
  • 确认与重传
  • 流量控制与拥塞控制

这四个点里,字节流最容易被低估。TCP 不知道 HTTP 请求、TLS 记录或者自定义协议里的一条消息从哪里开始、到哪里结束。对 TCP 来说,应用交给它的是一串字节;它负责的是尽量可靠、按序地把这串字节交给对端。

这既是它的优势,也是很多限制的来源。

一条典型链路

看一条典型的 TCP 连接建立和数据交换路径:

sequenceDiagram participant C as 客户端 participant S as 服务器 C->>S: SYN S->>C: SYN + ACK C->>S: ACK C->>S: Data(seq=...) S->>C: ACK(ack=...) S->>C: Data(seq=...) C->>S: ACK(ack=...)

这条主链路背后,TCP 同时完成几件事:

  • 确认双方都愿意建立连接
  • 为双向传输各自建立序号空间
  • 通过确认号告诉对方“哪些字节已经收到”
  • 在丢包时触发重传
  • 通过窗口和拥塞控制避免把接收端或网络本身压垮

表面上只是三次握手加一串 ACK,实际承载的是一套持续维护连接状态的机制。

为什么要三次握手

三次握手的作用,是让双方都确认两件事:

  • 对方是否可达
  • 双方的初始序号和连接状态是否已经建立

可以写成这样:

sequenceDiagram participant C as 客户端 participant S as 服务器 C->>S: SYN(seq=x) S->>C: SYN(seq=y) + ACK(x+1) C->>S: ACK(y+1)

如果只有两次握手,服务端无法确认客户端是否收到了自己的 SYN+ACK,连接状态就可能悬空。第三次握手的作用,是把“双方都知道对方可收发”的状态闭合起来。

这也解释了为什么抓包里看到大量 SYN 发出却没拿到完整三次握手时,问题通常要先往网络可达性、防火墙、监听端口或者中间设备丢包上查,而不是先怀疑应用协议。

为什么 TCP 要按序交付

TCP 不只是可靠传输,还承诺按序交付。这一层承诺直接简化了应用实现:

  • 应用不必自己重组乱序数据
  • 上层协议可以按连续字节流解析
  • 很多传统协议实现能保持简单

但代价也很大。只要某个前面的数据段没到,后面已经到了的数据也不能先交给应用。这就是 TCP 队头阻塞的根源。

这个设计在文件传输、数据库连接、早期 Web 里都很自然,因为一条连接承载的通常就是一段线性数据流。到了后来 HTTP/2 这种“一条连接里并发很多请求”的场景,问题就开始放大了。

为什么确认和重传会带来性能代价

TCP 靠确认和重传换可靠性,但它们不是免费的。

当一个数据段丢失时,发送方通常会通过下面两类信号发现:

  • 超时还没收到确认
  • 连续收到重复确认,推测中间有段丢了

随后它会重传缺失的数据。这个机制保证了可靠性,但也带来连锁反应:

  • 等待确认会增加时延
  • 重传会占用带宽
  • 按序交付会让后续已到达数据暂时不能上交
  • 拥塞控制可能进一步收紧发送速度

所以“TCP 保证可靠”这句话的另一面是:它用更多等待、更多状态和更多保守控制来换可靠。

为什么窗口控制是必需的

TCP 不可能让发送方永远按自己最快速度发。问题至少有两层。

第一层是接收方未必处理得过来,所以需要接收窗口告诉发送方:“我还能接多少。”

第二层是网络本身也未必扛得住持续高速发送,所以还要有拥塞窗口控制进入网络的数据量。

这两个窗口常被混在一起,但职责不同:

  • 接收窗口解决“对端来不来得及收”
  • 拥塞窗口解决“网络来不来得及运”

吞吐上不去时,如果不区分这两层,很容易误判成单纯“带宽不够”。

为什么 HTTP/2 跑在 TCP 上还是会卡

HTTP/2 已经把多路复用做进应用层,同一条连接上可以并发多个请求和响应,但它下面仍然是 TCP。

这意味着:

  • HTTP/2 的多个流共享同一条 TCP 字节流
  • 只要前面某段 TCP 数据丢了,后续段哪怕属于别的 HTTP 流,也要等待
  • 应用层多路复用并不能消除传输层按序交付带来的阻塞

这就是为什么 QUIC 会把多路复用下沉到传输层:HTTP/2 当然有流,但 TCP 只看到一条字节流。

四次挥手在解决什么

连接关闭也不是一句“断开”就够了。TCP 是全双工连接,两边都可能还有数据没发完,所以关闭要分两步进行:

sequenceDiagram participant C as 客户端 participant S as 服务器 C->>S: FIN S->>C: ACK S->>C: FIN C->>S: ACK

一端发 FIN,表示“我这边没数据要发了”;另一端确认后,仍然可以继续把自己剩余的数据发完,最后再发送自己的 FIN。这就是四次挥手需要四个报文的原因。

所以看到连接进入 FIN_WAITTIME_WAIT 之类状态时,它还处在关闭流程里,而不是已经完全释放。

抓包时真正该看什么

TCP 抓包最容易失焦的地方,是一上来就看头字段大全。更有效的顺序是下面这几步。

先看连接是否建立完成

先确认:

  • SYN
  • SYN+ACK
  • 最后的 ACK

这三步是否完整。如果三次握手没有完成,先排查网络可达性、防火墙、监听端口或中间设备,再看应用层。

再看有没有重传、重复确认和乱序

这些现象信息量很高:

  • Retransmission
  • Dup ACK
  • Out-of-Order
  • Zero Window

它们通常指向丢包、接收端压力、链路波动或者路径质量问题。

最后看 RTT 和窗口变化

很多“时快时慢”的问题,根因不在服务端逻辑本身,而是:

  • RTT 变大
  • 拥塞窗口收缩
  • 接收窗口变小
  • 重传把发送节奏打乱

如果只盯着应用层耗时,不看这层变化,判断容易跑偏。

工程上怎么看 TCP

  • TCP 不只是“可靠传输”,它的代价主要体现在连接状态、按序交付和重传控制里
  • 字节流不是消息流,应用层必须自己定义消息边界
  • 队头阻塞来自 TCP 的按序交付,而不是 HTTP/2 自己额外制造的
  • 吞吐问题不能只看带宽,窗口、RTT、丢包和拥塞控制往往更关键
  • QUIC 不是 TCP 的改名版,而是把现代 Web 更需要的能力重新放到传输层

继续阅读

  • TCP 状态为什么能看出连接卡在哪:继续看 SYN、ESTABLISHED、CLOSE-WAIT 和 TIME-WAIT 怎样帮助定位连接阶段
  • TCP 拥塞控制为什么让丢包后的网络变慢:继续看拥塞窗口、RTT、丢包和 RTO 怎样影响吞吐
  • IP:TCP 之下负责可达性、路由和尽力而为转发的网络层
  • NAT:TCP 连接穿过地址改写边界时为什么仍然依赖状态维持
  • HTTP:应用层的资源语义和中间层为什么建在 TCP 之上
  • HTTPSTCP + TLS 怎样把 HTTP 放进安全通道
  • QUIC:为什么现代 Web 会绕开 TCP 的部分代价

参考资料