字节序问题

本文探讨了处理器间通信时字节对齐及字节序的重要性,并提供了具体的处理方法,包括如何针对不同编译平台和处理器进行字节对齐以确保数据一致性。
处理器间通过消息(对于C/C++而言就是定义的结构体)进行通信时需要注意字节对齐以及字节序的问题。
1、字节对齐
1.1字节对齐
某些处理器不允许16位和32位的数据在内存中任意排放。例如,Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常。
通常32位的处理器通过总线访问(包括读和写)内存数据。每个总线访问周期可以访问32位内存数据。内存数据是以8位的字节为单位存放的。
假如一个32位的数据没有在4字节整除的内存地址处存放,那么处理器就需要2个总线周期对其进行访问。通过合理的内存对齐可以提高访问效率。
大多数编译器提供内存对其的选项供用户使用。这样用户可以根据处理器的情况选择不同的字节对齐方式。例如C/C++编译器提供的#pragma pack(n) n=1,2,4等,让编译器在生成目标文件时,使内存数据按照指定的方式排布在1,2,4等字节整除的内存地址处。
然而在不同编译平台,或者不同处理器上,字节对齐会造成消息结构长度的变化。现在假设用户定义结构体如下:
struct Message
{
  short opcode;
  char subfield;
  long message_length;
  char version;
  short destination_processor;
};
由于为了字节对齐而进行的填充是通过编译器进行的编译器可以将上述结构填充成:
struct Message
{
  short opcode;
  char subfield;
  char pad1; // Pad to start the long word at a 4 byte boundary
  long message_length;
  char version;
  char pad2; // Pad to start a short at a 2 byte boundary
  short destination_processor;
  char pad3[4]; // Pad to align the complete structure to a 16 byte boundary
};
也可能填充成其他形式。如此,不同编译平台,处理器间数据通信的风险就不必说了吧。
1.2处理方法
以下以32位处理器为例,提出一种内存对齐方法。
对于本地使用的数据结构,为提高内存访问效率,采用四字节对齐方式;同时为了减少内存的开销,合理安排结构成员的位置,减少四字节对齐导致的成员之间的空隙,降低内存开销。
对于处理器之间的数据结构,需要保证消息的长度不因为在不同编译平台和不同处理器导致消息结构的长度发生变化,使用一字节对齐方式对消息结构进行紧缩;为保证处理器之间的消息的数据结构的内存访问效率,采用字节填充的方式自己对消息中成员进行四字节对齐。
数据结构的成员位置要兼顾成员之间的关系、数据访问效率和空间利用率。顺序安排的原则是:四字节的放在最前面,两字节的紧接最后一个四字节成员,一字节紧接最后一个两字节成员,填充字节放在最后。举例如下:
typedef struct tag_T_MSG{
  long ParaA;
  long ParaB;
  short ParaC;
  char ParaD;
  char Pad; /* 填充字节 */
} T_MSG;
2、字节序:
2.1、字节序
小尾(Little Endian)就是低位字节排放在内存的低端,高位字节排放在内存的高端。例如对于一个4字节的整数Byte3 Byte2 Byte1 Byte0将在内存中按照如下顺序排放:
  Base Address+0 Byte0
  Base Address+1 Byte1
  Base Address+2 Byte2
  Base Address+3 Byte3
Intel处理器大多数使用小尾(Little Endian)字节序。
大尾(Big Endian)就是高位字节排放在内存的低端,低位字节排放在内存的高端。例如对于一个4字节的整数Byte3 Byte2 Byte1 Byte0将在内存中按照如下顺序排放:
  Base Address+0 Byte3
  Base Address+1 Byte2
  Base Address+2 Byte1
  Base Address+3 Byte0
Motorola处理器大多数使用大尾(Big Endian)字节序。
网络字节序:TCP/IP各层协议将字节序定义为大尾,因此TCP/IP协议中使用的字节序通常称之为网络字节序。
2.2处理方法
网络字节序作为一个标准字节序,如果系统并没有提供相关的转换函数,我们可以通过以下4个宏实现本地字节序和网络字节序的相互转换:
htons():将16位无符号整数从本地字节序转换成网络字节序 
htonl():将32位无符号整数从本地字节序转换成网络字节序
ntohs():将16位无符号整数从网络字节序转换成本地字节序
ntohl():将32位无符号整数从网络字节序转换成本地字节序
#if defined(BIG_ENDIAN) && !defined(LITTLE_ENDIAN)
#define htons(A) (A)
#define htonl(A) (A)
#define ntohs(A) (A)
#define ntohl(A) (A)
#elif defined(LITTLE_ENDIAN) && !defined(BIG_ENDIAN)
#define htons(A) ((((uint16)(A) & 0xff00) >> 8) | /
  (((uint16)(A) & 0x00ff) << 8))
#define htonl(A) ((((uint32)(A) & 0xff000000) >> 24) | /
  (((uint32)(A) & 0x00ff0000) >> 8) | /
  (((uint32)(A) & 0x0000ff00) << 8) | /
  (((uint32)(A) & 0x000000ff) << 24))
#define ntohs htons
#define ntohl htohl
#else
#error "Either BIG_ENDIAN or LITTLE_ENDIAN must be #defined, but not both."
#endif

在UDP通信中,IP地址的字节序处理是一个关键问题。由于不同计算机体系结构可能存在字节序差异(大端或小端),因此在进行网络通信时,必须确保数据的一致性。IP地址本质上是一个32位的整数,在网络传输中按照大端字节序进行处理,即高位字节先传输[^3]。 ### IP地址的表示与存储 IP地址在程序中通常以点分十进制字符串形式表示,例如 `192.168.1.1`。然而,在底层网络通信中,它需要被转换为一个32位的整数。这个整数在网络上传输时遵循大端字节序,即高位字节在前。为了实现这种转换,C语言中提供了 `inet_pton` 函数用于将字符串形式的IP地址转换为网络字节序的二进制形式[^1]。 例如,将 `192.168.1.1` 转换为网络字节序的32位整数后,其十六进制表示为 `0xC0A80101`,其中高位字节 `0xC0` 是第一个字节。 ### 主机字节序网络字节序的转换 由于主机的字节序可能是小端或大端,为了保证跨平台的一致性,程序中通常使用以下函数进行字节序转换: - `htonl()`:将32位整数从主机字节序转换为网络字节序。 - `htons()`:将16位整数从主机字节序转换为网络字节序。 - `ntohl()`:将32位整数从网络字节序转换为主机字节序。 - `ntohs()`:将16位整数从网络字节序转换为主机字节序。 在UDP通信中,发送端通常需要将IP地址和端口号转换为网络字节序,接收端则将其转换回主机字节序,以确保数据的正确解析。例如,端口号通常使用 `htons()` 转换为网络字节序后再填充到 `sockaddr_in` 结构体中。 ### 示例代码:UDP通信中的IP地址与端口号处理 以下是一个简单的UDP发送端代码片段,展示了如何进行IP地址和端口号的字节序处理: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> int main() { int sockfd; struct sockaddr_in server_addr; char *ip = "192.168.1.1"; int port = 8080; // 创建UDP套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { perror("Socket creation failed"); exit(EXIT_FAILURE); } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(port); // 将端口号转换为网络字节序 // 将IP地址从字符串转换为网络字节序的整数 if (inet_pton(AF_INET, ip, &server_addr.sin_addr) <= 0) { perror("Invalid address / Address not supported"); close(sockfd); exit(EXIT_FAILURE); } // 发送数据 const char *message = "Hello, UDP!"; sendto(sockfd, message, strlen(message), 0, (const struct sockaddr *)&server_addr, sizeof(server_addr)); printf("Message sent to %s:%d\n", ip, port); close(sockfd); return 0; } ``` ### 接收端处理 接收端在接收到数据后,通常需要将端口号从网络字节序转换回主机字节序,以进行服务匹配。例如: ```c struct sockaddr_in client_addr; socklen_t len = sizeof(client_addr); char buffer[1024]; int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &len); buffer[n] = '\0'; // 将端口号转换回主机字节序 int client_port = ntohs(client_addr.sin_port); char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN); printf("Received message from %s:%d\n", client_ip, client_port); ``` ### 总结 在UDP通信中,IP地址和端口号的字节序处理是确保跨平台兼容性的关键步骤。IP地址通过 `inet_pton` 转换为网络字节序的32位整数,端口号则通过 `htons` 和 `ntohs` 在主机与网络字节序之间转换。这些操作保证了不同架构的主机之间可以正确地进行通信。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值