字节序:主机如何存整数,网络如何传字段
核心结论:网络上传输的是有固定顺序的字节串,不是某台机器内存里的
int或 C++ 结构体。互联网协议把多字节整数统一编码为大端序(network byte order);主机端的 ISA/ABI 可以采用不同端序,但必须在协议边界完成一次明确的转换。
1. 问题从哪里出现
内存是按字节寻址的:每个地址对应一个 8-bit 字节。一个 16/32/64 位整数会占连续的多个字节,但“数值的高位字节与低位字节分别放在哪个地址”并不是由数学数值决定的,这就是字节序(byte order / endianness)。
设一个 k 字节无符号整数写成
x = d[0] + d[1] * 256 + ... + d[k-1] * 256^(k-1)其中 d[0] 是最低有效字节(least-significant byte,LSB),d[k-1] 是最高有效字节(MSB)。若对象起始地址为 a:
| 内存表示 | 低地址 a 放什么 |
|---|---|
| 小端序(little-endian) | d[0],即最低有效字节 |
| 大端序(big-endian) | d[k-1],即最高有效字节 |
例如 1024 = 0x0400 占两个字节时:
地址 a a + 1小端内存 0x00 0x04大端内存 0x04 0x00单字节对象没有端序差异。端序也不会改变整数的数学值;它只规定从低地址到高地址观察时,对象字节如何排列。
不要混淆三件事:
0x0400:调试器以十六进制打印的一个数值;"0x0400":六个字符组成的文本;04 00或00 04:真正的内存/报文字节。
2. 主机序由架构规定,协议序由协议规定
多字节标量的普通 load/store 如何解释内存字节,是 ISA/处理器架构约定的一部分;它会进一步影响 ABI 中对象表示的细节。x86/x86-64 的常规主机序是小端。ARM 架构支持端序配置,现代常见系统通常也使用小端;因此“x86 小端、ARM 大端”只是过度简化的教学说法。
不过,寄存器里的 0x12345678 本身没有“端序”:端序只在它被拆成字节写入内存、或从内存装载为多字节值时出现。
网络不能假设通信双方的 CPU、编译器或 ABI 相同。因此协议必须独立规定 wire format(线上表示):
wire format = (字段偏移, 宽度, 字节序, 语义)互联网协议对多字节整数采用网络字节序(network byte order),即大端序。对一个 16 位端口号 80 = 0x0050,线上两个字节必须是:
低偏移 → 高偏移: 00 50无论发送端与接收端分别使用小端还是大端主机,这个字节串都不变。端序只统一了多字节字段的排列;它不自动解决字段宽度、符号、浮点格式、位域布局或结构体填充问题。
3. hton* / ntoh*:跨越主机域与网络域
POSIX 中通常包含 <arpa/inet.h>,可使用如下接口(Windows 的 Winsock 也提供同名接口):
uint16_t htons(uint16_t hostshort); // host to network shortuint16_t ntohs(uint16_t netshort); // network to host shortuint32_t htonl(uint32_t hostlong); // host to network longuint32_t ntohl(uint32_t netlong); // network to host long| 函数 | 输入域 → 输出域 | 宽度 | 典型字段 |
|---|---|---|---|
htons | 主机序 → 网络序 | 16 bit | port、length |
ntohs | 网络序 → 主机序 | 16 bit | 收到的 port、length |
htonl | 主机序 → 网络序 | 32 bit | IPv4 地址、sequence number |
ntohl | 网络序 → 主机序 | 32 bit | 收到的 32 位字段 |
名称中的 h 是 host,n 是 network,s 是 16-bit short,l 是历史上表示 32-bit 的 long。最后一点是接口命名,不应把它等同于 C/C++ 里宽度因平台而异的 long。
在小端机器上,这些函数通常会交换字节;在大端机器上通常保持数值不变。调用者不该依赖这项实现细节,而应始终按字段当前所处的表示域调用正确转换。
// header.port 是从网络缓冲区得到的 16-bit 网络序字段const uint16_t port_host = ntohs(header.port);if (port_host == 80) { // 使用已经解码的主机序数值}
// 要发送的主机序端口号写回网络字段header.port = htons(port_host);最重要的边界不变量是:每个字段跨越 host/network 边界恰好转换一次。
- 发送:主机序数值 →
hton*或显式大端编码 → 字节缓冲区; - 接收:字节缓冲区 →
ntoh*或显式大端解码 → 主机序数值; - 内部业务逻辑尽量只保存、比较和计算主机序数值。
因此,不要直接将网络序字段与主机序常量比较:
// 错:在小端机上,网络字节 00 50 被当作主机 uint16_t 时会是 0x5000。if (header.port == 80) { /* ... */ }
// 对:先把它解释成主机序数值。if (ntohs(header.port) == 80) { /* ... */ }在小端机上,漏掉转换会把端口 00 50(80)错误读成 0x5000(20480),这是“数值突然变成 256 倍”的典型症状。重复转换也同样错误:在小端机上它往往又把正确数值交换回去。
4. 不能直接把报文当作本机结构体
下面的写法看似方便,但不是健壮的协议解析方式:
auto *ip = reinterpret_cast<const IPv4Header *>(buffer.data());即使你记得对 ip->total_length 调用 ntohs,仍可能有几个独立问题:
buffer的地址不保证满足IPv4Header的对齐要求;某些平台会因此异常,x86 上“碰巧可用”不等于可移植;reinterpret_cast后的对象生命周期与严格别名规则未必允许这样访问;- 编译器可在结构体成员之间插入 padding;位域布局也由实现定义;
- 网络输入可能比首部还短,在读取任何成员前都可能越界;
- 线上报文布局是协议规范,不是本机 ABI 的结构体布局。
更容易审计的解析方式是:先证明缓冲区足够长,再按偏移显式组合字节。例如读取 16 位大端字段:
bool read_u16be(const uint8_t *buf, size_t len, uint16_t &out) { if (len < 2) return false; out = static_cast<uint16_t>( (static_cast<uint16_t>(buf[0]) << 8) | buf[1]); return true;}其语义就是:read16be(p) = (p[0] << 8) | p[1]。读取 32 位字段时同理:
uint32_t read_u32be(const uint8_t *p) { return (static_cast<uint32_t>(p[0]) << 24) | (static_cast<uint32_t>(p[1]) << 16) | (static_cast<uint32_t>(p[2]) << 8) | static_cast<uint32_t>(p[3]);}这里先转换为无符号的固定宽度类型再移位,避免 char 的符号扩展和整数提升引入意外。写出时反向取高位字节即可。固定偏移的这种写法通常也能被编译器优化为高效的加载与字节交换;首先要守住正确性与可审计性。
5. 端序检查不替代边界检查
RFC 往往以每行 32 bit 的位图画首部。以 IPv4 为例,固定部分 5 行,因此最小首部是 5 × 32 bit = 20 bytes;其中 Total Length 占 16 bit,网络序为大端。
若 Total Length 是 1230 = 0x04CE,线上字节为 04 CE。但即便正确解出 1230,也不能马上读取这么多字节:该字段来自不可信输入。若捕获的实际长度为 Lcap,解析 IPv4 首部至少要证明:
20 <= Lheader <= Lip <= Lcap其中 Lheader = 4 * IHL,而 IHL 的单位是 32-bit word,不是字节。端序转换只解决“怎样把字节组成数值”,长度检查才解决“能否安全读取这些字节”。
6. 调试与测试方法
判断当前机器的对象表示
使用具有不同字节的值,如 0x01020304。安全地以字节视图查看它:低地址若是 04,当前对象表示为小端;若是 01,则为大端。不要选择 0x00000000、0xffffffff 或每个字节相同的值,它们不能区分端序。
三点定位法
对同一个字段记录:
发送前主机序数值 → 发送缓冲区原始字节 → 接收后的主机序数值- 第一段不一致:编码边界错误;
- 原始字节在线路/抓包中不一致:检查缓冲区构造、发送路径或抓包位置;
- 第二段不一致:解码边界错误。
日志应同时打印字段偏移、原始字节和解码后的主机序值。只打印十进制数,往往分不清“线上字节已经错了”还是“读法错了”。
应测试的性质
对合法字段,编码器与解析器应满足:
decode(encode(x)) = x测试除 0 和全 1 外,还应覆盖 1、255、256、最大值,以及 0x01020304 这种非对称字节模式。还要有黄金字节向量,例如:端口 443 必须编码为 01 BB,长度 1500 必须编码为 05 DC。只做往返测试不足以发现“编码器和解析器恰好以同一种错误端序配对”的问题。
总结
- 字节序描述多字节对象在内存低地址到高地址的排列;主机序属于体系结构/ABI 的表示约定。
- 网络协议独立规定线上表示;互联网的多字节整数使用大端网络字节序。
htons/htonl用于发送前的 host → network,ntohs/ntohl用于接收后的 network → host。- 转换必须在边界恰好一次;不要直接比较网络序字段与主机序常量。
- 端序正确并不保证解析安全:仍要按协议偏移读取、避免把字节缓冲区直接覆盖为结构体,并先做完整的长度检查。
课程材料依据
- AIGC
1-8 byte order,第 2–4 节(PDF 第 2–10 页,视频 00:00<00>00>–00:06<45>45>):字节序定义、网络字节序与htons/ntohs/htonl/ntohl的边界转换。 - AIGC
1-8 byte order,第 5、8–9 节(PDF 第 11–17 页,视频 00:07<00>00>–00:09<47>47>):RFC 位图、按偏移读取、长度不变量与常见端序错误。 - 官方 Winter 2024 材料未直接讲授端序;AIGC 讲义明确将端序、大端网络序及上述转换函数标为视频直接证据。官方材料只对“将报文视为字节串并先检查边界”的解析思路提供间接支持。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






