1.字节序和IP地址转换
字节序
1. 字节序是什么?(通俗理解)
字节序(Endianness)指的是多字节数据在内存中的存储顺序。
计算机存储数据时,单个字节(如 0x12)没有顺序问题,但多字节数据(如 16 位端口号 0x1234、32 位 IP 地址 0x12345678)会有两种存储方式:
- 把高位字节存在低内存地址(大端)
- 把低位字节存在低内存地址(小端)
1. 大端序(Big-Endian):“高位在前”(符合人类习惯)
规则:高位字节存在低地址内存格,低位字节存在高地址内存格;
比喻:像人类写数字
1234一样,先写高位的12,再写低位的34;例子:存储
0x1234(十进制 4660)内存地址 存储内容 说明 100 0x12 低地址存高位字节 101 0x34 高地址存低位字节
2. 小端序(Little-Endian):“低位在前”(计算机更常用)
规则:低位字节存在低地址内存格,高位字节存在高地址内存格;
比喻:把数字倒着写,先写低位的
34,再写高位的12;例子:存储
0x1234(十进制 4660)内存地址 存储内容 说明 100 0x34 低地址存低位字节 101 0x12 高地址存高位字节
补充:x86/x86_64 架构的 CPU(绝大多数电脑、服务器)都是小端序,而网络传输协议规定必须用大端序(也叫 “网络字节序”)。
2. 字节序的分类
(1)主机字节序(Host Byte Order)
- 定义:当前机器 CPU 的原生字节序(大部分是小端,少数嵌入式 CPU 是大端);
- 特点:程序中直接定义的变量(如
int port = 8080)默认是主机字节序。
(2)网络字节序(Network Byte Order)
- 定义:TCP/IP 协议规定的统一字节序(固定为大端);
- 原因:不同机器的主机字节序可能不同(比如 A 机小端、B 机大端),如果直接传输,对方会解析出错误数据,因此必须统一为大端序。
3. 为什么网络编程必须转换字节序?
以端口号为例:
你在小端机器上定义 int port = 8080(十进制 8080 = 十六进制 0x1F90),内存中存储为 0x90 0x1F(小端);
如果直接传给网络,接收方按大端解析会得到 0x901F(十进制 36895),完全错误。
因此必须把主机字节序的端口 / IP,转换成网络字节序再传输;接收方拿到数据后,再转回主机字节序。
4. 字节序转换函数(核心工具)
系统提供了专门的函数处理字节序转换,无需手动拆分字节,这些函数在 <netinet/in.h> 中声明:
| 函数名 | 作用 | 适用场景 |
|---|---|---|
htons() |
Host to Network Short(16 位) | 转换端口号(16 位) |
htonl() |
Host to Network Long(32 位) | 转换 IPv4 地址(32 位) |
ntohs() |
Network to Host Short(16 位) | 解析网络传来的端口号 |
ntohl() |
Network to Host Long(32 位) | 解析网络传来的 IPv4 地址 |
注意:IPv6 地址是 128 位,无需这些函数转换(
sockaddr_in6的sin6_addr直接存储字节数组,由inet_pton()/inet_ntop()处理)。
5. 代码示例:字节序转换的实际应用
1 |
|
运行结果(小端机器):
1 | 主机字节序端口:8080 → 网络字节序:38000 |
6. 关键注意事项
- 这些转换函数是跨平台的:如果机器本身是大端,函数会直接返回原值(无需转换);
- 只转换多字节数据:单个字节(如
char)无需转换; - IPv6 无此类函数:
sockaddr_in6的sin6_port仍用htons()(16 位),但sin6_addr是 16 字节数组,由inet_pton()直接处理为网络字节序。
总结
- 字节序是多字节数据的内存存储顺序,分为主机字节序(机器原生,多为小端)和网络字节序(协议规定,固定大端);
- 网络编程中,端口(16 位)、IPv4 地址(32 位)必须用
htons()/htonl()转网络字节序,接收时用ntohs()/ntohl()转回; - 转换函数跨平台,无需关心机器本身是大端还是小端,直接调用即可保证数据传输正确。
IP地址转换
虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:
1 | // 主机字节序的IP地址转换为网络字节序 |
参数:
- af: 地址族(IP地址的家族包括ipv4和ipv6)协议
- AF_INET: ipv4格式的ip地址
- AF_INET6: ipv6格式的ip地址
- src: 传入参数, 对应要转换的点分十进制的ip地址: 192.168.1.100
- dst: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中
返回值:成功返回1,无效IP返回0,失败返回-1
1 |
|
参数:
- af: 地址族协议
- AF_INET: ipv4格式的ip地址
- AF_INET6: ipv6格式的ip地址
- src: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址
- dst: 传出参数, 存储转换得到的小端的点分十进制的IP地址
- size: 修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节
返回值:
- 成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串
- 失败: NULL
示例 1:客户端指定服务器 IP(inet_pton())
1 | // 配置服务器地址 |
示例 2:服务端解析客户端 IP(inet_ntop())
1 | // accept接收客户端连接后,解析客户端IP和端口 |
INET_ADDRSTRLEN这个宏的含义和用法,它是系统定义的常量宏,核心作用是「指定存储 IPv4 字符串格式地址的缓冲区最小长度」,是编写 IP 地址转换代码时的关键常量。
为什么是 16?
IPv4 地址的最大长度是 xxx.xxx.xxx.xxx(比如 255.255.255.255):
- 4 组数字(每组最多 3 位) + 3 个点 = 3×4 + 3 = 15 个字符;
- 额外加 1 个字节存储字符串结束符
\0,总计 16 字节。
在使用 inet_ntop() 转换「二进制 IP → 字符串 IP」时,需要提前分配字符串缓冲区,INET_ADDRSTRLEN 能保证缓冲区大小足够,避免缓冲区溢出(新手最容易踩的坑)。
反例(错误):缓冲区太小导致溢出
1 | char ip_buf[15]; // 仅15字节,少了结束符的位置 |
正例(正确):用 INET_ADDRSTRLEN 定义缓冲区
1 | char ip_buf[INET_ADDRSTRLEN]; // 系统保证足够大(16字节) |
三、对比:IPv6 对应的常量
如果处理 IPv6 地址,对应的常量是 INET6_ADDRSTRLEN(值为 46),用法完全一致:
1 | // 存储IPv6字符串的缓冲区 |
还有一组函数也能进程IP地址大小端的转换,但是只能处理ipv4的ip地址:
1 | // 点分十进制IP -> 大端整形 |
关键避坑要点
字节序问题:
- IP 转换函数(
inet_pton()/inet_addr())返回的二进制 IP已经是网络序,无需再调用htonl(); - 只有手动构造的 32 位整数 IP(如
0x7F000001)才需要用htonl()转网络序。
- IP 转换函数(
inet_ntoa()线程不安全:- 它返回的是静态缓冲区,多线程下会被覆盖,优先用
inet_ntop()。
- 它返回的是静态缓冲区,多线程下会被覆盖,优先用
非法 IP 处理:
inet_addr()无法区分255.255.255.255和非法 IP(都返回-1),推荐用inet_pton()。
缓冲区大小:
inet_ntop()的dst缓冲区大小至少为INET_ADDRSTRLEN(IPv4)/INET6_ADDRSTRLEN(IPv6),避免缓冲区溢出。