必须用 htons/ntohs 处理 16 位网络字段,因协议要求大端序而 x86/x64 主机为小端序;直接 memcpy 会导致端口等字段解释错误,且需配对使用 htonl/ntohl 处理 32 位字段,结构体须逐字段转换而非整体 memcpy。

为什么必须用 htons / ntohs 处理 16 位整数
网络协议(如 IP、TCP、UDP 头)规定所有多字节整数字段必须以大端序(Big-Endian)传输,而 x86/x64 主机是小端序(Little-Endian)。直接 memcpy 会导致端口号、IP 分片偏移等字段被解释错误——比如你 bind( ) 一个端口 8080,在小端主机上内存布局是 0x3020(低字节在前),若不转换就发出去,对端收到的会是 8208(0x2030 解释为大端)。
所以必须用标准函数做显式转换:
-
htons():host to network short,把主机字节序的uint16_t转成网络字节序(大端) -
ntohs():network to host short,把从网络收到的uint16_t转回主机字节序
这两个函数在 <arpa/inet.h>(Linux/macOS)或 <>winsock2.h>(Windows)中声明,且对大小端一致的平台(如某些 ARM 模式)是空操作,安全可移植。
32 位整数要用 htonl / ntohl,别混用
IP 地址(in_addr.s_addr)、IPv4 数据报总长度、序列号等是 32 位字段,必须用对应的长整型转换函数:
立即学习“C++免费学习笔记(深入)”;
-
htonl()把uint32_t从主机序转网络序 -
ntohl()把收到的uint32_t从网络序转主机序 - 不能用
htons()处理 32 位值——它只取低 16 位,高位直接丢弃,结果完全错误
示例:
struct sockaddr_in addr; addr.sin_port = htons(8080); // ✅ 正确:端口是 uint16_t addr.sin_addr.s_addr = htonl(INADDR_ANY); // ✅ 正确:INADDR_ANY 是 uint32_t // addr.sin_port = htonl(8080); // ❌ 编译可能过,但运行时端口变成 0x00001f90 的低 16 位即 0x1f90 → 8080?不对,htonl 返回的是 0x00001f90,赋给 uint16_t 会截断成 0x1f90 → 实际绑定端口 8080?等等——错!8080 的十六进制是 0x1F90,htonl(0x00001F90) = 0x901F0000,再截断为 uint16_t 就是 0x0000 → 绑定端口 0
结构体整体 memcpy 前必须逐字段转换
像 struct iphdr 或自定义协议头这类结构体,不能直接 memcpy 到发送缓冲区——因为编译器可能加填充字节(padding),且字段字节序不统一(有的 16 位、有的 32 位、有的 8 位)。
正确做法是手动对每个需转换的字段调用对应函数:
- 版本/IHL/服务类型/TTL 等 8 位字段:无需转换(
uint8_t单字节无序) - 总长度、标识、分片偏移、校验和等 16 位字段:用
htons() - 源/目的 IP 地址:用
htonl() - 结构体本身不要 memset(0) 后直接填原始值再发——填之前就得转
常见坑:有人把整个结构体 cast 成 uint16_t* 循环调用 htons,这会误转 padding 字节和 8 位字段,还可能因对齐导致越界读写。
现代 C++ 中避免裸函数调用的替代思路
如果你用 C++17 及以上,且不依赖系统 socket API 直接组包(比如用 std::byte + std::bit_cast 构造协议头),可以封装字节序转换逻辑,提升类型安全:
- 用
constexpr模板判断当前平台字节序(std::endian::native == std::endian::big) - 对
uint16_t/uint32_t写泛型翻转函数,比宏更易测试 - 但注意:只要调用系统 socket 接口(
sendto,recvfrom),最终发出去的数据仍必须符合 RFC 规定的网络字节序,底层不会替你转
也就是说,无论你用什么高级封装,最终塞进 sockaddr_in.sin_port 的值,还是得是经过 htons() 处理过的——这是 ABI 层约定,绕不开。


















