2、TCP报文段结构:从端口到校验和的完整拆解

TCP报文段的结构,说白了就是TCP协议在网络上传输数据的「信封」。我刚开始调网络协议栈的时候,总觉得这些字段就是背背概念,直到有一次线上服务出现大量丢包,我才真正意识到——每一个bit的设计都有它的道理。

今天咱们就把这个信封拆开,看看里面到底装了些什么。

2.1 源端口与目的端口:谁在跟谁说话

端口号是TCP层的「门牌号」。源端口标识发送方的应用程序,目的端口标识接收方的应用程序。你想想看,IP地址只能把数据包送到某台机器,但机器上跑着那么多服务——Web服务器、数据库、SSH——到底该交给谁?端口号就是干这个的。

关键点:源端口 + 源IP + 目的端口 + 目的IP,这四元组唯一确定一条TCP连接。

我记得有一次排查一个连接泄漏的问题,发现大量TIME_WAIT状态的连接,就是因为客户端每次请求都随机分配了不同的源端口,导致四元组无法复用。嗯,这里要注意,源端口的选择策略直接影响连接复用效率。

2.2 序列号与确认号:TCP的「快递单号」

序列号(Sequence Number)和确认号(Acknowledgment Number)是TCP可靠传输的基石。序列号标记了本报文段数据的第一个字节在整个数据流中的位置,确认号则表示「我已经收到了你发来的哪些数据,接下来请发这个序号开始的」。说白了,这就是TCP的收发记账本。

我习惯把序列号理解成快递包裹上的单号——每个包裹都有唯一编号,收件人收到后回执说「1号包裹已签收,请发2号」。TCP也是这样,只不过它不按「个」算,按「字节」算。

字段 长度 作用
序列号 32位 标识本报文段数据的第一个字节序号
确认号 32位 期望收到的下一个字节序号

个人经验:我曾经在调优一个高吞吐的文件传输服务时,发现序列号回绕(wraparound)导致接收端校验失败。32位的序列号空间在高速链路上很容易耗尽,这时候就需要PAWS(Protection Against Wrapped Sequences)机制来兜底。

2.3 标志位:TCP的「控制开关」

标志位是TCP报文段里最灵活的部分。一共6个bit,每个bit代表一个控制信号。我经常跟团队里的新人说,理解了这6个标志位,你就理解了TCP状态机的一半。

  • SYN:发起连接。三次握手的第一次和第二次报文都会带上它。
  • ACK:确认收到。除了SYN报文,几乎所有报文都带ACK。
  • FIN:关闭连接。表示发送方没有数据要发了。
  • RST:重置连接。遇到异常情况时强制断开。
  • PSH:推送数据。告诉接收方别缓存了,赶紧交给上层应用。
  • URG:紧急指针。很少用,但设计上是为了处理带外数据。

你可能会问:为什么需要PSH?其实TCP默认会做Nagle算法,攒够一定数据才发。但有些交互式应用(比如SSH敲命令)希望立即发送,PSH就是干这个的。我遇到过一个问题:一个实时监控系统延迟很高,排查下来发现是Nagle算法和PSH标志位没配合好,数据被延迟发送了。

避坑指南:我曾经在调试一个防火墙策略时,发现RST报文被误拦截,导致客户端一直收不到连接拒绝信号,超时等待了很长时间。RST报文虽然短,但在异常处理中至关重要,别把它当普通数据包过滤掉。

2.4 窗口大小:TCP的「流量阀门」

窗口大小字段告诉对方:「我的接收缓冲区还能装多少数据,你悠着点发」。这是一个16位的字段,最大只能表示65535字节。但在高速网络上,这个值远远不够——你想想看,一个万兆网卡,65535字节的窗口连一个RTT都填不满。

所以后来有了窗口缩放选项(Window Scale),通过三次握手协商一个缩放因子,把窗口大小扩展到最大1GB。我调优过的一个数据中心项目,就是靠调整窗口缩放因子,把吞吐量从200Mbps提升到了2Gbps。

核心公式:吞吐量 ≈ 窗口大小 / RTT。窗口越大,吞吐越高,但前提是网络不能丢包。

2.5 校验和:TCP的「防伪标签」

校验和覆盖了整个TCP报文段(头部+数据)以及一个伪头部(包含源IP、目的IP、协议类型和TCP长度)。为什么需要伪头部?因为TCP要确保数据没有被错误地投递到别的机器上。说白了,校验和既检查数据完整性,也检查路由正确性。

我记得有一次在嵌入式设备上调试,发现TCP连接频繁断开,抓包一看校验和全是错的。最后定位到是网卡驱动的硬件校验和卸载(checksum offloading)有bug,数据在传输过程中被损坏了。嗯,从那以后我养成了一个习惯:遇到奇怪的TCP问题,先关掉硬件校验和卸载试试。

2.6 整体结构一览

下面这张图把TCP报文段的完整结构画了出来,你可以对照着看每个字段的位置和长度。

TCP报文段结构 源端口 (16位) 目的端口 (16位) 序列号 (32位) 确认号 (32位) 数据偏移 保留 标志位 (6位) 窗口大小 (16位) 校验和 (16位) 紧急指针 (16位) 选项(可变长度,填充到32位对齐) 数据(应用层载荷) ← TCP头部(20字节固定 + 选项)→

2.7 几个容易踩的坑

最后分享几个我在实际项目中遇到的跟报文段结构相关的问题:

  • 端口复用冲突:我曾经在容器化部署时,宿主机和容器监听了同一个端口,导致SYN报文被错误路由。解决办法是用SO_REUSEADDR选项。
  • 序列号预测攻击:早期TCP实现使用固定的初始序列号,容易被伪造RST报文攻击。现在内核都用了随机化ISN,但如果你自己实现协议栈,千万别忘了这个。
  • 窗口缩放协商失败:有些老旧设备不支持窗口缩放选项,如果两端协商不一致,窗口会回退到65535。我遇到过跨运营商传输大文件时吞吐上不去,最后发现是中间设备把窗口缩放选项给丢了。

调试小技巧:用tcpdump抓包时,加上-S参数可以显示绝对序列号,而不是相对序列号。这在分析序列号回绕问题时特别有用。我习惯在排查连接异常时,先看SYN和SYN-ACK报文里的窗口缩放因子是否一致。