Appearance
第12章 TCP 标志位
序列号(Sequence Number)和确认号(Acknowledgment Number)负责标记字节流的位置,而标志位(Flags)则为 TCP 报文段(Segment)赋予了控制语义。连接建立、数据确认、单向关闭、立即重置以及显式拥塞反馈,这些复杂的网络行为,全都通过这几个单比特(bit)标志位及其组合来表达。
九个已分配标志的地图
在现代 TCP 首部中,我们通常会遇到以下 9 个标志位。在使用 Wireshark 抓包时,你能看到它不仅会把各个独立的比特位展开显示,还会贴心地汇总成类似 Flags: 0x012 (SYN, ACK) 这样的直观格式。
| 标志 | 掩码 | 核心语义 | 是否占用一个序列号位置 |
|---|---|---|---|
| FIN | 0x001 | 结束本端的数据发送 | 是 |
| SYN | 0x002 | 同步初始序列号 | 是 |
| RST | 0x004 | 重置连接,或拒绝没有匹配连接的报文 | 否 |
| PSH | 0x008 | 提示接收端尽快推送已排队的数据 | 否 |
| ACK | 0x010 | Acknowledgment Number 有效 | 否 |
| URG | 0x020 | Urgent Pointer 有效 | 否 |
| ECE | 0x040 | ECN 能力协商或拥塞反馈 | 否 |
| CWR | 0x080 | 经典 ECN 中,表明发送方已响应拥塞控制 | 否 |
| AE | 0x100 | Accurate ECN 使用的扩展标志 | 否 |
标志位是可以组合使用的。例如 SYN + ACK = 0x012、FIN + ACK = 0x011、PSH + ACK = 0x018、RST + ACK = 0x014。虽然这些十六进制数值能帮我们快速核对原始首部,但要理解报文段的完整语义,你还需要结合当前的连接状态、序列号(Seq)、确认号(Ack)、数据长度以及传输方向来综合判断。
回顾一下我们在本篇开头提到的共用样本,它的 Flags 字节就是 0x18,即 PSH 和 ACK 同时置位。这里 ACK 的作用是让 Ack = 0x10203041 生效;PSH 则是在提示接收端,收到当前这一批连续数据后请尽快交付给应用层;而 10 字节的有效载荷(Payload)则把样本的 Seq 从 0x6a1b2c3e 向前推进到了 0x6a1b2c48。这三个信息在 TCP 层各司其职,而应用层则会继续按照自己的分帧规则去解析 HELLO TCP\n 的具体含义。
SYN:为单向传输建立序列号起点
SYN 的全称是 Synchronize(同步)。当客户端发送 SYN, Seq = C_ISN 时,服务端就能知道客户端所选择的初始序列号(ISN)。接着,服务端会回复 SYN, ACK, Seq = S_ISN, Ack = C_ISN + 1,这既公布了服务端自己的序列号起点,也确认了客户端发来的 SYN。
需要特别注意的是,SYN 标志本身会占用一个序列号位置,因此第一个普通数据字节的序列号会从 ISN + 1 开始。此外,TCP 还经常利用握手阶段的 SYN 报文来声明或协商各种能力,比如最大报文段长度(MSS)、窗口缩放(Window Scale)、选择性确认(SACK Permitted)、时间戳(Timestamp)以及 ECN 能力。
常见握手时间线为:
text
客户端 服务端
SYN, Seq=1000 ----------->
<----------- SYN, ACK, Seq=7000, Ack=1001
ACK, Seq=1001, Ack=7001 ----------->虽然偶尔也会遇到像“同时打开(Simultaneous Open)”这样比较少见的连接路径,但我们在抓包分析时,核心还是根据实际的 Flags 和 TCP 状态机来推演。上面这三行经典的时间线,已经足以覆盖绝大多数“客户端主动连接、服务端被动监听”的常见场景了。
ACK:让确认号字段生效
当 ACK 标志位设为 1 时,TCP 首部中的确认号(Acknowledgment Number)字段才具有实际意义,它代表本端接下来期望收到的对端序列号。一旦连接进入同步状态,后续正常的报文段通常都会携带 ACK 标志。不论是纯粹的确认报文、携带数据的报文、包含 FIN 标志的报文,甚至许多带有 RST 标志的报文,都会将 ACK 置位。
与 SYN 和 FIN 不同,ACK 标志本身不占用序列号空间。举个例子,如果你发送了一个 Seq = 1001, Ack = 7001, Len = 0, ACK = 1 的报文段,那么本端下一次发送数据时,Seq 仍然可以从 1001 开始。而且,正如我们在第 11 章介绍过的那样,确认号采用的是累积确认语义,这意味着一个 ACK 就能够一次性确认多个连续到达的数据报文段。
FIN:结束一个方向的数据发送
FIN 标志代表发送方已经完成了自己这个方向的数据发送。它通常会跟在所有需要发送的数据之后,并且和 SYN 一样,它也会占用一个序列号位置。假设你发送了一个 Seq = 5001, Len = 300, FIN = 1 的报文段,对端在完整接收后,期望收到的下一个序列号应该是:
因此对端会回复一个确认号为 5302 的 ACK。在接收端,应用程序会先读完 FIN 之前的全部 300 字节数据,随后在调用 recv 时就会收到 EOF(文件结束符)。不过,接收端在收到 FIN 后,虽然知道对方不发了,但它自己的发送方向依然可以继续工作。这种双向独立关闭的机制,构成了 TCP “半关闭(Half-Close)”状态的基础。在第 17 章中,我们会把 FIN 标志、Socket API 以及 TCP 状态机放在同一条时间线上为你详细梳理。
在实际抓包中,FIN 经常和 ACK 组合在一起出现(FIN, ACK),有时也会和最后一批数据“搭车”一起发送。当然,由于 ACK 和 FIN 可以分开独立发送,所以 TCP 的正常关闭过程在抓包结果中,可能会呈现出经典的三次挥手、四次挥手,甚至更多个报文段的交互。
RST:立即撤销连接状态
RST 表示 Reset(重置连接)。常见的 RST 触发场景包括以下几种:
- SYN 报文到达了一个当前没有监听任何程序的端口,目标主机会直接退回一个
RST, ACK报文段; - 一个已建立的连接收到了与本地状态严重冲突的报文,且该报文满足重置校验条件;
- 应用程序请求了异常终止(Abortive Close),比如在启用
SO_LINGER选项并将超时时间设为 0 的情况下关闭套接字(Socket); - 系统进程崩溃,或者 TCP 协议栈在处理特定错误时,不得不强行撤销连接。
一旦收到有效的 RST,TCP 协议栈就会立刻无情地移除该连接的所有状态。此时,应用程序通常会抛出类似 ConnectionResetError、ECONNRESET 或者当前平台对应的连接重置错误。对比一下:正常的 FIN 路径会确保之前所有有序字节都能安全交付并产生 EOF;而 RST 路径则是彻头彻尾的异常终止,那些尚未交付给应用的数据很可能就随着连接状态的销毁而直接灰飞烟灭了。
在生产环境中遇到 RST 时,千万别急着下结论,你需要收集证据来确认它的真实来源。你可以通过分析四元组、TTL(或 Hop Limit)、IP ID,再结合两端双向抓包、主机日志甚至中间设备日志,来判断这个 RST 到底是由端点直接发出的,还是代理、防火墙或负载均衡器中间截断产生的。此外,由于攻击者经常伪造 RST 进行连接劫持,现代 TCP 实现还引入了 Challenge ACK(挑战确认)等机制,专门用来校验那些序列号落在接收窗口内、却又不完全精确匹配的 RST 报文。
PSH:提示尽快交付已排队数据
PSH 源自 TCP 的 Push(推送)功能。发送方会在某个报文段上设置 PSH 标志,以此来提示接收方的 TCP 协议栈:请把目前已经到达的这批连续数据,尽快交付给接收端应用程序,别再等缓冲区填满了。许多协议栈实现都会自动在当前发送队列的最后一个报文段上打上 PSH 标志,不过它的具体出现位置,还会受到系统缓冲、线程调度、报文分段策略以及具体实现细节的影响。
我们要牢记一点:TCP 是基于字节流的,应用层的消息边界只能由长度字段、分隔符、固定长度或其他应用层分帧规则来定义。在发送端,一次写入(Write)可能被拆分成多个报文段发送,而多次小的写入也可能被拼接到同一个报文段中;PSH 在这里仅仅提供“尽快推送”的提示,它并不是应用消息结束的标志。对于接收端来说,一次 recv 调用到底能返回多长的数据,仍然是由当前可用的连续字节数、接收缓冲区的大小以及系统的运行时调度共同决定的。
URG:让紧急指针生效
当 URG 被设置为 1 时,TCP 首部中 16 位的紧急指针(Urgent Pointer)字段才具有实际意义。根据 RFC 9293 的规范,这个指针提供了一个相对当前报文段 Seq 的正向偏移量,它指向的是“紧急数据”之后的第一个字节位置。接收方的 TCP 协议栈可以借此通知应用程序进入紧急模式(Urgent Mode)。
然而,由于历史上各大操作系统对紧急指针边界的解释,以及对 Socket “带外数据(Out-of-Band Data)”接口的实现存在较大分歧,再加上各类网络中间设备对 URG 的处理也缺乏一致性,这个功能在实践中非常容易踩坑。正因如此,RFC 9293 明确建议新开发的应用程序避开 TCP 的紧急机制,尽管为了向后兼容,规范依然要求现代 TCP 实现必须继续支持它。如今的现代自定义应用协议,往往更倾向于通过应用层消息类型、优先级字段或建立独立的控制流通道,来优雅地表达紧急控制信息。
ECE 与 CWR:经典 ECN 的拥塞反馈回路
显式拥塞通知(ECN,Explicit Congestion Notification)允许支持该机制的路由器在遇到网络拥堵时,不再简单粗暴地丢弃报文,而是在 IP 首部中打上 CE(Congestion Experienced,经历拥塞)标记,以此向端点发送拥塞信号。经典的 TCP ECN 机制巧妙地利用了 ECE 和 CWR 这两个标志位来构成反馈回路:
- 客户端在初始的 SYN 报文中设置
ECE = 1, CWR = 1,以此向服务端声明自己具备经典 ECN 能力; - 如果服务端也支持该能力,就会在回复的 SYN+ACK 报文中单独设置
ECE = 1,确认能力协商成功; - 进入数据传输阶段后,如果接收方发现收到的 IP 报文带有 CE 标记,就会在接下来返回给发送方的 ACK 报文中,将 ECE 标志置为 1;
- 发送方收到带 ECE 标志的 ACK 后,会立即执行拥塞控制算法(比如减小拥塞窗口),并在后续发出的新数据报文中设置 CWR(Congestion Window Reduced,拥塞窗口已减小)标志;
- 接收方一旦看到 CWR 标志,就知道发送方已经处理了这次拥塞,于是便结束这一轮的 ECE 回显。
通过这条反馈回路,ECN 信号依然能有效地驱动 TCP 的拥塞控制。CE 标记的最大价值在于,它能够在网络链路不丢弃珍贵报文的前提下,提前预警拥塞。至于收到预警后,拥塞窗口具体要缩减多少,则完全由系统当前采用的拥塞控制算法来决定。
AE:更精确的 AccECN 反馈机制
RFC 9768 将 TCP 首部标志区中 bit 7 曾用的 NS(Nonce Sum)标志,重新定义为 AE,也就是 Accurate ECN(精确 ECN)。支持 AccECN 的客户端会在初始 SYN 报文中使用 (AE, CWR, ECE) = (1, 1, 1) 的组合来请求协商。而支持该机制的服务端,则会在 SYN+ACK 报文中,通过这三个比特位的特定组合来确认协商结果,并同时反馈 SYN 报文到达时的 IP 层 ECN 状态。
在连接建立之后,AE、CWR 和 ECE 这三位将联手工作,提供比经典 ECN 更细粒度的计数反馈机制。除此之外,接收方还可以利用 AccECN 专属的 TCP 选项来携带更完整的拥塞计数。如此一来,发送方就能获取到比单一 ECE 状态精确得多的拥塞标记信息,从而做出更精准的速率调整。
需要留意的是,AE 属于比较前沿的协议扩展。你的操作系统是否支持、路径上的中间设备会不会乱改标志位,甚至你正在使用的 Wireshark 版本够不够新,都会直接影响我们在实验中的观察结果。在分析报文时,记得先查看 SYN 握手阶段的协商组合,然后再根据协商成功的模式去解释后续数据流中这三个标志位的含义。如果协商失败或者根本没有协商,那就老老实实退回到经典 ECN 含义或普通保留位的处理规则即可。
动手实验:抓取并观察 Flags 时间线
让我们在 Npcap Loopback Adapter 上开启抓包,首先来观察一个正常的 TCP 连接全过程。请打开终端 A 并运行以下 Python 代码启动一个简单的服务端:
powershell
python -c "import socket; l=socket.socket(); l.bind(('127.0.0.1',39012)); l.listen(1); c,_=l.accept(); f=c.makefile('rb'); print(f.read(5)); f.close(); c.sendall(b'OK'); c.shutdown(socket.SHUT_WR); c.close(); l.close()"接着在终端 B 运行客户端代码:
powershell
python -c "import socket; c=socket.create_connection(('127.0.0.1',39012)); c.sendall(b'hello'); f=c.makefile('rb'); print(f.read(2)); print('eof:',f.read(1)); f.close(); c.close()"测试完正常连接后,我们找一个当前没有监听程序的端口(例如 39013),来观察 TCP 是如何拒绝连接并返回 RST 标志的:
powershell
python -c "import socket; c=socket.socket(); c.settimeout(3); c.connect(('127.0.0.1',39013))"最后,我们在终端 A 的 39012 端口上重新启动一个会主动发出重置(RST)信号的异常服务端。这里用到的小技巧是利用 SO_LINGER 选项。在 Windows 的 Winsock API 中,这里的 linger 二元组对应着两个无符号短整数:
powershell
python -c "import socket,struct,time; l=socket.socket(); l.bind(('127.0.0.1',39012)); l.listen(1); c,_=l.accept(); c.recv(16); c.setsockopt(socket.SOL_SOCKET,socket.SO_LINGER,struct.pack('HH',1,0)); c.close(); l.close(); time.sleep(1)"随后,让客户端再次发起连接,并尝试读取服务端发来的数据:
powershell
python -c "import socket; c=socket.create_connection(('127.0.0.1',39012)); c.sendall(b'hello'); print(c.recv(16))"现在,回到 Wireshark 并使用以下过滤器表达式:
text
tcp.port == 39012 || tcp.port == 39013在过滤出来的报文列表中,你可以逐条记录 Time、Source、Destination、Seq、Ack、Len 和 Flags 这些核心字段。经过分析,你应该能清晰地整理出以下三类经典的时间线:
| 场景 | 关键 Flags | 应用现象 |
|---|---|---|
| 正常通信 | SYN → SYN+ACK → ACK;数据;FIN/ACK | 收到 OK 响应,随后顺利读到 EOF |
| 无监听端口 | SYN → RST+ACK | 调用 connect 后会极快地抛出“拒绝连接”错误 |
| 主动重置 | 握手与数据交互后,突然出现 RST 或 RST+ACK | 调用 recv 时会直接报连接重置错误 |
在你的抓包结果中,PSH 标志的具体出现位置完全取决于你本机的 TCP 协议栈实现。同样地,ECN 和 AccECN 相关的标志也会受制于你的操作系统配置以及沿途网络路径的支持度。在记录实验报告时,请如实记录你的实际观察值;如果没有观察到 ECN 协商的标志,把这种“协商缺席”的状态记录下来,本身也是一个很有价值的实验结论。
理解自测
- 假设一个报文段
Seq = 4000, Len = 50, SYN = 0, FIN = 1完整到达对端,接收方回复的 Ack 应该推进到哪个位置? - 假设抓包看到了一个带有
PSH, ACK标志且携带了 200 字节应用数据的报文段。接收端程序能仅仅通过这个 PSH 标志,就推断出应用层一条完整消息的长度吗? - 对于一个已经建立好的连接,如果突然收到一个有效的 RST 报文,应用程序通常会有什么样的直观感受或错误表现?
- 在经典 ECN 的数据传输阶段,ECE 和 CWR 这两个标志,分别是由哪一方(发送方还是接收方)、在什么样的时机下触发设置的?
- 客户端在初始发出的 SYN 报文中带上
(AE, CWR, ECE) = (1,1,1),这究竟是在向服务端请求什么?
参考答案
- 50 字节的有效数据会让序列号向前推进 50,而 FIN 标志本身又会占用 1 个序列号位置,因此
Ack = 4000 + 50 + 1 = 4051。 - 绝对不能。应用层消息的长度边界只能由应用层自己的分帧规则(比如长度前缀、特殊分隔符等)来界定;PSH 标志仅仅是 TCP 层面的一个“尽快推送排队数据”的优化提示,与业务层面的消息边界毫无关系。
- TCP 协议栈会立即强行撤销该连接的所有状态,当前正在阻塞中的 Socket 调用或者后续的读写操作,通常都会抛出类似
ConnectionResetError的连接重置异常。 - 接收方:在发现收到的 IP 报文携带 CE 标记后,就会在随后发回的 ACK 中持续设置 ECE;发送方:在收到带 ECE 的确认并执行拥塞降速动作后,会在新发出的数据报文段中设置 CWR,以此通知接收方“我已降速”。
- 这是客户端在向服务端发出请求,希望在这条连接上协商并启用更细粒度、更精确的 AccECN(Accurate ECN)拥塞反馈机制。
本章小结
- SYN 和 FIN 都会实打实地占用一个序列号位置;而其他所有的标志位仅仅提供控制语义,并不会增加数据流的长度。
- ACK 的存在让确认号字段变得合法有效,PSH 用于提示接收端尽早推送缓存数据,而 URG 则负责让紧急指针生效。
- FIN 用于优雅地结束单个方向的数据发送并向应用层传递 EOF 信号,而 RST 则极其粗暴地立即撤销连接状态,并向应用层报告严重的连接重置异常。
- ECE 和 CWR 构成了经典 ECN 的拥塞反馈闭环,而较新的 AE 标志则与它们俩共同协作,支持 AccECN 机制以提供更精细的拥塞反馈。
- 绝不要孤立地看待标志位,必须将它们与数据传输方向、TCP 连接状态、Seq、Ack、数据长度(TCP Len)以及连接初期的协商结果联合起来,才能给出准确的解释。
规范依据
- RFC 9293: Transmission Control Protocol (TCP):核心规范,详细定义了 SYN、ACK、FIN、RST、PSH、URG 以及当前 TCP 的所有基础行为。
- RFC 3168: The Addition of Explicit Congestion Notification (ECN) to IP:定义了经典 ECN 机制中 ECE 和 CWR 的协商与反馈流程。
- RFC 9768: More Accurate Explicit Congestion Notification (AccECN) Feedback in TCP:定义了全新的 AE 标志以及 AccECN 相关的精确反馈协议。
上一章:第11章 Acknowledgment Number · 所属篇:第三篇 · 下一章:第13章 固定字段 · 教程总览