QUIC协议数据格式详解:长头短头、变长整数与帧结构全解析
2026/9/17 4:44:09 网站建设 项目流程

写这篇笔记的起因,是我在实现一个最小 QUIC 协议栈时,被包里面那堆变长字段狠狠折腾了几周。很多人看 QUIC 的 RFC 9000 都会有一个感觉:概念好懂,但落到字节层面就头大。长头短头、连接 ID、包号、帧类型、流 ID、Offset、ACK Range,每个字段都不是定长的,很多还要按“几位前缀 + 变长整数”来解析,稍不留神就会把下一个字段的起点读错。这篇内容就是我踩完坑之后的整理,把 QUIC 的数据格式从头到帧掰开讲清楚,适合正在调试 QUIC 抓包结果、或者准备自己撸一个协议栈的开发者。不管你是只想知道 tshark 里那些字段怎么来的,还是真的要写解析器,这篇都值得你花点时间过一遍。

1. 数据格式的设计背景:为什么 QUIC 不沿用 TCP 的头格式

1.1 TCP 头部的思维方式,在 QUIC 这里行不通

先回看一下传统 TCP 头:固定 20 字节开头,源端口、目的端口、序号、确认号、标志位、窗口大小、校验和、紧急指针,每个字段的位置都是写死的。对应到代码里,解析 TCP 头很简单,拿着 offset 从 0 开始往后数就行。但它的问题也很明显:加新功能就得改头部布局,旧设备无法识别新标志位,中间设备还会因为某些字段没语义而随意丢弃或篡改。

QUIC 的设计目标之一,就是让协议未来能平滑演进,同时把所有功能都放进“帧”这个统一容器里。你在 QUIC 的数据包里几乎看不到“TCP 式”的头部选项区,取而代之的是长头/短头两个最小公共头部,后面跟着一串帧。这种“头部只负责寻址和状态切换,业务逻辑都交给帧”的思想,是理解 QUIC 数据格式的第一把钥匙。

另一个很关键的背景是加密。QUIC 的绝大多数字段在整包上是有认证的,甚至部分字段还做了加密。头部里的连接 ID、包号,帧里的 Stream ID、Offset,讲究非常多。因为有了加密,你没法像看 TCP 头那样直接用一个十六进制工具去解读 QUIC 报文,必须先建立连接、拿到密钥,才能在抓包工具里看到明文帧。这也是为什么网上讨论 QUIC 实现的帖子,有很大一部分都在聊 TLS 1.3 握手和密钥导出,因为数据格式的不透明性会直接影响调试体验。

1.2 把“连接”语义放进 64 位的 Connection ID

TCP 在两端之间用“四元组(源 IP、源端口、目的 IP、目的端口)”来唯一标识一条连接。QUIC 虽然也用 IP 和端口,但真正的连接锚点是 Connection ID,原因有二。

第一,连接迁移。手机从 Wi-Fi 切到 4G,IP 变了,TCP 连接直接断了;QUIC 只要连接 ID 不变,服务端仍然能识别出这是同一条连接,连接就能继续用。第二,负载均衡。服务端收到 QUIC 包后,不需要重新计算四元组哈希,直接读目标连接 ID 就能决定把包转发给哪个后端实例,这在大型数据中心里非常有用。

不过连接 ID 也从 0 到 20 字节不等,所以它在头部里不能像 TCP 端口那样固定占两个字节。长头里设计了两个长度字段:目标连接 ID 长度和源连接 ID 长度,每个各占 1 字节。短头里则不再重复长度,因为连接建立后,对端 ID 长度已经是双方协商好并缓存下来的状态,不需要每次都在线上重复传。

1.3 定长和变长混合,是效率优先的选择

你可能会问:为什么不干脆把所有字段都用固定长度,解析不是更省事?答案很简单,传输层协议的数据头在每一条报文里都会出现,能省一个字节,收益都会被放大到成百上千万个包上。QUIC 选择了一种很聪明的折中:头部里诸如类型、长度、ID 这类字段,用“变长整数”来编码;而版本号、连接 ID 长度这类必须定长的字段,就老老实实占用固定字节数。

所以你在实际抓到的 QUIC 包里,会看到“几个字节定长字段 + 几个字节变长字段 + 帧数据”这种混合结构。解析的时候最忌讳把每一个字节都当成固定 offset,正确姿势是先读变长字段的长度前缀,算出它实际占了几字节,再往后移动指针。下面几节我会把这两种编码方式都拆开讲。

2. 头部格式拆解:长头与短头的完整字节布局

2.1 第一个字节决定了你怎么看后面所有数据

QUIC 数据包的第一个字节最高位是 Header Form 位:为 1 时代表长头(Long Header),为 0 时代表短头(Short Header)。长头用于连接建立阶段,也就是 Initial、Handshake、0-RTT、Retry 这些包;短头用于连接建立完成后的 1-RTT 数据包。

为什么需要两种头?因为阶段不同,需要携带的信息不同。连接刚建立时,双方可能还没有交换连接 ID,也不知道包号基数,所以长头必须带完整版本号和双向连接 ID。连接建立之后,双方都记住了必要状态,短头就可以大幅压缩,省掉版本号、源连接 ID、甚至某些长度字段,只保留最核心的包号和目标连接 ID。这个设计有点像 HTTP 的请求头和请求体:头部小到极致,才有更多空间留给真正的业务数据。

长头里还有一个 Fixed Bit,固定为 1。这个位存在的意义是给将来可能出现的“非 QUIC 协议”预留区分空间,同时也能帮助中间盒区分 QUIC 和别的 UDP 载荷。如果你在调试时看到某个 QUIC 包第一位为 0x40 或 0x60 之类的值,就要怀疑是不是抓到了老版本或者错误标记的载荷。

2.2 长头字段逐个看:版本号、类型、连接 ID

长头的标准布局如下(以 RFC 9000 为基准):

Long Header Packet { Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2), Type-Specific Bits (4), Version (32), Destination Connection ID Length (8), Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160), Type-Specific Payload (..), }

第一字节的低 2 位是 Long Packet Type,0 表示 Initial,1 表示 0-RTT,2 表示 Handshake,3 表示 Retry。Type-Specific Bits 低 4 位在 Initial、Handshake、0-RTT 里有不同含义,其中预留位必须为 0,否则直接视为非法报文。

Version 是固定的 4 字节。QUIC v1 的版本号是0x00000001,将来如果协议有大版本更新,这个数字会变。Header Form、Fixed Bit、Type 这些位在头部最前面,是为了让接收方在不读完整版本号前,就能判断包类型并按对应逻辑处理。Version Negotiation 包比较特殊,它的第一个字节和普通长头不一样,后面不跟连接 ID 长度,而是跟着一堆支持的版本列表,格式要单独处理。

接下来是两个长度字段。注意:连接 ID 长度本身是 1 字节无符号整数,加上这个数字之后,才是实际要读的连接 ID 字节数。目标连接 ID 通常由服务端选择,用于包路由;源连接 ID 由发送方生成,用于在响应包中填写目标连接 ID。在 Initial 包中,客户端如果之前没有服务端连接 ID,会填一个长度为 0 的目标连接 ID,服务端会用自己的连接 ID 回复。

Type-Specific Payload 因包类型而异。Initial 包里包含 Token Length 和 Token,可以用来做地址校验;然后是 Length、Packet Number 和加密负载。Handshake 包没有 Token,直接是 Length、Packet Number 和加密负载。0-RTT 包同样没有 Token。Retry 包则包含服务端生成的 Retry Token 和 Retry Integrity Tag,用于防伪造。

2.3 短头字段逐个看:包号和 Key Phase 是关键

连接建立完成后,两端进入 1-RTT 阶段,这时发送的短头包布局如下:

Short Header Packet { Header Form (1) = 0, Fixed Bit (1) = 1, Spin Bit (1), Reserved Bits (2), Key Phase (1), Packet Number Length (2), Destination Connection ID (0..160), Packet Number (8..32), Packet Payload (..), }

这里有一个很容易忽略的点:短头里没有目标连接 ID 长度字段,接收端是通过 TLS 传输参数或对端长头里声明的连接 ID 计算出来的。所以抓包工具能正确解析,前提是它已经跟踪并缓存了每个连接的目标连接 ID 长度。

短头里的 Spin Bit 是让人人都能观测到的“主动探测”位,主要给运营商做网络延迟抖动测量用。Reserved Bits 固定为 0,收到非 0 直接丢包。Key Phase 表示当前使用的是哪一把 1-RTT 密钥,这是 QUIC 密钥更新机制的关键,每次密钥更新后这个位会反转,接收端才能知道应该用新密钥还是旧密钥解密。

Packet Number Length 占 2 位,表示包号的长度:0 代表 1 字节包号,1 代表 2 字节,2 代表 3 字节,3 代表 4 字节。包号长度之所以是可变的,是因为 QUIC 的包号压缩策略:连接初期包号小,用 1 字节就够;随着包号变大,再逐步扩展。接收端不需要真的记录每个包的完整包号,只需要在本地维护一个最大包号基数,再结合包里省略掉的低位部分,即可还原完整包号。

2.4 变长整数:快速上手和容易翻车的地方

QUIC 里到处可见变长整数(VARINT)。它最多占用 8 字节,但实际只编码 62 位有效值,前 2 位表示长度,剩余位是大端整数。规则如下:

前 2 位总字节数有效位数
0016
01214
10430
11862

举个例子,十六进制0x25的二进制是00 100101,前缀是 00,表示 1 字节变长整数,值就是二进制100101,即 37。十六进制0x4123的二进制是01 000001 00100011,前缀 01 表示 2 字节,把前两位抹掉后剩下 14 位,值是0x0123,即 291。解析时先读首字节的高 2 位,决定要读多少字节,再读取后续字节并组装成整数。

实际开发中,变长整数最容易踩的坑有三个。第一,很多新手只看了低 6 位,忘了高 2 位同时也是数据的一部分,导致大值解析出错。第二,写入和读取大小时要严格保持最低编码字节数一致,QUIC 要求编码器必须使用最小长度表示一个值,比如值为 0 就必须用 1 字节0x00,不能写成 2 字节0x4000,否则接收端会判为格式错误。第三,要注意降级兼容:实现新协议版本时,如果某个字段在将来变成变长,老实现读了高 2 位但不认识,会直接拒绝,这种“向前不兼容”是有意为之,防止歧义。

3. 帧层数据格式:业务逻辑的统一容器

3.1 帧头只是一个大类型字节吗

长头/短头之后就是 Packet Payload,里面装着一串帧。每个帧由“帧类型字节 + 若干字段”组成。一个包可以包含多个帧,帧之间没有分隔符,不能像解析 TCP 选项那样按固定长度遍历,而是每解析完一个帧,根据该帧的字段长度算出下一个帧起始位置。

帧类型本身是一个变长整数,但实际常用类型都在 0x00 到 0x1e 之间,占用 1 字节。所有未知帧类型都必须触发 CONNECTION_CLOSE,这是 QUIC 相对宽松的帧格式里很少见的“强约束”,因为协议希望新帧类型能被安全引入,但又不想让旧实现对未知帧自作主张地做不完整处理。

下面这张表列出了 v1 里所有标准帧类型:

帧类型名称含义
0x00PADDING填充,用于混淆包长或扩大包
0x01PING保活,确认路径可达
0x02ACK确认,不带 ECN 信息
0x03ACK_ECN确认,带 ECN 计数
0x04RESET_STREAM终止发送方流
0x05STOP_SENDING请求对端停止发送流数据
0x06CRYPTO传输 TLS 握手数据
0x07NEW_TOKEN提供新地址校验令牌
0x08..0x0fSTREAM携带流数据,低 4 位为标志
0x10MAX_DATA通知连接级流量上限
0x11MAX_STREAM_DATA通知单流流量上限
0x12 / 0x13MAX_STREAMS双向/单向最大流数
0x14DATA_BLOCKED连接级数据被阻塞
0x15STREAM_DATA_BLOCKED单流数据被阻塞
0x16 / 0x17STREAMS_BLOCKED双向/单向流数被阻塞
0x18NEW_CONNECTION_ID告知新连接 ID
0x19RETIRE_CONNECTION_ID废弃某个连接 ID
0x1aPATH_CHALLENGE路径连通性探测
0x1bPATH_RESPONSE路径探测响应
0x1cCONNECTION_CLOSE应用层关闭连接
0x1dCONNECTION_CLOSE传输层关闭连接
0x1eHANDSHAKE_DONE服务端通知握手完成

3.2 ACK 帧的 Range 压缩原理

ACK 帧是 QUIC 里最绕的帧之一,但它的设计非常值得学。TCP 的 SACK 是“几段区间就写几个区间块”,QUIC 则是用“一个最大确认号 + 范围计数 + 多个长度”的方式压缩范围描述。

标准格式如下:

ACK Frame { Type (i) = 0x02..0x03, Largest Acknowledged (i), ACK Delay (i), ACK Range Count (i), First ACK Range (i), ACK Range (..) ..., [ECN Counts (..)], }

Largest Acknowledged 表示当前确认范围内最大的包号。First ACK Range 表示这个最大包号往回连续确认了多少个包,比如 Largest Acknowledged 是 100,First ACK Range 是 9,那就意味着 100 到 91 这 10 个包都被确认了。接下来是交替出现的 Gap 和 ACK Range:Gap 表示跳过了多少个未确认包,ACK Range 表示下一个连续确认区间的长度。这种“先确认一个区间,再跳一段,再确认一个区间”的编码方式非常紧凑,尤其适合大带宽高丢包场景。

实际调试 ACK 帧时,最需要注意的是 ACK Range Count 的单位是“ACK Range 的数量”,不是“所有包的数量”。很多解析器第一次写出来,会把 ACK Range Count 当成要读的总包数,结果读着读着就把包解析错位了。记住:首区间单独一个 First ACK Range,后面的区间再由 ACK Range 数组表示。

ECN 计数只在帧类型为 0x03 时出现,包含 ECT0 Count、ECT1 Count 和 ECN-CE Count 三个变长整数。网络设备如果支持显式拥塞通知,可以通过这些计数告诉发送端真正发生了多少拥塞事件,这是 QUIC 比 TCP 更容易部署 ECN 的原因之一。

3.3 STREAM 帧:类型字节低 4 位就是一个小型位图

STREAM 帧是所有帧里最常用的,它负责把一个流的数据块装进去。它的类型范围是 0x08 到 0x0f,低 4 位里每一位都表示一个可选字段是否存在:

  • Bit 3(0x08):表示是否有 Offset 字段
  • Bit 2(0x04):表示是否有 Length 字段
  • Bit 1(0x02):表示 Offset 字段的编码长度是否为 8 字节
  • Bit 0(0x01):表示是否最后一帧

等等,这里我用了位的位置描述,但很多文档里喜欢写成 OFF、LEN、FIN 三个标志位:OFF 位(0x04)、LEN 位(0x02)、FIN 位(0x01)。类型字节等于 0x08 加上这三个标志。比如0x0f就是 OFF+LEN+FIN 三位置 1,0x0c是 OFF+LEN 置 1 但没有 FIN。

该帧中,Stream ID 是变长整数,标识属于哪条流。Offset 是变长整数,表示这段数据的起始字节位置;如果这个位没有设置,默认偏移为 0。Length 是变长整数,表示数据部分的字节数;如果 LEN 位没设置,Length 会根据包尾自动推导。FIN 位为 1 表示这个 frame 的末尾就是整条流的终止点。

这里有个容易混淆的地方:Offset 和 Length 在 STREAM 帧里即使存在,也不是“固定占用 4 字节”或“固定占用 8 字节”,而是用变长整数编码的。所以解析时要先解析变长整数的长度前缀,再读取后续字节。我在实现时写过一个通用的read_varint()函数,只要那个函数稳了,STREAM 帧的解析就成功了一半。

STREAM 帧的 Offset 还有一个特殊用途:配合 HTTP/3 的请求响应。HTTP/3 的流模型建立在 STREAM 帧之上,一个服务器推送的响应数据可能被拆成多个 STREAM 帧,分别带不同的 Offset,接收端要根据 Offset 去重和排序。你如果在抓包工具里看到请求数据被拆得七零八落,不要惊讶,这是 QUIC 流复用在底层干的活。

3.4 CRYPTO 帧:TLS 握手数据背后的搬运工

CRYPTO 帧和 STREAM 帧长得非常像,但它是给 TLS 握手数据用的,不归属于应用流。格式是:

CRYPTO Frame { Type (i) = 0x06, Offset (i), Length (i), Crypto Data (..), }

这里没有 Stream ID,因为 CRYPTO 帧只属于连接本身,不分流。Offset 表示这段数据在加密握手消息中的位置。TLS 握手消息通常比一个 QUIC 包大,所以一条握手消息会被切分到多个 CRYPTO 帧里,接收端按 Offset 重新拼装。CRYPTO 帧有重传机制,但重传时使用的是新的包号,接收端通过 Offset 来去重,这一点和 TCP 按序号去重的思路一致。

CRYPTO 帧不能在 0-RTT 包里发送,因为 0-RTT 还没有完成握手,数据可能被重放攻击利用。为了防止攻击者把 0-RTT 包里的数据篡改成握手数据,CRYPTO 帧只允许出现在 Initial、Handshake 和 1-RTT 包里。Implementation 里如果发现 0-RTT 包带有 CRYPTO 帧,应该直接丢弃或视为协议错误。

3.5 连接关闭帧:错误信息和帧类型也能拿来排错

CONNECTION_CLOSE 帧有两种:0x1c 是应用层主动关闭,0x1d 是传输层错误关闭。前者的字段不包含 Frame Type,后者必须包含一个触发错误的帧类型,帮助接收方定位是哪个帧出了问题。格式:

CONNECTION_CLOSE Frame { Type (i) = 0x1c..0x1d, Error Code (i), [Frame Type (i)], Reason Phrase Length (i), Reason Phrase (..), }

这里最实用的点在于 Frame Type 字段。如果服务端在解析一个 STREAM 帧时出错,比如 Stream ID 对应的流早就关闭了,CONNECTION_CLOSE 里的 Frame Type 会写明是 0x08 到 0x0f 里的哪个类型。我调试协议栈时碰到过一种情况:客户端发来的 STREAM 帧的 Offset 不递增,服务端直接回了一个携带 Frame Type 的传输关闭帧。抓包工具里,这个 Frame Type 可以帮你迅速定位是加解密的问题,还是应用逻辑对流状态机管理出了问题。

另外注意,应用层关闭帧 Error Code 使用的是应用自定义错误空间,传输层关闭帧使用的是传输错误码。HTTP/3 里错误码和 QUIC 传输错误码完全不同,排查时千万别混着看。

4. 实操解析过程与常见问题排查

4.1 手工拆一个 Initial 包:从十六进制到字段

纸上谈兵再多,不如动手拆一个包。假设我在局域网里抓到了一个 QUIC Initial 包,十六进制开头是:

c0 00 00 00 01 08 08 01 02 ...

注意我故意只给了前几字节,方便演示头部解析。

第一个字节是0xc0,二进制是11000000。最高位 1,说明是长头;低 2 位是 0,说明是 Initial 包。Fixed Bit 为 1,没问题。

接下来 4 字节00 00 00 01是版本号,表示 QUIC v1。

然后08是目标连接 ID 长度,表示接下来要读 8 字节目标连接 ID。但这里我给的数据不完整,为了演示假设后续 8 字节就是目标连接 ID。紧接着08是源连接 ID 长度,又是 8 字节源连接 ID。

读到这里,你可能会发现目标连接 ID 和源连接 ID 顺序反复出现。这里确实是一个新手极易犯的错:长头里的顺序是先目标连接 ID 再源连接 ID,而 Initial 包里生成源连接 ID 的规则,又是根据服务端返回的源连接 ID 反过来填目标连接 ID。一定要按字节顺序严格读,不要凭印象跳字段。

Initial 包接下来是 Token Length。假设 Token Length 是变长整数0x00,表示没有 Token。然后才是 Length、Packet Number、加密负载。Length 是整个 Type-Specific Payload 的字节数,在解包时不能混淆为负载长度,因为 Length 之后还有 Packet Number 字段。用 Wireshark 跟着读一遍,很快就理解这个顺序了。

这里我强烈建议你在本地用 tcpdump 抓一个真实 QUIC 握手包,然后用 Wireshark 的“解码为 UDP 端口号”功能把负载标记为 QUIC,对照着上面的字段结构看。第一次看时,你会惊讶于 Wireshark 能把每个变长整数的长度前缀都给你标出来,能直观地验证我上面讲的所有规则。

4.2 调试中遇到最多的四个格式问题

第一,变长整数的最小长度不满足。这个问题好发在自定义实现里,比如某些库会把0x00编码成0x4000,也就是用 2 字节表示 0 值。按照 RFC 要求,编码器必须用最小字节数表示,对端收到后直接判错。解决方法很粗暴:在编码函数里写一个分支,值在 0 到 63 之间用 1 字节,64 到 16383 之间用 2 字节,依此类推,不要图省事统一用 4 字节。

第二,读取 STREAM 帧时,把 Offset 长度和 Length 长度的解析顺序搞反。有的实现会先把整个 STREAM 帧读成“数据块”,但没意识到 Offset 和 Length 也是变长整数,在判断长度时出错。最简单的方法是把 STREAM 帧的解析拆成三步:先读 Stream ID,再按 flag 读 Offset,再按 flag 读 Length,最后才读数据。每一步都用同一个read_varint(),不要混。

第三,ACK Range Count 和 First ACK Range 的单位不一致。ACK Range Count 表示的是 ACK Range 数组的元素数量,不是包数量。经常有实现把 ACK Range Count 加 1 当成总确认区间数,导致多读或少读一段。正确逻辑是先读 Largest Acknowledged,再读 First ACK Range,最后用循环读取 ACK Range Count 组 Gap 和 ACK Range。

第四,短包里没有目标连接 ID 长度,容易在解析时少读或多读。前面说了,短头的目标连接 ID 长度是从握手阶段长头里继承来的。如果你只孤零零地看一个 1-RTT 包,不跟踪连接状态,就没法解析。这也提醒我们,实现 QUIC 解析器时,必须维护一个“连接上下文”,而不是简单地把每个 UDP 包当成独立对象。

4.3 抓包和解析工具的选择心得

成熟的 Wireshark 已经能很好地解析 QUIC,但如果你要调试自己的实现,建议抓包时把 QUIC 的密钥日志也一起导出,这样才能看到明文帧。Chrome、ngtcp2、quiche 等主流实现都支持通过环境变量或者 API 输出 TLS key log,格式是CLIENT_HANDSHAKE_TRAFFIC_SECRET这类。Wireshark 里配置好 key log 文件后,就能把每个帧展开成年纪:先是头部字段,然后是帧列表,再到每个帧的具体字段。

如果你是在自己写的解析器里面调试,想快速打印帧类型分布,可以在拿到明文帧之后写一个小工具,按我上面那一节讲的方法遍历帧。我通常先统计帧类型出现的次数,再单独打印 STREAM 帧的 Stream ID、Offset、Length 三元组,用来判断有没有流乱序或数据丢失。这种方法比直接对着十六进制肉眼看高效得多。

需要注意,QUIC 包里的数据是有加密嵌套的:短头包的 Packet Number 部分是明文,但 Packet Payload 是整体加密的。抓包工具能显示 STREAM 帧,是因为它拿密钥解密了 Payload;如果你拿到的抓包文件里只有一堆UnknownEncrypted,那基本是没导入 key log,或者 key log 对应的连接和你抓的目标不是同一条。

4.4 试过就回不去的自查清单

最后给你一份我做格式解析时的自查清单:

  • 长头首字节高 2 位是否为11,长包类型是否在合法范围内。
  • 版本号是否为 4 字节,Initial/Handshake/0-RTT/Retry 是否和自己的预期一致。
  • 连接 ID 长度字段加起来,是否和包实际剩余长度匹配。
  • Token Length 在 Initial 包里是否存在,以及是否影响后续 Length 字段偏移。
  • 短头首字节高 2 位是否为10,Spin Bit、Reserved Bits、Key Phase 的位置是否对齐。
  • 包号长度是否为 1、2、3、4 字节之一,包号还原时是否超出本地窗口。
  • STREAM 帧的类型字节低 4 位标志,是否与你期望的 Offset/Length/FIN 状态一致。
  • ACK 帧的 Range 遍历顺序是否为“先 First,再循环 Gap+ACK Range”。
  • 所有变长整数是否使用了最小字节数,尤其是为 0 的字段。

这条清单不是一次性写完的。我在第二遍校对协议栈时才补上了其中几条,大都是被真实抓包数据教做人的结果。你照着清单过一遍,至少能避开绝大多数格式对齐问题。

QUIC 的数据格式看起来琐碎,但哲学上是“所有信息都可被压缩、所有状态都可被推导”。你自己写一遍解析,再对着 Wireshark 验证一遍,就会发现长头、短头、变长整数、帧组合这些设计,没有一处是多余的。我个人的体会是,真正卡住你的往往不是某个字段的值是什么,而是“下一段该从哪一字节开始”。把每个字段的长度边界都理清楚,后续加密、丢包重传、流控这些实现就会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询