一、什么是大端和小端?
小端:低位数据放在低地址
大端:高位数据放在低地址
比如 0x12 34 56 78
,其中78是低位,12是高位。该数的右侧是低地址,左侧是高地址。也就是说低位对应低地址(小端字节序)
如果0x12 34 56 78
是小端字节序,那么0x78 56 34 12
就是大端字节序,它把高位12放在最右侧的低地址(大端字节序)
一般,
主机字节序 是 小端模式
网络字节序 是 大端模式
而一个字节中的比特序,是低位放在低地址的。
为什么要有大端小端之分?
小端: 在内存中通常低位放在低地址。比如说0x12 34 56 78
,从右往左,这样使得程序处理速度更快。
大端:网络字节序,0x78 56 34 12
将字节序反了一下。
但是一般人们阅读习惯就是从左往右。举个例子,对于网络字节序0x78 56 34 12
,网络通常以字节流的方式传输,先接受到的是低地址的12,接着是34,…这样就能接收到的顺序就是12345678
,也就是正常的顺序。如果直接按照0x12 34 56 78
进行传输,会先接受到78然后是56…这个样子显然是不方便人为的阅读。
看了一堆文字,可能会觉得很抽象,不如动手验证下。
下面将会验证主机序是小端字节序,并且会以IP协议头为例,更好地去解释大端模式的网络字节序。
二、主机序:小端模式
1、验证主机的字节序
将num=0xff00ff00
通过memcpy
拷贝到struct MyInt
看看内存地址是如何分布的
struct MyInt{ unsigned char F1;//低地址 unsigned char F2; unsigned char F3; unsigned char F4;//高地址 }; void test1(){ MyInt* myint=(MyInt*)malloc(sizeof(MyInt)); unsigned int num=0xff00ff00; memcpy((void*)myint,(void*)&num,sizeof(int)); cout<<"num:"<<std::hex<<num<<endl; cout<<"F1:"<<std::hex<<(int)myint->F1<<endl; cout<<"F2:"<<std::hex<<(int)myint->F2<<endl; cout<<"F3:"<<std::hex<<(int)myint->F3<<endl; cout<<"F4:"<<std::hex<<(int)myint->F4<<endl; } int main(){ test1(); system("pause"); }
运行结果
可以看到F1是低地址 对应 低位,F4是高地址 对应 高位,也就是说 主机序是小端字节序
2、查看比特序
进行略微修改,通过位域操作,将 F1的一个字节,拆分成两个半字节(4比特),来验证下比特序
struct MyInt{ unsigned char F11:4, F12:4; unsigned char F2; unsigned char F3; unsigned char F4;//高地址 }; void test1(){ MyInt* myint=(MyInt*)malloc(sizeof(MyInt)); unsigned int num=0xff00ff0f;//0xff00ff00改为0xff00ff0f memcpy((void*)myint,(void*)&num,sizeof(int)); cout<<"num:"<<std::hex<<num<<endl; cout<<"F11:"<<std::hex<<(int)myint->F11<<endl; cout<<"F12:"<<std::hex<<(int)myint->F12<<endl; cout<<"F2:"<<std::hex<<(int)myint->F2<<endl; cout<<"F3:"<<std::hex<<(int)myint->F3<<endl; cout<<"F4:"<<std::hex<<(int)myint->F4<<endl; } int main(){ test1(); system("pause"); }
运行结果
可以看到 F11
的地址比F12
的地址更低,由此可见,在比特位 的低位 在低地址
3、主机序总结
对于主机序,低位字节 在低地址,因此是小端字节序。
同时,比特序,低位 在 低地址。
三、网络序
如果 0x12 34 56 78
是小端序,那么大端序就是0x78 56 34 12
(也就是网络字节序)
在发送或者接受到的数据,都必须要是网络字节序的。
1、以IP协议头为例
左上角为高位,右下角为低位,以这种方式组装成数据帧,在网络中进行发送。
可以通过定义一个结构体,存放协议头中每个标签。
struct iphdr { unsigned char hdrlen:4, version:4; unsigned char tos;//type of service unsigned short totlen;//total length unsigned short id;//16位标识 unsigned short flag_offset; //3位标志+13位片偏移 unsigned char ttl; //time to live 生存周期(比如:每经过一个网关ttl-1) // 0x1234// htons unsigned char type;//8位协议 用于指明IP的上层协议. unsigned short check;//16位首部校验和 unsigned int sip;//源ip unsigned int dip;//目的ip }; // 20字节
为什么要用结构体?
因为结构体起始是低地址,低地址处放协议头的高位,那么不就是大端模式了嘛。
接下去,还有一步骤没有做,就是将数据帧iphdr
+数据
等内容通过memcpy
的方式加入到 发送缓冲char* buffer
中去,低地址 依然 放的高位不会变。
通过memcpy,可以将version和hdrlen拷贝到最右侧(只是为了分析字节序,所以不考虑以太网头等内容)。
那样对于高位的version和hdrlen 不就是在低地址了吗(高位在低地址,也就是 大端字节序)
因此协议头的高位在在char* buffer
中的低地址处,因此是网络字节序,这种网络字节序的请求,可以被服务端所解析。
2、字节中的比特序
为什么 hdrlen
(header len 首部长度)是高位呢?而不是version
(4位版本)是高位?
因为网络字节序是按字节为单位的,发送的是字节流,char*
作为类型,无论是服务端或者客户端,都只对数据进行解包和打包,char*
作为一个基本单位,因此不会再去继续分。
也就是说,不会涉及比一个char*
更小的操作,那么 比特序只要和主机端的比特序一致就行了,也就是说低地址放低比特位,于是可知hdrlen
相比version
是低位,因此放在前面。
但是在用结构体 定义协议头的时候,为了结构清晰,一般会用域操作细分。
unsigned char hdrlen:4, version:4;
可以看到高位放在低地址中,也就是大端模式。