“字节序”网络中的大小端问题

607 阅读5分钟

大小端在计算机业界,endian表示数据在存储器中的存放顺序。“endian”一词来源于乔纳森·斯威夫特的小说格列佛游记。

小说中,小人国为水煮蛋该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开而争论,争论的双方分别被称为“大端派”和“小端派”。

看到没有,仅仅是剥鸡蛋就能产生这么大的分歧,“大端”和“小端”有这么重要嘛!

什么是字节序

字节

字节(Byte)是存储数据的基本单位,并且是硬件所能访问的最小单位。

常见的存储单位主要有bit(位)、B(字节)、KB(千字节)、MB(兆字节)、GB(千兆字节)。它们之间主要有如下换算关系:

1B=8bit 1KB=1024B 1MB=1024KB 1GB=1024MB

其中 B 是 Byte 的缩写。

字节序

字节序,也就是字节的顺序,指的是多字节的数据在内存中的存放顺序。

在几乎所有的机器上,多字节对象都被存储为连续的字节序列。例如:如果C/C++中的一个int型变量 a 的起始地址是&a = 0x100,那么 a 的四个字节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置。

根据整数 a 在连续的 4 byte 内存中的存储顺序,字节序被分为大端序(Big Endian) 与 小端序(Little Endian)两类。 然后就牵涉出两大CPU派系:

Motorola 6800,PowerPC 970,SPARC(除V9外)等处理器采用 Big Endian方式存储数据; x86系列,VAX,PDP-11等处理器采用Little Endian方式存储数据。 ARM, PowerPC (除PowerPC 970外), DEC Alpha, SPARC V9, MIPS, PA-RISC and IA64的字节序是可配置的。

大端也好,小端也罢,就权当是个人爱好吧,只要你不影响别人就行,是这个道理吧?

大端小端概念

大端小端其实是我们通俗意义上的叫法,实际上指的是计算机存储字节的顺序模式,根据数据在内存中的存储方式分为两种大端字节序模式和小端字节序模式。

大端字节序模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。符合我们的阅读习惯。

小端字节序模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。这个比较符合逻辑思维习惯,高对高,低对低。

那么,到底什么是大端,什么是小端? 如下图(图片来源于网络):

相信上面的图已经够直观了。也就是说:

Big Endian 是指低地址端 存放 高位字节。 Little Endian 是指低地址端 存放 低位字节。

为什么要注意字节序

如果你写的程序只在单机环境下面运行,并且不和别人的程序打交道,那么你完全可以忽略字节序的存在。

但是,如果你的程序要跟别人的程序产生交互呢? 比如,当一个 C/C++ 的程序要与一个 Java 程序交互时:

C/C++语言编写的程序里数据存储顺序是跟编译平台所在的CPU相关的,而现在比较普遍的 x86 处理器是 Little Endian Java编写的程序则唯一采用 Big Endian 方式来存储数据

试想,如果你的C/C++程序将变量 a = 0x12345678 的首地址传递给了Java程序,由于Java采取 Big Endian 方式存储数据,很自然的它会将你的数据翻译为 0x78563412。显然,问题就出现了!!!

网络传输一般采用 Big Endian,也被称之为网络字节序,或网络序。当两台采用不同字节序的主机通信时,在发送数据之前都必须经过字节序的转换成为网络字节序后再进行传输。

网络序和主机序

前面的大端和小端都是在说计算机自己,也被称作主机字节序。而网络序,是指网络传输采用的字节序。所幸,网络序是标准化的,即一般统一采用大端序。因此,发送网络数据之前需要将数据转换为网络序,从而避免了前面所担心的问题。而C语言也针对整型数据提供了一组接口,htonl、htons用于本地序转网络序,以及ntohl、ntohs用于网络序转本地序。

示例演示

我们通过一个例子来观察大端序和小端序,本地序和网络序的不同。

#include <stdio.h>
#include <arpa/inet.h> //htonl


void string_to_hex(char *string, unsigned int len);

int main(int argc, char  **argv){


	printf("Before the conversion\n");
	int a = 0x12345678;
	printf("a = %d\n",a );

	//强制转换取到a最低字节的地址
	string_to_hex((char *)&a,sizeof(int));

	printf("After the transformation\n");

	a = htonl(a); //转换为网络序

	printf("a = %d\n",a);

	string_to_hex((char *)&a,sizeof(int));
	
	return 0;
}


void string_to_hex(char *string, unsigned int len){

	unsigned int loop = 0;
	char *temp = string;

	if (temp == NULL){
		printf("input value is NULL\n");
		return;
	}

	for(loop = 0; loop < len; loop++){

		printf("%p:0x%2x\n", temp,*(temp));
		temp ++;
	}

}

编译运行:

在这里插入图片描述 由于本人使用的是x86系列处理器,且编译时未使用交叉编译,因此本地序为小端序。话说怎么知道呢?

小端序a低位的0x78存储在低地址,而高位的12存储在高地址,也就是说对于小端序,其低位存储在高位之前。使用htonl宏将a转为网络序(大端序)之后,a的低位存储在高位之后。

我们可以通过readelf -h endian看到:

在这里插入图片描述 更多readelf使用可以参考:Linux调试工具之readelf命令

身边的字节序

字符编码方式UTF-16、UTF-32同样面临字节序的问题,因为他们分别使用2个字节和4个字节编码Unicode字符,一旦某个值用多个字节表示,就必须要考虑存储的顺序了。

相信很多人都被UTF-8的BOM给坑过,多了这个BOM的UTF-8文件,会导致很多问题啊。比如,写的Shell脚本,内容为#! /bin/bash,在UTF-8有BOM和UTF-8无BOM的编码下,对应的16进制为:

UTF-8有BOM情况下:

在这里插入图片描述 使用UE来切换十六进制编辑模式来查看: 在这里插入图片描述

UTF-8无BOM:

在这里插入图片描述

使用UE来切换十六进制编辑模式来查看: 在这里插入图片描述

所以,有BOM的话,Shell解释器就报错啦。原因在于,解释器希望遇到#! /bin/bash,而使用UTF-8有BOM进行编码的内容会多了3个字节的EF BB BF。

对于UTF-8和UTF-8无BOM两种编码格式,我们更多的使用UTF-8无BOM。

总结

通过前面的介绍和分析,我们总结出以下几点:

C/C++语言编写的程序里数据存储顺序是跟编译平台所在的CPU相关的,而现在比较普遍的 x86 处理器是 Little Endian。不同处理器之间采用的字节序可能不同。 网络序一般统一为大端序。 多字节数据对象才需要转字节序,例如int,short等,而char不需要。 Java编写的程序则唯一采用 Big Endian 方式来存储数据 对于UTF-8和UTF-8无BOM两种编码格式,我们更多的使用UTF-8无BOM格式。

参考:字节顺序

www.zhihu.com/question/20…

songlee24.github.io/2015/05/02/…