网络通信大小端转换与结构体字节对齐
最近在做雷达与IMU的无线数据转发,但在数据解析过程中出现了数据混乱,让我很是头疼,因为之前是用Python做的,由于Python代码较为简单,所以对当时的大小端问题有疏忽;这次换做了C++的Boost库来做UDP通信,发现了这个问题。
在网络通信当中,要传输的多字节数例如 16位,32位数据,通常需要涉及到数据的大小端转换问题,也与发送端和接收端的大小端存储模式相关,为了避免在数据接收端出现数据错乱的情况,我们通常要使用大小端转换来解决。
发送端:ESP32S3 小端模式
接收端:电脑 x86 小端模式
uint16位数据发送和接收过程(无大小端转换)
例如:uint16_t temp = 0x1234 ,他有两个8位的字节组成。
- 高字节:
0x12 - 低字节:
0x34
发送端:由于ESP32S3的小端特性,低字节放低地址,先存低字节
内存排列(从左到右 → 地址由低到高): 内存:0x34 0x12 网络线路传输顺序:先发 0x34,再发 0x12
接收端:由于电脑 x86架构也是小端模式,收到顺序存入内存;收到第一个字节0x34放到低内存地址;收到第二个字节0x12放进高内存地址 拼接规则:低地址当低字节,高地址当高字节读到的值 = 0x1234
⭐ 此时好像没什么问题,但是他必须要求发送和接收端都是小端模式存储,如果你更换接收设备,同时这个设备他不是小端模式存储,那么数据就会错乱,直接乱码。
⚡ 因此历史规范:互联网规定「网络字节序强制大端」所有标准字段:IP、端口、协议长度字段,都统一按照大端模式存储
uint16位数据发送和接收过程(有大小端转换)
要发送的数据:uint16_t temp = 0x1234 ,目标:接收端同样收到 0x1234
- 发送前转成大端模式:使用
htons()转换
uint16_t send_net = htons(val); 转换后逻辑值还是0x1234,但内存字节顺序翻转,原来是0x3412,现在是0x1234;在网络中,先发0x12,再发0x34.
- 接收后转成小端模式:接收到的数据在内存中存储为
0x3412
转换小端模式,使用uint16_t recv_host = ntohs(recv) 此时 recv_host存储的值就是0x1234
大小端转换的4个函数原型(字节顺序翻转)
包含头文件 #include <arpa/inet.h>
- 主机 → 网络(发送前用)
uint16_t htons(uint16_t hostshort); // 16位:端口号
uint32_t htonl(uint32_t hostlong); // 32位:IP/数值- 网络 → 主机(接收后用)
uint16_t ntohs(uint16_t netshort); // 16位
uint32_t ntohl(uint32_t netlong); // 32位- 16 位转换:
htons /ntohs内部实现
因为翻转两次 = 还原,所以收发共用一套逻辑
// 内部真实实现(简化版)
uint16_t htons(uint16_t x)
{
// 把 2 个字节反过来!
return ((x & 0xFF) << 8) | ((x >> 8) & 0xFF);
}输入:0x1234
x & 0xFF→ 取低字节0x34<< 8→ 左移 8 位 →0x3400x >> 8→ 右移 8 位 →0x12- 两者相或 →
0x3400 | 0x0012 = 0x3412
结果:0x1234 → 变成 → 0x3412,字节完全翻转!
- 32 位转换:
htonl /ntohl内部实现
uint32_t htonl(uint32_t x)
{
return ((x & 0xFF) << 24) |
(((x >> 8) & 0xFF) << 16) |
(((x >> 16) & 0xFF) << 8) |
((x >> 24) & 0xFF);
}把 4 个字节 ABCD → DCBA
有符号int16_t数据处理
- 不管
int16_t(有符号)还是uint16_t(无符号):内存里只是两个二进制字节,符号位本身就藏在高位字节里; - 大小端转换只做一件事:翻转两个字节顺序,不碰里面的 bit、不修改正负;
- 发送:强转
uint16_t→htons翻转字节 → 发送 - 接收:收网络大端数据 →
ntohs翻回本机顺序 → 强转回int16_t自动恢复正负
问题表述:在UDP套接字传输过程中,我在解析ESP32S3发来的雷达数据时,发现雷达数据解析失败,进一步排查是雷达的距离数据和强度数据出现了错位,距离数据时16位,强度数据是8位,后续查找资料后发现是结构体字节对齐问题。
什么是结构体对齐
CPU 不是按 1 字节随便读内存,而是按「固定块」读取(4 字节、8 字节一块)编译器为了让 CPU 读得更快、硬件不报错,会自动在结构体成员之间填充空白字节,这就是结构体内存对齐
举例说明结构体字节对齐
uint16_t(2 字节,16 位)uint8_t(1 字节,8 位)
// 没有任何对齐指令:默认自然对齐
struct MsgDefault
{
uint8_t flag; // 1字节 8位
uint16_t value; // 2字节 16位
};规定:
- 8 位(1 字节):随便放哪里都可以(0、1、2、3…)
- 16 位(2 字节):必须从偶数位置开始放(0、2、4…)
- 32 位(4 字节):必须从 4 的倍数开始放
下面一步步来排一下内存分布
- 首先放
flag变量,由于它是1个字节,那么他无所谓放在那里,我们把它放在偏移0的位置,那么下一个位置就是偏移1 - 接下来放
value变量,他是16位的,占两个字节,又由于16位数据必须放在偏移位置位偶数的地方,而此时偏移位置为1,是奇数;此时编译器自动帮你塞一个空字节(填充)占住偏移 1,0: flag;1: 填充字节(空的,没用) - 现在下一个位置变成了 偏移 2(偶数),可以放
value了
最终的内存分布
0: flag
1: 填充
2: value 第1字节
3: value 第2字节总大小 = 1 + 1 + 2 = 4 字节(原来是1+2=3字节)现在编译器自动添加了一个字节,那么在读取的时候就会出现呢内存错位。
解决结构体字节对齐问题(强制紧凑对齐)
🔥使用__attribute__((packed))关键字 直接告诉编译器:这个结构体,取消所有自动填充,全部紧凑排列。
typedef struct {
uint8_t a;
uint16_t b;
} __attribute__((packed)) test_t;它强制覆盖对齐规则:
- 不让 16 位变量必须从偶数开始
- 不让 32 位变量必须从 4 的倍数开始
- 所有成员紧贴着放,没有任何空隙
a(1字节) + b(2字节) = 总3字节
无填充、无空位🔥使用#pragma pack(push, 1) + #pragma pack(pop) 它告诉编译器:从现在开始,所有结构体按 1 字节对齐,不许填充
作用
push:保存当前对齐方式1:强制改成 1 字节对齐pop:恢复原来的对齐方式
#pragma pack(push, 1) // 临时改成1字节对齐
struct Test {
uint8_t a;
uint16_t b;
};
#pragma pack(pop) // 恢复默认🔥_Alignas(1) (C11 标准关键字)
_Alignas(N) = 强制按 N 字节对齐
struct Test {
uint8_t a;
uint16_t b;
} _Alignas(1);本项目实际情况
传输雷达距离和强度数据到电脑
// 雷达单个点的结构体
typedef struct {
uint16_t distance; // 距离,单位mm
uint8_t intensity; // 信号强度
} ld14p_point_t;发送端ESP32S3
for (int i = 0; i < LD14P_POINT_PER_PACK; i++) //每包12个数据点
{
ros_lidar.lidar.points[i].distance = hton16(temp_lidar.points[i].distance);//距离数组16位
ros_lidar.lidar.points[i].intensity = temp_lidar.points[i].intensity;//强度数据8位
printf("距离:0x%x,强度:0x%x \\r\\n", temp_lidar.points[i].distance , temp_lidar.points[i].intensity);
}接收端电脑x86
for(int i=0;i< LD14P_POINT_PER_PACK;i++){ //此处注意接口提字节对齐问题,距离数据位16位,强度数据位8位,不用对齐会错位(已踩坑2026.3.27 21:07)
lidar_rawData.points[i].distance = (recv_buf_[3*i+7] << 8) | recv_buf_[3*i+8];
lidar_rawData.points[i].intensity = recv_buf_[3*i+9];
printf("距离:0x%x,强度:0x%x \\r\\n", lidar_rawData.points[i].distance , lidar_rawData.points[i].intensity);
}结果出现了字节错位,原因是距离为2个字节,强度为1个字节

修改发送与接收的雷达单点结构体加上__attribute__((packed))
// 雷达单个点的结构体
typedef struct {
uint16_t distance; // 距离,单位mm
uint8_t intensity; // 信号强度
} __attribute__((packed)) ld14p_point_t;结果正确

转换成实际距离M

| 方式 | 语法风格 | 适用平台 | 特点 |
|---|---|---|---|
__attribute__((packed)) | GCC 风格 | Linux / 嵌入式 | 最常用、最简单 |
#pragma pack(1) | 预处理指令 | 全平台(Windows+Linux) | 最兼容 |
_Alignas(1) | C11 标准 | 现代编译器 | 标准官方 |
注意事项:如果你定义的结构体中含有子结构体,那么在使用__attribute__((packed)) 和_Alignas时,要对每一个结构体加上,否则无法实现紧凑对齐
// 子结构体:加 packed
struct Sub {
uint8_t a;
uint16_t b;
} __attribute__((packed));
// 父结构体:也加 packed
struct Main {
uint8_t x;
struct Sub sub;
uint32_t y;
} __attribute__((packed));