字节序与网络编程基础

← 返回 MOC | ← 主页

ARM Cortex-M 开发板和 x86 服务器通信,数据发过去读出来全是乱的——不是代码写错了,是字节序没转换。


什么是字节序

一个多字节的数(比如 32 位整数 0x12345678),在内存里怎么存?高位字节放前面还是后面?不同架构的答案不一样。

**大端序(Big-Endian):**高位字节存低地址,“大头在前”。

**小端序(Little-Endian):**低位字节存低地址,“小头在前”。

以 0x12345678 为例,存在地址 0x1000 开始的 4 个字节:

地址大端序小端序
0x10000x12(最高字节)0x78(最低字节)
0x10010x340x56
0x10020x560x34
0x10030x78(最低字节)0x12(最高字节)

常见架构的字节序:

  • x86 / x86-64(PC、服务器):小端
  • ARM Cortex-M / Cortex-A(嵌入式、手机):默认小端,可配置
  • MIPS(部分路由器):默认大端
  • PowerPC:大端

代码检测当前机器字节序:

#include <stdio.h>
 
int main(void) {
    uint32_t x = 0x12345678;
    uint8_t *p = (uint8_t *)&x;
    if (p[0] == 0x78) {
        printf("小端序\n");
    } else {
        printf("大端序\n");
    }
    return 0;
}

网络字节序

TCP/IP 协议规定:网络字节序统一使用大端序

为什么要统一?不同架构的机器字节序不同,如果不统一,x86(小端)发出去的数据,MIPS(大端)读出来就是乱的。统一成大端,所有机器发送前转换,接收后转换,就能互通。

**哪些字段受影响:**IP 首部、TCP/UDP 首部中所有多字节字段都是网络字节序,包括:

  • IP 地址(4 字节)
  • 端口号(2 字节)
  • 序列号、确认号(4 字节)
  • 数据长度等

转换函数(重点)

POSIX 标准提供了一组转换函数,<arpa/inet.h><netinet/in.h> 中:

函数含义用途
htons(x)Host to Network Short(16位)端口号发送前转换
htonl(x)Host to Network Long(32位)IP地址、序列号发送前转换
ntohs(x)Network to Host Short(16位)端口号接收后转换
ntohl(x)Network to Host Long(32位)IP地址、序列号接收后转换

记忆口诀:发出去用 hton,收进来用 ntoh。

在大端机器上,这些函数什么都不做(直接返回原值),因为本机字节序就是网络字节序。在小端机器上,它们会翻转字节顺序。

IP 地址字符串转换:

#include <arpa/inet.h>
 
// 字符串 IP → 网络字节序(推荐用 inet_pton,支持 IPv6)
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.1", &addr);
 
// 网络字节序 → 字符串 IP
char buf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, buf, sizeof(buf));

典型用法:

struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(8080);              // 端口号转网络字节序
inet_pton(AF_INET, "192.168.1.1", &server.sin_addr);  // IP 地址转网络字节序

跨平台处理实践

直接 memcpy 结构体的坑:

// 错误做法:直接把结构体发出去
struct Packet {
    uint32_t id;
    uint16_t length;
    uint8_t  data[64];
};
 
struct Packet pkt = {.id = 1, .length = 64};
send(sock, &pkt, sizeof(pkt), 0);  // id 和 length 没有转换字节序!

对端如果是不同字节序的机器,读出来的 id 和 length 就是错的。

正确做法:逐字段转换

// 正确做法:发送前转换
struct Packet pkt;
pkt.id     = htonl(1);    // 32位字段用 htonl
pkt.length = htons(64);   // 16位字段用 htons
memcpy(pkt.data, src, 64);
send(sock, &pkt, sizeof(pkt), 0);
 
// 接收后转换
recv(sock, &pkt, sizeof(pkt), 0);
uint32_t id     = ntohl(pkt.id);
uint16_t length = ntohs(pkt.length);

htons 的实现原理:

// 在小端机器上,htons 就是把两个字节交换
uint16_t htons(uint16_t x) {
    return (x >> 8) | (x << 8);
}
// 在大端机器上,直接返回原值

笔试题直击

Q:什么是大端序和小端序?网络字节序是哪种?

大端序:高位字节存低地址;小端序:低位字节存低地址。x86/ARM 默认小端,TCP/IP 规定网络字节序为大端。

Q:htons 的实现原理

在小端机器上,把 16 位整数的高低字节交换:(x >> 8) | (x << 8)。在大端机器上直接返回原值。

Q:跨平台通信时如何处理字节序问题?

发送前用 htonl/htons 把多字节字段转成网络字节序,接收后用 ntohl/ntohs 转回主机字节序。不要直接 memcpy 整个结构体发送,要逐字段转换。


如果你正在跟随梳理, 返回 MOC←