40. 网络字节序转换:htonl/htons/ntohl/ntohs、inet_pton/inet_ntop

网络编程里有个坑,十个人有八个刚入坑时都踩过——字节序问题。说白了,就是你的电脑和别人的电脑,对同一个数字的“读写顺序”可能不一样。

我记得刚工作那会儿,写了个简单的聊天程序,本地测试一切正常。结果一部署到服务器上,端口号死活对不上。查了半天,发现是字节序没转。嗯,从那以后,我再也不敢忽略这几个函数了。

40.1 为什么需要字节序转换?

计算机存储多字节数据时,有两种主流方式:

  • 大端序(Big-Endian):高位字节存低地址,低位字节存高地址。网络协议统一用这个。
  • 小端序(Little-Endian):低位字节存低地址,高位字节存高地址。x86、ARM 默认用这个。

你想想看,如果两台机器直接拿内存里的二进制流通信,一个读成 0x1234,另一个读成 0x3412,那不乱套了?所以网络协议规定:所有多字节数据,必须用大端序传输。

核心原则:发送前转成网络字节序(大端),接收后转回主机字节序(本机格式)。

40.2 四个基础转换函数

C 标准库提供了四个函数,名字很直观:

函数 含义 典型用途
htonl() Host to Network Long(32位) 转换 IPv4 地址
htons() Host to Network Short(16位) 转换端口号
ntohl() Network to Host Long(32位) 接收 IPv4 地址
ntohs() Network to Host Short(16位) 接收端口号

个人习惯:我写代码时,只要涉及 sockaddr_in 结构体里的 sin_portsin_addr,一律用 htonshtonl 包一下。哪怕本机就是大端序,也写上——这样代码可读性更好,别人一看就知道“这里做了字节序转换”。

代码示例:绑定端口

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);          // 端口转网络字节序
    addr.sin_addr.s_addr = htonl(INADDR_ANY); // 地址转网络字节序

    printf("端口 8080 的网络字节序: %d\n", htons(8080));
    printf("INADDR_ANY 的网络字节序: %u\n", htonl(INADDR_ANY));
    return 0;
}

输出结果取决于你的机器。如果是小端序机器,你会发现 htons(8080) 的值和 8080 不一样。别慌,这是正常的。

40.3 字符串与二进制地址转换

早期网络编程里,人们喜欢用 inet_addr()inet_ntoa()。但这两个函数有缺陷——不支持 IPv6,而且 inet_ntoa() 返回的是静态缓冲区,多线程下会出问题。

我个人建议:新代码一律用 inet_pton()inet_ntop()。它们支持 IPv4 和 IPv6,而且线程安全。

函数 作用 参数
inet_pton() 字符串 → 二进制(网络字节序) AF_INET/AF_INET6, 字符串, 结果缓冲区
inet_ntop() 二进制 → 字符串 AF_INET/AF_INET6, 二进制地址, 输出缓冲区, 长度

代码示例:IPv4 地址转换

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct in_addr addr;
    char str[INET_ADDRSTRLEN];

    // 字符串 → 二进制
    if (inet_pton(AF_INET, "192.168.1.1", &addr) != 1) {
        perror("inet_pton 失败");
        return 1;
    }
    printf("二进制地址: 0x%x\n", addr.s_addr);

    // 二进制 → 字符串
    if (inet_ntop(AF_INET, &addr, str, sizeof(str)) == NULL) {
        perror("inet_ntop 失败");
        return 1;
    }
    printf("转回字符串: %s\n", str);

    return 0;
}

我曾经踩过的坑:用 inet_ntop() 时,缓冲区大小一定要用 INET_ADDRSTRLEN(IPv4)或 INET6_ADDRSTRLEN(IPv6)。我见过有人随手写了个 char buf[16],结果 IPv6 地址一长串直接截断了,排查了半天才发现是缓冲区不够。

40.4 知识体系图

下面这张图帮你理清整个流程:

主机A(小端序) 端口: 8080 地址: 192.168.1.1 主机字节序 htonl/htons 网络 大端序 网络字节序 ntohl/ntohs 主机B(大端序) 端口: 8080 地址: 192.168.1.1 主机字节序 字符串 ↔ 二进制转换 "192.168.1.1" inet_pton 0xC0A80101 二进制(网络序) inet_ntop "192.168.1.1"

40.5 实战中的注意事项

  • 不要假设本机字节序:哪怕你只在 x86 上开发,也请老老实实调用转换函数。代码的可移植性就是这么一点一点积累出来的。
  • 检查返回值inet_pton() 返回 1 表示成功,0 表示字符串格式无效,-1 表示地址族不支持。我见过有人只检查返回值是否为 1,忽略了格式错误的情况。
  • 端口号范围htons() 的参数是 16 位无符号整数,范围 0~65535。如果你传了个大于 65535 的值,编译器会截断,但不会报错。建议加个范围检查。

一个小技巧:调试时可以用 ntohs() 把收到的端口号转回主机序再打印,这样看着更直观。我经常在日志里同时打印原始值和转换后的值,方便对比。

40.6 总结

字节序转换是网络编程的基本功。说白了,就是记住一句话:发前转网络序,收后转主机序。字符串和二进制地址的转换,用 inet_pton/inet_ntop 准没错。

嗯,这几个函数虽然简单,但用错了后果很严重。我建议你写个测试程序,把本机字节序、网络字节序、字符串转换都跑一遍,亲眼看看数据是怎么变的。实践出真知嘛。


公众号:蓝海资料掘金营,微信 deep3321