当浏览器访问网站、客户端连接数据库,或者程序通过 Socket 交换数据时,底层经常都离不开 TCP。我们通常用“面向连接、可靠传输”概括它,但真正排查网络问题时,仅记住三次握手和四次挥手远远不够。
本文以现行 TCP 主规范 RFC 9293 为基础,从报文首部、序列号和确认机制开始,逐步解释连接建立、丢包重传、流量控制、拥塞控制、连接释放及常见编程陷阱。读完后,你应该能够看懂一条 TCP 流,并知道遇到超时、重传、粘包、TIME_WAIT 或 CLOSE_WAIT 时该从哪里入手。
一、TCP 到底提供了什么
TCP 位于传输层。IP 负责把数据报送往目标主机,TCP 则在端点之间提供可靠、按序、全双工的字节流。应用写入的是一串连续字节,TCP 会根据路径和协议状态把它们切分为若干报文段,再交给 IP 发送。
一条 TCP 连接通常由四元组唯一标识:
(源 IP, 源端口, 目的 IP, 目的端口)
网络分析中也常加入传输层协议,称为五元组。客户端通常使用临时端口,服务器监听固定端口。例如客户端 192.0.2.10:52000 可以连接服务器 198.51.100.8:443;同一个服务器端口能够同时服务大量不同四元组的连接。
TCP 的能力边界
- TCP 保证字节按序交给接收应用,但不保留应用消息边界。
- TCP 能检测传输错误并重传,但不承诺固定延迟,也不存在“永不失败”的连接。
- TCP 校验和用于发现传输差错,不提供机密性、身份认证或防篡改能力;需要安全通信时应在上层使用 TLS。
send()成功通常只说明数据进入了本机内核缓冲区,不代表对端应用已经读取或处理。
二、TCP 首部结构

| 字段 | 长度 | 作用 |
|---|---|---|
| Source/Destination Port | 各 16 位 | 标识两端应用进程 |
| Sequence Number | 32 位 | 本报文段第一个数据字节的序号;SYN 时表示初始序列号 |
| Acknowledgment Number | 32 位 | 累计确认,表示下一步期望收到的字节序号 |
| Data Offset | 4 位 | 以 32 位字为单位表示 TCP 首部长度 |
| Window | 16 位 | 接收方通告的可用接收窗口 |
| Checksum | 16 位 | 覆盖 TCP 首部、数据和 IP 伪首部;TCP 校验和不可省略 |
| Options | 0–40 字节 | 携带 MSS、窗口扩大、SACK、时间戳等能力 |
八个常见标志位
| 标志 | 含义 | 常见场景 |
|---|---|---|
| SYN | 同步序列号 | 建立连接 |
| ACK | 确认号有效 | 连接建立后绝大多数报文都会设置 |
| FIN | 本方向没有更多数据 | 正常关闭或半关闭 |
| RST | 立即重置连接 | 端口未监听、连接状态异常、应用中止 |
| PSH | 提示接收端尽快向应用交付 | 交互式数据;不能用来划分消息 |
| URG | 紧急指针有效 | 现代应用较少使用 |
| ECE/CWR | 显式拥塞通知相关 | 端点和网络支持 ECN 时使用 |
需要特别记住:序列号按字节计数,而不是按报文段计数。SYN 和 FIN 各自占用一个序号空间,纯 ACK 不占用序号。因此握手时收到 Seq=x 的 SYN,响应确认号就是 Ack=x+1。
三、三次握手:同步两端的序列号

假设客户端选择初始序列号 x,服务器选择 y,最基本的握手过程如下:
- 客户端发送
SYN, Seq=x,状态从 CLOSED 进入 SYN-SENT。 - 服务器回复
SYN+ACK, Seq=y, Ack=x+1,进入 SYN-RECEIVED。 - 客户端发送
ACK, Ack=y+1,双方随后进入 ESTABLISHED。
三次握手不只是确认“服务器在线”。它让双方交换并确认各自的初始序列号,同时验证双向通信路径,降低网络中延迟的旧 SYN 被误认为新连接的风险。第二步把服务器的 SYN 和对客户端 SYN 的 ACK 合并,所以最终是三个报文,而不是四个。
握手时通常还会协商什么
- MSS:本端愿意接收的最大 TCP 数据段大小。
- Window Scale:扩大 16 位 Window 字段的表达范围;只能在 SYN 中协商。
- SACK Permitted:表示后续可以使用选择性确认。
- Timestamps:用于更精确的 RTT 测量和防止序列号回绕造成歧义。
这些选项如果没有在握手阶段协商成功,不能在连接中途随意开启。抓包分析性能问题时,应先看 SYN/SYN-ACK 中双方实际谈成了哪些能力。
四、TCP 如何做到可靠传输

1. 序列号、累计 ACK 与乱序重组
如果接收方已经连续收到字节 [1000, 1500),它会回复 ACK=1500,意思是“1500 之前都收到了,请从 1500 继续”。若 [1500, 2000) 丢失,而 [2000, 2500) 先到达,接收方可以缓存乱序数据,但累计确认号仍保持 1500。
协商 SACK 后,接收方还可以同时报告 SACK [2000, 2500),告诉发送方后一段已经收到。这样发送方只需要补发缺口,不必盲目重传整片窗口。SACK 不会改变累计 ACK 字段原有的含义。
2. 校验和、去重与按序交付
接收端校验报文;损坏的数据不会作为有效字节交付。序列号还让接收端识别重复报文,并把乱序到达的数据重新排列。只有从当前期望序号开始的连续字节,才可以按序推进累计 ACK 并交给应用。
3. 超时重传与 RTO
发送方为未确认数据维护重传计时器。RTO 不是固定拍脑袋值,而是根据往返时间 RTT 的测量动态计算。RFC 6298 给出的基本思路是平滑 RTT 与 RTT 波动:
首次测量 R:
SRTT = R
RTTVAR = R / 2
RTO = SRTT + max(G, 4 × RTTVAR)
后续测量 R':
RTTVAR = 3/4 × RTTVAR + 1/4 × |SRTT - R'|
SRTT = 7/8 × SRTT + 1/8 × R'
RTO = SRTT + max(G, 4 × RTTVAR)
G 是时钟粒度。RFC 6298 的标准算法要求计算结果小于 1 秒时把 RTO 向上取到 1 秒,实现也可以采用更保守的值。发生 RTO 超时后,重传计时通常指数退避,避免在已经拥塞的网络中持续加压。对发生过重传的数据,除非使用时间戳等机制消除歧义,否则不能简单判断某个 ACK 对应原始报文还是重传报文,因此不能直接拿来采样 RTT。
4. 快速重传
等待 RTO 可能很慢。经典 RFC 5681 算法中,发送方收到 3 个重复 ACK 后,会认为确认号对应的缺口很可能已经丢失,立即执行快速重传。现代系统可能结合 SACK 和更先进的丢包检测算法,但“重复 ACK 表示后续数据仍在到达、前方可能有缺口”这一判断思路仍然重要。
五、流量控制与拥塞控制不是一回事
这两个概念经常混淆,但它们保护的对象不同:
| 机制 | 核心变量 | 保护对象 | 信息来源 |
|---|---|---|---|
| 流量控制 | rwnd |
接收端缓冲区与处理能力 | 接收方通告的 Window |
| 拥塞控制 | cwnd |
网络路径与路由器队列 | 发送方根据 ACK、丢包、ECN 等推断 |
发送方可保留在途、尚未确认的数据量,受两者较小值约束:
effective_window = min(rwnd, cwnd)
接收窗口 rwnd
接收应用来不及读取时,内核接收缓冲区逐渐被占满,通告窗口会缩小,甚至变成零。发送方此时停止发送普通数据,并通过窗口探测确认接收窗口何时重新打开。抓包中出现大量 ZeroWindow,优先检查接收应用是否处理过慢、阻塞或缓冲区设置不合理。
首部中的 Window 只有 16 位,最大原始值为 65535。Window Scale 选项允许把它左移最多 14 位,以适应高带宽时延积链路;缩放因子在握手中确定,抓包工具需要结合 SYN 才能还原真实窗口。
拥塞窗口 cwnd
cwnd 位于发送端,不直接写入 TCP 首部。经典拥塞控制通常包含:
- 慢启动:从较小窗口开始,随 ACK 到来快速扩大窗口,整体上近似每个 RTT 成倍增长。
- 拥塞避免:达到慢启动阈值
ssthresh后转为更平缓的增长。 - 快速重传:经典情况下收到 3 个重复 ACK 后重传缺失段。
- 快速恢复:发现单个丢包时避免完全回到初始发送状态,并在修复后继续传输。
不同操作系统可以采用不同的拥塞控制实现,但发送数据时仍必须同时尊重接收方窗口和网络拥塞约束。高吞吐连接不仅需要带宽,还需要足够大的窗口覆盖带宽时延积:
BDP = bandwidth × RTT
例如 100 Mbit/s、RTT 为 80 ms 的路径,带宽时延积约为 1 MB。若有效窗口只有 64 KB,即使链路没有丢包,也难以跑满带宽。
六、MSS、MTU 与分段
MTU 是链路层一次能够承载的最大 IP 包大小,MSS 是 TCP 一次报文段愿意接收的最大应用数据大小。以常见以太网 MTU 1500 为例:
- IPv4 基本首部 20 字节 + TCP 基本首部 20 字节,典型 MSS 为 1460。
- IPv6 基本首部 40 字节 + TCP 基本首部 20 字节,典型 MSS 为 1440。
实际 TCP 选项会占用额外首部空间,协议栈会相应控制数据长度。路径 MTU 发现失败时,可能出现握手成功、小包正常、大数据停住的“PMTU 黑洞”。排查时要检查 ICMP 是否被错误屏蔽、隧道是否降低 MTU,以及 MSS Clamp 是否合理。
还要注意网卡的 TSO/GSO/GRO/LRO 等卸载功能。若在发送主机本机抓包,可能看到远大于 MTU 的“TCP 报文”,那往往只是内核把大块数据交给网卡后再分段,并不代表超大 IP 包真的上了线。
七、为什么会出现“粘包”和“半包”
严格地说,这不是 TCP 协议故障,而是应用把字节流误当成消息队列。两次 send() 的数据可能在接收端一次 recv() 返回,也可能一次发送被拆成多次接收。PSH 标志同样不能充当可靠的应用消息边界。
常见的应用层分帧方式有三种:
- 固定长度:适合每条消息大小完全一致的协议。
- 分隔符:例如文本协议按 CRLF 分行,但必须处理转义和最大长度。
- 长度前缀:先发送固定宽度的长度字段,再发送对应字节数的负载,最通用。
下面是一个简化的 Python 长度前缀示例。重点不是语言,而是必须循环接收直到拿满指定长度,并限制消息上限:
import struct
MAX_MESSAGE = 4 * 1024 * 1024
def recv_exact(sock, size):
chunks = []
remaining = size
while remaining:
chunk = sock.recv(remaining)
if not chunk:
raise ConnectionError("peer closed the connection")
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)
def send_message(sock, payload):
if len(payload) > MAX_MESSAGE:
raise ValueError("message is too large")
sock.sendall(struct.pack("!I", len(payload)) + payload)
def recv_message(sock):
size = struct.unpack("!I", recv_exact(sock, 4))[0]
if size > MAX_MESSAGE:
raise ValueError("message is too large")
return recv_exact(sock, size)
!I 表示网络字节序的 32 位无符号长度。生产代码还需要读写超时、取消机制、并发限制、协议版本、认证和错误响应。
八、连接释放、半关闭与 TIME_WAIT
TCP 是全双工的,两个发送方向需要分别关闭。主动关闭方发送 FIN,只表示“我不会再发送数据”,仍可继续接收对端数据,这就是半关闭。因此典型正常关闭会看到四个报文:
A → B: FIN + ACK
B → A: ACK
B → A: FIN + ACK
A → B: ACK
第二个 ACK 可以和后续 FIN 合并,所以并非每次抓包都严格出现四个独立报文;双方也可能同时发送 FIN。应用可以使用 shutdown() 只关闭一个方向,再继续读取对端剩余响应。
TIME_WAIT 为什么存在
典型情况下,发送最后 ACK 的主动关闭方进入 TIME-WAIT,并等待 2 MSL。这样既能在最后 ACK 丢失、对端重发 FIN 时再次回应,也能让网络中属于旧连接的延迟报文自然消失,避免污染随后复用相同四元组的新连接。
大量 TIME_WAIT 不一定是故障,它首先说明系统创建了大量短连接且本端经常主动关闭。优先考虑连接复用、HTTP Keep-Alive 或连接池,并确认关闭责任是否合理。不要在不了解语义和安全影响时通过粗暴缩短等待时间来掩盖架构问题。
CLOSE_WAIT 又意味着什么
收到对端 FIN 后,本端回复 ACK 并进入 CLOSE-WAIT。若该状态长期大量堆积,通常是本端应用已经知道对方关闭,却迟迟没有调用 close() 释放自己的方向,常见原因是异常路径遗漏关闭、线程阻塞或资源生命周期管理错误。
RST 是异常中止
RST 会立即终止连接,不像 FIN 那样完成有序关闭。连接不存在、目标端口未监听、应用设置强制关闭,或一端收到无法匹配当前状态的报文时,都可能看到 RST。排查时必须结合 RST 由哪一端发出以及它之前的报文上下文。
九、超时、Keepalive 与应用心跳
TCP 不会自动给业务提供一个万能超时。可靠网络程序至少需要区分:
- 连接建立超时;
- 单次读写超时;
- 整个请求的截止时间;
- 空闲连接回收时间。
TCP Keepalive 是可选机制,通常需要应用显式开启,而且不同系统的默认探测间隔可能很长。它适合清理长期失联的连接,但不等同于业务健康检查。应用层心跳可以携带协议版本、会话状态或服务健康信息,也能采用更符合业务的超时策略。
操作系统仍认为连接 ESTABLISHED,不代表远端业务进程一定健康;一次 TCP ACK 也只证明对端协议栈收到了字节,不代表业务已经执行成功。
十、用 Wireshark 快速定位 TCP 问题
先按一条流过滤,再沿时间轴判断问题发生在哪个阶段。常用过滤条件:
tcp.port == 443
tcp.stream eq 7
tcp.flags.syn == 1 && tcp.flags.ack == 0
tcp.analysis.retransmission
tcp.analysis.fast_retransmission
tcp.analysis.duplicate_ack
tcp.analysis.zero_window
tcp.flags.reset == 1
| 现象 | 常见方向 |
|---|---|
| SYN 重传,没有 SYN-ACK | 路由、防火墙、安全组、服务未监听或 SYN 队列压力 |
| 立即收到 RST | 端口未监听、应用主动拒绝、连接状态不匹配 |
| 大量 Retransmission | 真实丢包、拥塞、无线质量、链路错误,也要排除抓包点丢包 |
| 大量 Duplicate ACK | 前方数据缺口、乱序或丢包 |
| ZeroWindow | 接收应用处理不及时或接收缓冲区不足 |
| 握手正常,大包停滞 | 路径 MTU 黑洞、ICMP 被屏蔽、隧道 MTU 不匹配 |
| 大量 CLOSE_WAIT | 本端应用收到 EOF 后未及时关闭 Socket |
| RTT 很高但不重传 | 物理距离、排队延迟、缓冲膨胀或服务端响应慢 |
Wireshark 的 tcp.analysis.* 是分析器根据抓包推断出的提示,不是线路中真实存在的字段。单点抓包也可能因为网卡卸载、抓包丢包或镜像端口缺包产生误判。关键问题最好在连接两端同时抓包,并统一时钟后对照。
十一、常见误区
- “三次握手后网络一定正常”:握手只验证当时的双向可达与连接状态,后续仍可能丢包、改路或中断。
- “一个 send 对应一个 recv”:TCP 是字节流,应用必须自行分帧。
- “ACK 表示业务处理成功”:ACK 是协议栈层面的字节确认,不是业务确认。
- “重传一定是服务器慢”:重传通常先指向链路丢包、拥塞或抓包位置问题。
- “TIME_WAIT 越少越好”:它承担最后 ACK 重发和隔离旧报文的职责,应先优化连接复用。
- “TCP 自带安全”:可靠性不等于加密与认证,敏感数据仍需 TLS。
十二、工程实践检查表
- 应用协议是否有清晰的消息边界、长度上限和版本字段。
- 所有读写是否正确处理短读、短写、EOF 和中断。
- 连接、请求、读写、空闲是否分别设置合理超时。
- 是否限制单连接缓冲、并发连接数和未完成请求数。
- 长连接是否有业务心跳与断线重连,并处理重复请求的幂等性。
- 是否区分正常 FIN、对端 RST、本地取消与超时错误。
- 跨公网传输是否使用 TLS,并正确验证证书与主机名。
- 排错时是否保留四元组、时间戳、错误码、连接状态和必要抓包。
结语
TCP 的“可靠”并不是一句抽象承诺,而是一整套相互配合的机制:三次握手同步连接状态,序列号和累计 ACK 维护连续字节流,重传修复丢包,rwnd 保护接收端,cwnd 保护网络,FIN 与 TIME_WAIT 则完成连接的有序退出。
真正掌握 TCP 的标志,是能把应用现象对应到协议状态:连接不上先看 SYN,传输卡顿看 RTT、重传和窗口,数据解析异常检查应用分帧,资源堆积则区分 TIME_WAIT 与 CLOSE_WAIT。这样,网络问题就不再只剩下“可能是网络不好”。


到此一游!