字节序

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_in6sin6_addr 直接存储字节数组,由 inet_pton()/inet_ntop() 处理)。

5. 代码示例:字节序转换的实际应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <netinet/in.h>

int main() {
// 1. 端口号转换(16位)
uint16_t host_port = 8080; // 主机字节序(小端机器:0x90 0x1F)
uint16_t net_port = htons(host_port); // 转网络字节序(大端:0x1F 0x90)
printf("主机字节序端口:%d → 网络字节序:%u\n", host_port, net_port);

// 2. IPv4地址转换(32位)
uint32_t host_ip = 0x0100007F; // 主机字节序(小端:对应127.0.0.1)
uint32_t net_ip = htonl(host_ip); // 转网络字节序(大端)
printf("主机字节序IP(0x%X) → 网络字节序(0x%X)\n", host_ip, net_ip);

// 3. 反向转换(接收数据时)
uint16_t recv_port = ntohs(net_port); // 网络字节序转回主机字节序
printf("网络字节序端口转回主机:%u\n", recv_port);

return 0;
}

运行结果(小端机器):

1
2
3
主机字节序端口:8080 → 网络字节序:38000
主机字节序IP(0x100007F) → 网络字节序(0x7F000001)
网络字节序端口转回主机:8080

6. 关键注意事项

  • 这些转换函数是跨平台的:如果机器本身是大端,函数会直接返回原值(无需转换);
  • 只转换多字节数据:单个字节(如 char)无需转换;
  • IPv6 无此类函数:sockaddr_in6sin6_port 仍用 htons()(16 位),但 sin6_addr 是 16 字节数组,由 inet_pton() 直接处理为网络字节序。

总结

  1. 字节序是多字节数据的内存存储顺序,分为主机字节序(机器原生,多为小端)和网络字节序(协议规定,固定大端);
  2. 网络编程中,端口(16 位)、IPv4 地址(32 位)必须用 htons()/htonl() 转网络字节序,接收时用 ntohs()/ntohl() 转回;
  3. 转换函数跨平台,无需关心机器本身是大端还是小端,直接调用即可保证数据传输正确。

IP地址转换

虽然IP地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的IP地址进行大小端转换:

1
2
3
// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
int inet_pton(int af, const char *src, void *dst);

参数:

  • 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
2
3
#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数:

  • af: 地址族协议
    • AF_INET: ipv4格式的ip地址
    • AF_INET6: ipv6格式的ip地址
  • src: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址
  • dst: 传出参数, 存储转换得到的小端的点分十进制的IP地址
  • size: 修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节

返回值:

  • 成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串
  • 失败: NULL

示例 1:客户端指定服务器 IP(inet_pton()

1
2
3
4
5
6
7
8
9
10
11
12
13
// 配置服务器地址
struct sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080); // 端口转网络序

// 字符串IP "127.0.0.1" 转二进制IP(网络序),存入sin_addr
if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
std::cerr << "IP转换失败!" << std::endl;
return -1;
}

// 连接服务器
connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

示例 2:服务端解析客户端 IP(inet_ntop()

1
2
3
4
5
6
7
8
9
10
11
12
13
// accept接收客户端连接后,解析客户端IP和端口
struct sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);

// 二进制IP(网络序)转字符串IP
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));

// 端口转主机序(必须用ntohs())
uint16_t client_port = ntohs(client_addr.sin_port);

std::cout << "客户端:" << client_ip << ":" << client_port << std

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
2
char ip_buf[15]; // 仅15字节,少了结束符的位置
inet_ntop(AF_INET, &ip_bin, ip_buf, 15); // 可能导致内存越界

正例(正确):用 INET_ADDRSTRLEN 定义缓冲区

1
2
char ip_buf[INET_ADDRSTRLEN]; // 系统保证足够大(16字节)
inet_ntop(AF_INET, &ip_bin, ip_buf, INET_ADDRSTRLEN); // 安全无溢出

三、对比:IPv6 对应的常量

如果处理 IPv6 地址,对应的常量是 INET6_ADDRSTRLEN(值为 46),用法完全一致:

1
2
3
// 存储IPv6字符串的缓冲区
char ipv6_buf[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &ipv6_bin, ipv6_buf, INET6_ADDRSTRLEN);

还有一组函数也能进程IP地址大小端的转换,但是只能处理ipv4的ip地址:

1
2
3
4
5
// 点分十进制IP -> 大端整形
in_addr_t inet_addr (const char *cp);

// 大端整形 -> 点分十进制IP
char* inet_ntoa(struct in_addr in);

关键避坑要点

  1. 字节序问题

    • IP 转换函数(inet_pton()/inet_addr())返回的二进制 IP已经是网络序,无需再调用 htonl()
    • 只有手动构造的 32 位整数 IP(如 0x7F000001)才需要用 htonl() 转网络序。
  2. inet_ntoa() 线程不安全

    • 它返回的是静态缓冲区,多线程下会被覆盖,优先用 inet_ntop()
  3. 非法 IP 处理

    • inet_addr() 无法区分 255.255.255.255 和非法 IP(都返回 -1),推荐用 inet_pton()
  4. 缓冲区大小

    • inet_ntop()dst 缓冲区大小至少为 INET_ADDRSTRLEN(IPv4)/ INET6_ADDRSTRLEN(IPv6),避免缓冲区溢出。