Linux 网络编程:深度理解网络字节序与主机字节序、大端字节序与小端字节序

📖引言

在上一篇 Linux 网络编程的博客Linux网络编程:Socket套接字编程概念及常用API接口介绍-CSDN博客中,介绍了基本 Socket 网络编程的 API ,当我在使用 bind()函数绑定IP地址和端口时,遇到了一个看似简单却十分关键的问题------字节序。 所以在能进行完整的 TCP协议或者 UDP 协议的网络通讯之前还需要知道什么是网络字节序与主机字节序,二者之间如何转换。

🔍一、什么是大端字节序与小端字节序

1.1 什么是字节序

在网络编程中,特别是在跨平台和网络通信时,字节序(Byte Order)是非常重要的概念。字节序指的是多字节数据在内存中的存储顺序。

对于单字节数据(如 char),不需要考虑字节序。但是对于多字节数据(如 int, short),不同架构的 CPU 在内存中存放它们的顺序是不同的。需要决定:高位字节放在前面还是后面?

这就好比我们写日期:

  • 中国人习惯:年-月-日(2023-12-20)→ 高位在前

  • 美国人习惯:月-日-年(12-20-2023)→ 低位在前

1.2 大端字节序 (Big-endian)

规则高位字节存储在低地址,低位字节存储在高地址。

记忆口诀:"大端模式,高位在前"

特点:符合人类的阅读习惯(从左到右)。

1.3 小端字节序 (Little-endian)

规则低位字节存储在低地址,高位字节存储在高地址。

记忆口诀:"小端模式,低位在前"

特点:更符合计算机读取逻辑(先读低位,方便运算)。

假设有一个 32 位整数(4字节):0x12345678,同时内存起始地址为 0x100,其中 0x12 为最高位,0x78 为最低位。

内存地址 小端模式 (Little-endian)(x86主机常用) 大端模式 (Big-endian)(网络标准)
0x100 (低地址) 0x78 (低位) 0x12 (高位)
0x101 0x56 0x34
0x102 0x34 0x56
0x103 (高地址) 0x12 (高位) 0x78 (低位)

⚖️二、什么是主机字节序与网络字节序

2.1 主机字节序

主机字节序其本质是当前计算机CPU内部处理多字节数据的字节顺序,也就是说在我们的计算机内,CPU处理的数据都是按照这种顺序进行存储,其本质是服务于CPU的。

但是由于 CPU 架构不同(x86, ARM, MIPS, PowerPC),数据在内存里的存放顺序是不统一 的。

我们常用的 Intel/AMD x86 系列 CPU 采用的是 小端序 (Little-Endian)

常见的CPU字节序

CPU架构 字节序 代表设备
x86/x64 小端序 个人电脑、服务器
ARM 可配置(通常小端) 手机、嵌入式设备
PowerPC 大端序 早期苹果电脑、游戏机
SPARC 大端序 工作站服务器

2.2 网络字节序

为了确保不同计算机之间通信时不出现字节顺序不一致导致的数据混乱,网络字节序由TCP/UDP协议规定统一规定使用大端字节序(Big-Endian)。也就是说网络字节序就是大端序。

即使你的机器使用小端,在发送数据之前也要转换成大端。

🔄三、 主机字节序与网络字节序之间的转换

介绍完了两种字节序的概念之后,可以知道,不同主机之间要进行网络通讯,那么就必须要将数据的字节序统一转换为网络字节序也就是大端序。

3.1 转换相关函数

Linux/POSIX 网络编程提供以下四个转换函数:

cpp 复制代码
#include <arpa/inet.h>

/**
 * @brief 将无符号整数 hostlong 从主机字节顺序(h)转换为网络字节顺序(n)。
 */
uint32_t htonl(uint32_t hostlong);

/**
 * @brief 将无符号短整数 hostshort 从主机字节顺序(h)转换为网络字节顺序(n)。
 */
uint16_t htons(uint16_t hostshort);

/**
 * @brief 将无符号整数 netlong 从网络字节顺序(n)转换为主机字节顺序(h)。
 */
uint32_t ntohl(uint32_t netlong);

/**
 * @brief 将无符号短整数 netshort 从网络字节顺序(n)转换为主机字节顺序(h)。
 */
uint16_t ntohs(uint16_t netshort);

功能列表(根据列表理解更易背记):

函数名 功能
htons() Host TO Network Short(端口)
htonl() Host TO Network Long(IPv4地址)
ntohs() Network TO Host Short
ntohl() Network TO Host Long
  • h: host (主机字节序)

  • n: network (网络字节序)

  • s : short (16位,通常用于端口号)

  • l : long (32位,通常用于IPv4地址)

3.2 现代 IP 地址转换 API

网络字节序转换之后还存在一个问题,就是如何将IP地址写入到结构体中,再进行bind函数的绑定。

这里就涉及到 现代 IP 地址转换 API

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

/**
 * @brief 将来自 IPv4 点分十进制表示法的 Internet 主机地址 cp 转换为二进制形式(以网络字节顺序)并将其存储在 inp 指向的结构体中。
 * @return int 成功返回 1;失败 返回 0
 */
int inet_aton(const char *cp, struct in_addr *inp);

/**
 * @brief 将来自 IPv4 点分十进制表示法的 Internet 主机地址 cp 转换为网络字节顺序的二进制数据。
 * @return 如果输入无效,则返回 INADDR_NONE(通常为 -1)。使用此函数存在问题,因为 -1 是一个有效地址(255.255.255.255)。请优先使用 inet_aton()、inet_pton(3) 或 getaddrinfo(3),它们提供了更清晰的错误返回方式。
 */
in_addr_t inet_addr(const char *cp);

/**
 * @brief 函数将字符串 cp(以 IPv4 点分十进制表示法表示)转换为适合用作 Internet 网络地址的主机字节顺序中的数字。
 * @return in_addr_t 成功时,返回转换后的地址。如果输入无效,则返回 -1。
 */
in_addr_t inet_network(const char *cp);

/* @brief   字符串格式转换为sockaddr_in格式
   @param int af: 通常为 AF_INET 用于IPv4地址,或 AF_INET6 用于IPv6地址
   @param  char *src: 包含IP地址字符串的字符数组,如果是IPv4地址,格式为点分十进制(如 "192.168.1.1");如果是IPv6地址,格式为冒号分隔的十六进制表示(如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
   @param  void *dst:指向一个足够大的缓冲区(对于IPv4是一个struct in_addr结构体,对于IPv6是一个struct in6_addr结构体),用于存储转换后的二进制IP地址
   @return int : 成功转换返回0; 输入地址错误返回1;发生错误返回-1
*/
int inet_pton(int af, const char *src, void *dst);

/**
 * @brief 
 * 
 * @param in 将以网络字节顺序给出的 Internet 主机地址 in 转换为 IPv4 点分十进制表示法的字符串。字符串存储在静态分配的缓冲区中,后续调用将覆盖该缓冲区。
 * @return char* 缓冲区指针
 */
char *inet_ntoa(struct in_addr in);

/**
 * @brief 是 inet_netof() 和 inet_lnaof() 的反函数。它返回一个以网络字节顺序表示的 Internet 主机地址,由主机字节顺序中的网络号 net 和本地地址 host 组成。
 */
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);

/**
 * @brief 返回 Internet 地址 in 的本地网络地址部分。返回的值以主机字节顺序表示。
 */
in_addr_t inet_lnaof(struct in_addr in);

/**
 * @brief 返回 Internet 地址 in 的网络号部分。返回的值以主机字节顺序表示。

 */
in_addr_t inet_netof(struct in_addr in);

这里我们再复习一下 socket 网络编程所涉及到的重要数据结构,方便我们理解上面的函数

所有的API都使用通用的 struct sockaddr*,但在IPv4 TCP编程中,我们实际填充的是 struct sockaddr_in,最后强制类型转换(Cast)过去。

cpp 复制代码
#include <netinet/in.h>

struct sockaddr_in {
    sa_family_t    sin_family; /* 地址族: AF_INET (IPv4) */
    in_port_t      sin_port;   /* 端口号 (使用网络字节序) */
    struct in_addr sin_addr;   /* IP地址 */
};

struct in_addr {
    uint32_t       s_addr;     /* 32位IP地址 (使用网络字节序) */
};

介绍完所有的字节序内容之后,我们来看一段代码实例,该文件来自于尚硅谷 Linux 应用层开发课程:

cpp 复制代码
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>

int main(int argc, char const *argv[])
{

    // 网络地址赋值
    struct sockaddr_in server_addr;
    struct in_addr server_in_addr;
    in_addr_t server_in_addr_t;
    memset(&server_addr, 0, sizeof(server_addr));
    memset(&server_in_addr, 0, sizeof(server_in_addr));
    memset(&server_in_addr_t, 0, sizeof(server_in_addr_t));

    // 打印16进制IP地址作为对照
    printf("192.168.6.101 的16进制表示为 0x%X 0x%X 0x%X 0x%X\n", 192, 168, 6, 101);

    // 不推荐使用,因为输入-1返回的地址是一个有效地址(255.255.255.255)
    server_in_addr_t = inet_addr("192.168.6.101");
    printf("inet_addr convert: 0x%X\n", server_in_addr_t);

    inet_aton("192.168.6.101", &server_in_addr);
    printf("inet_aton convert: 0x%X\n", server_in_addr.s_addr);

    // 推荐使用
    // 字符串转sockin_addr结构体
    inet_pton(AF_INET, "192.168.6.101", &server_addr.sin_addr);
    printf("inet_pton 后 server_addr.sin_addr 的16进制表示为 0x%X\n", server_addr.sin_addr.s_addr);

    // 结构体转化为字符串
    printf("通过inet_ntoa打印inet_pton转化后的地址: %s\n", inet_ntoa(server_addr.sin_addr));

    // 打印本地网络地址部分
    printf("local net section: 0x%X\n", inet_lnaof(server_addr.sin_addr));
    
    // 打印网络号部分
    printf("netword number section: 0x%X\n", inet_netof(server_addr.sin_addr));

    // 使用本地网络地址和网络号可以拼接成in_addr
    server_addr.sin_addr = inet_makeaddr(inet_netof(server_addr.sin_addr), 102);

    // 以网络字节序16进制打印拼接的地址
    printf("inet_makeaddr: 0x%X\n", server_addr.sin_addr.s_addr);
    // 打印拼接的地址
    printf("通过inet_ntoa打印inet_makeaddr拼接后的地址%s\n", inet_ntoa(server_addr.sin_addr));
    return 0;
}

Makefile 文件:

bash 复制代码
inet_endian_convert: inet_endian_convert.c
    -$(CC) -o $@ $^
    -./$@
    -rm ./$@

处理结果:

🧱 四、 bind() 与字节序的关系(实战解析)

在以后我们写服务器代码时通常需要写下面一段代码:

cpp 复制代码
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);         // 端口号转换
addr.sin_addr.s_addr = htonl(INADDR_ANY);  // IP 转换

让我们拆解一下

① sin_port(端口号)必须 htons

端口号是16位整数,TCP/IP 要求按大端传输。

② sin_addr.s_addr 必须 htonl

cpp 复制代码
addr.sin_addr.s_addr = htonl(INADDR_ANY);

虽然 INADDR_ANY = 0,但写法必须规范,因为 IP 地址字段本质上是 32 位的大端整数。

这句代码的意思就是将主机IP转换为32 位的大端整数存入s_addr

③ 使用 inet_pton() 时无需手动 htonl

cpp 复制代码
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);

这个函数已经帮你转换成网络字节序了。

④ socket bind() 正确示例

cpp 复制代码
struct sockaddr_in addr;
addr.sin_family = AF_INET;

// 端口号转网络字节序
addr.sin_port = htons(8080);

// IP 地址转网络字节序
addr.sin_addr.s_addr = htonl(INADDR_ANY);

// 绑定
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

📚 五. 写在最后

主机字节序与网络字节序是 Linux 网络编程中的基础知识点,但对初学者来说也**是最容易混淆的部分。**本篇文章不仅解释了两者的区别与联系,还重点介绍了在实际 socket 程序中如何正确使用字节序转换函数。

**掌握字节序,你的 socket 编程将更加稳健、规范。**希望这篇博客能成为你学习路上的"知识锚点"。欢迎点赞、收藏、评论交流!

相关推荐
姓蔡小朋友1 小时前
Redis网络I/O模型
网络·数据库·redis
量子物理学1 小时前
openssl自建CA并生成自签名SSL证书
网络·网络协议·ssl
成空的梦想1 小时前
除了加密,它还能验明正身:SSL如何防范网络钓鱼?
网络·https·ssl
tang_vincent2 小时前
linux 虚拟内存映射原理与启动初始化过程
linux
a3158238062 小时前
Android Framework开发知识点整理
android·java·linux·服务器·framework·android源码开发
honsor3 小时前
精准监测 + 实时传输!网络型温湿度传感器,筑牢环境数据管理防线
网络·物联网
赖small强3 小时前
【Linux C/C++开发】 GCC -g 调试参数深度解析与最佳实践
linux·c语言·c++·gdb·-g
white-persist3 小时前
VSCode 快捷键大全:从设计理念到场景化高效运用(详细解析)(文章末尾有vim快捷键大全)
linux·ide·vscode·python·编辑器·系统安全·vim
杭州泽沃电子科技有限公司3 小时前
煤化工精炼与加工环节的监测:智能平台如何保障最终产品价值与环保合规?
运维·科技