一、大小端存储
什么是大小端存储?
一个4字节的数据:0x12345678 在内存中的存储方式有两种:
一种是数据的高位存到内存中的低地址,数据的低位存到内存中的高地址----------大端存储
另一种是数据的低位存到内存中的高地址,数据的高位存到内存中的低地址----------小端存储
读取一个4字节的数据:从内存的低地址到高地址依次读取,大端存储先读取到的是高位,小端存储先读取到的是低位,读取完成后会组合成一个正确的值。
为什么会有大小端存储?
简单说是因为个厂商的存储硬件没有进行统一,所以存现的大小端存储共存的情况。大小端存储的区别是在多字节的数据类型上,因为从内存中读取数据是按照数据类型读取相应的字节,比如读取一个Int,就要从内存中取出4个字节,将内存中的4个字节转换成int值,这个转换过程就与大小端存储有关。可以理解成从内存中读取4个char类型,然后将其转换成int。
在同一台机器上数据的存储和读取,不管是大端存储和小端存储都不会产生影响。当数据从网络中接收并存储到本地机器的内存中,就需要注意本地数据的存储方式。
二、网络字节序与主机字节序
为什么会有网络字节序和主机字节序?
网络字节序为大端字节序,现代Pc大多采用小端字节序,所以主机字节序被称为小端字节序,但严格意义来讲主机字节序也有可能是大端字节序。
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释。解决的办法是:发送端总是把要发送的数据转换成大端字节序数据后再发送,而接受端知道对方传过来的数据总是大端字节序,所以接收端可以根据自生采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。
下面以一个复杂结构体在网络传输为例说明没有进行转换的错误。
下面代码发送端准备发送数据
struct {
char buf[2];
int a;
}DATA;
int main()
{
char tmp_buf[2] ={ 0x10,0x11 };
int data = 0x12345678;
char buf[8]={0};
memcpy(DATA.buf,tmp_buf,2);
DATA.a=data;
memcpy(buf,&DATA,sizeof(DATA));
for(int i=0;i<8;i++)
{
printf("%.2x ",buf[i]);
}
}
上图为将结构体转换成字节流进行网络传输,发送端为小端存储,存储到内存的int值为0x78563412,接受端为大端存储,将接受到的字节流从地址值到高地址依次存储,存储的顺序与发送端一致。
接收端从网络传输的字节流中读取buf,将buf转换成结构体,获得int的值。
DATA = (DATA_st*)buf;
int data = DATA.a;
printf("%x ", data);
这时int的值为0x78563412,而发送端int的值为0x12345678,所以由于两台主机的存储方式不同,在网络传输的过程中多字节数据就有可能被错误解释。
将发送端转换成网络字节序,接收端转换成主机字节序可解决该问题。
struct {
char buf[2];
int a;
}DATA;
int main()
{
char tmp_buf[2] ={ 0x10,0x11 };
int data = 0x12345678;
char buf[8]={0};
memcpy(DATA.buf,tmp_buf,2);
DATA.a=htonl(data);
memcpy(buf,&DATA,sizeof(DATA));
for(int i=0;i<8;i++)
{
printf("%.2x ",buf[i]);
}
}
接收端如下
DATA = (DATA_st*)buf;
int data = ntohl(DATA.a);
printf("%x ", data);
通过htonl将int转换成网络字节序,ntohl将网络转换成主机字节序,由于接收端本身就是大端存储,所以数据不会改变。这时发送端int数据为0x12345678,接收端接受的数据也为0x12345678。这样就保证了不同主机的数据在网络传输的过程中仍然能保证一致性。
注意:
字节流在存储的时候都是从内存的低地址存到高地址,主机是不会感知该字节流是大端字节序还是小端字节序。只有把字节流转换成Int值是,主机才会对大端存储和小端存储进行区分,大端存储先读取的是高位,小端存储先读取的是低位。
htons和htonl的实现原理
// 短整型大小端互换
#define BigLittleSwap16(A) ((((uint16)(A) & 0xff00) >> 8) | \
(((uint16)(A) & 0x00ff) << 8))
// 长整型大小端互换
#define BigLittleSwap32(A) ((((uint32)(A) & 0xff000000) >> 24) | \
(((uint32)(A) & 0x00ff0000) >> 8) | \
(((uint32)(A) & 0x0000ff00) << 8) | \
(((uint32)(A) & 0x000000ff) << 24))
// 本机大端返回1,小端返回0
int checkCPUendian()
{
union{
unsigned long int i;
unsigned char s[4];
}c;
c.i = 0x12345678;
return (0x12 == c.s[0]);
}
// 模拟htonl函数,本机字节序转网络字节序
unsigned long int t_htonl(unsigned long int h)
{
// 若本机为大端,与网络字节序同,直接返回
// 若本机为小端,转换成大端再返回
return checkCPUendian() ? h : BigLittleSwap32(h);
}
// 模拟ntohl函数,网络字节序转本机字节序
unsigned long int t_ntohl(unsigned long int n)
{
// 若本机为大端,与网络字节序同,直接返回
// 若本机为小端,网络数据转换成小端再返回
return checkCPUendian() ? n : BigLittleSwap32(n);
}
// 模拟htons函数,本机字节序转网络字节序
unsigned short int t_htons(unsigned short int h)
{
// 若本机为大端,与网络字节序同,直接返回
// 若本机为小端,转换成大端再返回
return checkCPUendian() ? h : BigLittleSwap16(h);
}
// 模拟ntohs函数,网络字节序转本机字节序
unsigned short int t_ntohs(unsigned short int n)
{
// 若本机为大端,与网络字节序同,直接返回
// 若本机为小端,网络数据转换成小端再返回
return checkCPUendian() ? n : BigLittleSwap16(n);
}
}
注意
htonl,htons只是先检测本端是大端存储还是小端存储,如果是小端存储则将多字节的存储顺序颠倒,如果本身是大端存储则存储顺序不变;ntohl,ntohs相似,如果主机是小端存储则将存储顺序交换,如果是大端存储则不变;如果一个主机对一个数调用两次htonl,如果该主机是小端存储则会对该数的存储顺序转换两次,相当于不变,如果是大端存储则不变;如果一个主机对一个数调用两次ntohl,如果主机是小端也会对该数的存储顺序转换两次。
总结
多字节数据存储到内存中和从内存中读取多字节数据才分大小端存储,一个char类型的字节流在内存中从低地址存到高地址。在一个主机中对多字节数据的存储和读取都不会出现问题,当两台主机进行通信时,就可能出现解析错误的情况,所以要注意网络字节序和主机字节序的转换。