字符编码-教程(4)-字节序、本地序、网络序和多字节码元

一、字节序含义


字节序,又称字节顺序,其英文为Byte-Order;另外英文中还可称为Endianness,因此也翻译为端序。

Endianness这个词,源自于1726年Jonathan Swift的名著:Gulliver’s Travels(格列佛游记)。在书中有一个童话故事,大意是指Lilliput小人国的国王下了一道指令,规定其人民在剥水煮蛋时必须从little-end(小端)开始剥。这个规定惹恼了一群觉得应该要从big-end(大端)开始剥的人。事情发展到后来演变成了一场战争。后来,支持小端的人被称为little-endian,反之则被称为big-endian(在英语中后缀“-ian”表示“xx人”之意)。

1980年,Danny Cohen在他的论文“On Holy Wars and a Plea for Peace”中,第一次使用了Big-endian和Little-endian这两个术语,最终它们成为了异构计算机系统之间进行通讯、交换数据时所要考虑的极其重要的一个问题。

(注:所谓异构是指不同架构、不同结构、不同构造等,而这里的异构计算机系统,主要指的是采用不同CPU和/或不同操作系统的计算机系统。)

字节序,具体来说,就是多字节数据(大于一个字节的数据)在计算机中存储、读取时其各个字节的排列顺序。

  • 大端序BE(Big-Endian,也称高尾端序、大尾),小端序LE(Little-Endian,也称为低尾端序、小尾)  。两者是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。如果将49写在前面,就是little endian。
  • BOM: Byte Order Mark, Unicode(后面教程会提到)规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。
  • 如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

二、字节序分类


字节序共分为三种:

  • 大端序BE(Big-Endian,也称高尾端序);
  • 小端序LE(Little-Endian,也称为低尾端序);
  • 中间序ME(Middle-Endian,也称为混合序),不太常用,本文不作介绍。

计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。

举例来说,数值0x2211使用两个字节储存:高位字节是0x22,低位字节是0x11

  • 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
  • 小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存。

同理,0x1234567的大端字节序和小端字节序的写法如下图。

a) 小端序Little-Endian(低尾端序)

就是低位字节(即小端字节)存放在内存的低地址,而高位字节(即大端字节)存放在内存的高地址。

这是最符合人的直觉思维的字节序(但却不符合人的读写习惯),因为从人的第一观感来说,低位字节的值小,对应放在内存地址也小的地方,也即内存中的低位地址;反之,高位字节的值大,对应放在内存地址大的地方,也即内存中的高位地址。

b) 大端序Big-Endian(高尾端序)

就是高位字节(即大端字节)存放在内存的低地址,低位字节(即小端字节)存放在内存的高地址。

这是最符合人平时的读写习惯的字节序(但却不符合人的直觉思维),因为不用像在Little-Endian中还需考虑字节的高位、低位与内存的高地址、低地址的对应关系,只需把数值按照人通常的书写习惯,从高位到低位的顺序直接在内存中从左到右或从上到下(下图中就是从上到下)按照由低到高的内存地址,一个字节一个字节地填充进去。

 c) 中间序Middle-Endian(混合序Mixed-Endian

混合序具有更复杂的顺序。以PDP-11为例,32位的0x0A0B0C0D被存储为:

混合序较少见,常见的多为大端序和小端序。

三、字节序起因


字节序的选择:因为历史上设计不同计算机系统的人在当时基于各自的理由和原因(这里的理由和原因网上存在着各种不同的说法,但也或许根本就没有具体理由和原因,只是设计人员的个人偏好,甚至是随意决定的),在各自计算机系统的设计上作出了不同的选择。

我一直不理解,为什么要有字节序,每次读写都要区分,多麻烦!统一使用大端字节序,不是更方便吗?

上周,我读到了一篇文章,解答了所有的疑问。而且,我发现原来的理解是错的,字节序其实很简单。

1、首先,为什么会有小端字节序?

答案是,计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。

但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

2、计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读第一个字节,再读第二个字节。

如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节。小端字节序正好相反。

理解这一点,才能理解计算机如何处理字节序。

四、字节序与编码单元(码元)


在多字节编码的字符方案中(后面会介绍比如GBK编码,UTF-16编码):

有些编码方案规定了多字节编码中前面的字节是怎么样后面的字节是怎么样(比如GBK编码),这种就叫单字节码元。【计算机读取时,只能用大端序或者小端序的一种,能够解码成功,另一种解码会失败,不会两种都能解码成功】

有些编码方案没有规定前面的字节怎么样后面的字节怎么样,只规定了这个字用了几个字节。就是多字节码元。【可以用大端序或小端序两种解码方法,会产生歧义,所以需要字节序】

总结:只有多字节码元才需要字节序。
以字节为单位就是说它是一个字节一个字节来的,utf16是以一个字一个字来的。学计算机基础的时候应该有说过字节byte、字word、双字double word(dword)之间的关系。一个字节一个字节来就没这个问题,一个字一个字来就要考虑这个字是哪个字节在前。
utf8的标准说了前面的字节是怎么样后面的字节是怎么样,gbk同理。但是utf16是“字”怎么样,这个不同。编码单元简称码元,码元有可能是单字节也有可能是多字节。

五、字节序与数据类型


实际上,int、short、long等数据类型一般是编程语言层面的概念,更进一步而言,这其实涉及到了机器硬件层面(即汇编语言)中的数据类型byte字节、word字、dword双字等在硬件中的表达与处理机制(实质上字节序跟CPU寄存器的位数、存放顺序密切相关)。具体可参看附文:《本质啊本质之一:数据类型的本质》、《寄存器与字、字节》。

【附:本质啊本质之一:数据类型的本质

CSDN博客 博主:band_of_brothers 发表于:2007-10-10 22:20

研究一个层面的问题,往往要从更深的层面找寻答案。这就如C语言与汇编、汇编与机器指令,然而终究要有个底限,这个底限以能使我们心安理得为准,就好比公理之于数学、三大定律之于宏观物理。

在这里就将机器指令作为最后的底限吧,尽管再深入下去还有微指令,但那毕竟是太机器了,可以了。以下所有从C代码编译生成汇编代码用的是命令:cl xxx..c /Fa /Ze。

类型的本质

类型这个概念,好多地方都有讲,但说实话,你真的理解吗?什么是类型?类型是一个抽象的概念还是一个真实的存在?嗯?

开始:

1、“好多相同或相似事物的综合”(辞海)。

2、X86机器的数据类型有byte、word、dword、fword、tword、qword,等等。

3、“给内存块一个明确的名字,就象邮件上的收件人一样。给其一个明确的数据类型,就好象说,邮件是一封信,还是一个包裹。”

4、类型就是一次可以操作的块的大小,就是一个单位,就像克、千克、吨一样。双字一次操作32位;字,一次操作16位;如果没有各种类型,机器只有一个类型单位——字节,那么当需要一个4字节大小的块时,就需要4次操作,而如果有双字这个类型单位,那么只需要一次操作就可以了。

5、类型,是机器层面支持的,不是软的,是硬的,有实实在在的机器码为证。

类型的反汇编

W32dasm反汇编出来的东西,可以看出不同的类型,机器码不同,说明类型是机器硬件级别支持的,不是通过软件实现的,更不是一个抽象的概念。

Opcodes上关于mov的机器码讲的更清楚:

需要说明的是,一些大的类型单位,如qword等,在mov等标准指令里是没有的,在一些特殊指令里才能用到,如浮点指令:fmul qword ptr [0067FB08] 机器码:DC0D08FB6700。】

【附:寄存器与字、字节

字节:记为byte,一个字节由8个比特(bit)组成,可以直接存在一个8位寄存器里

1 0 1 0 1 0 0 1

一个字节

字:记为word,一个字由2个字节(共16比特)组成,可以直接存在一个16位寄存器里

1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0

高位字节        低位字节

一个8位寄存器用2位十六进制数表示,只能存1个字节(1个byte类型的数据)

一个16位寄存器用4位十六进制数表示,可存1个字(1个word类型的数据)或2个字节(2个byte类型的数据)

一个32位寄存器用8位十六进制数表示,可存2个字(1个dword类型的数据或2个word类型的数据)或4个字节(4个byte类型的数据)】

六、网络字节序(网络序)


网络字节顺序(network byte order)是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。

不过,容易令人困惑的是,IP协议作为网络层协议,其面向的数据是报文,是没有字节的概念的,也就无关字节序了。因此,英文版wikipedia上说:

In fact, the Internet Protocol defines a standard big-endian network byte order. This byte order is used for all numeric values in the packet headers and by many higher level protocols and file formats that are designed for use over IP.

也就是说,IP协议里的字节序实际上是用在分组头里的数值上的,例如每个分组头会包含源IP地址和目标IP地址,在寻址和路由的时候是需要用到这些值的。

比如,4个字节的32 bit值以下面的次序传输:首先是高位的0~7bit,其次8~15bit,然后16~23bit,最后是低位的24~31bit。这种传输次序称作大端字节序。由于TCP/IP头部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。

再比如,以太网头部中2字节的“以太网帧类型”字段,表示是的后面所跟数据帧的类型。对于ARP请求或应答的以太网帧类型来说,在网络传输时,发送的顺序是以大端方式进行的:0x08,0x06。其在内存中的映象如下所示:

内存低地址

————————

0x08 — 高位字节

0x06 — 低位字节

————————

内存高地址

该字段的值为0x0806,也是以大端方式存放在内存中的。

其实,IP报文中的很多数据都是需要做字节序转换的,比如包长度、check sum校验和等,这些值大都是short(16bit)或者long(32bit)型,所以解析IP报文时也需要做网络->主机字节序转换,而生成报文字节流时则需要进行主机->网络字节序转换(计算机中的字节序被称之为主机字节序,简称主机序;相对于网络传输中的字节序被称之为网络字节序,简称网络序)。

为了进行网络字节序与主机字节序的转换,BSD sockets(Berkeley sockets)提供了四个转换的函数:htons、ntohs、htonl、ntohl,其中h是host、n是network、s是short、l是long:

htons把unsigned short类型从主机字节序转换到网络字节序

htonl把unsigned long类型从主机字节序转换到网络字节序

ntohs把unsigned short类型从网络字节序转换到主机字节序

ntohl把unsigned long类型从网络字节序转换到主机字节序

在使用little endian的系统中,这些函数会把字节序进行转换;在使用big endian类型的系统中,这些函数会定义成空宏。

Windows系统API中也提供了类似的转换函数。而在.Net中,网络字节序与主机字节序两者之间的转换,由IPAddress类的静态方法提供:HostToNetworkOrder和NetworkToHostOrder。】

七、本地序(主机序)


其实这个就是指本机操作系使用的字节序。

Intel和AMD的X86平台,以及DEC(Digital Equipment Corporation,后与Compaq合并,之后Compaq又与HP合并)采用的是Little-Endian,而像IBM、Sun的SPARC采用的就是Big-Endian。有的嵌入式平台是Big-Endian的。JAVA字节序也是Big-Endian的。

当然,这不代表所有情况。有的CPU即能工作于小端,又能工作于大端,比如ARM、Alpha、摩托罗拉的Power PC、SPARC V9、MIPS、PA-RISC和IA64等体系结构(具体情形可参考处理器手册),其字节序是可切换的,这种可切换的特性可以提高效率或者简化网络设备和软件的逻辑。

这种可切换的字节序被称为Bi-Endian(前缀“Bi-”表示“双边的、二重的、两个的”),用于硬件上意指计算机存储时具有可以使用两种不同字节序中任意一种的能力。具体这类CPU是大端还是小端,和具体设置有关。如Power PC可支持Little-Endian字节序,但其默认配置为Big-Endian字节序。

一般来说,大部分用户的操作系统,如windows、FreeBsd、Linux是Little-Endian的;少部分,如Mac OS是Big-Endian的。

具体参见下表:

七、字节序处理模拟


举例来说,处理器读入一个16位整数。如果是大端字节序,就按下面的方式转成值。

x = buf[offset] * 256 + buf[offset+1];

上面代码中,buf是整个数据块在内存中的起始地址,offset是当前正在读取的位置。第一个字节乘以256,再加上第二个字节,就是大端字节序的值,这个式子可以用逻辑运算符改写。

x = buf[offset]<<8 | buf[offset+1];

上面代码中,第一个字节左移8位(即后面添8个0),然后再与第二个字节进行或运算。

如果是小端字节序,用下面的公式转成值。

x = buf[offset+1] * 256 + buf[offset];

32位整数的求值公式也是一样的。

/* 大端字节序 */
i = (data[3]<<0) | (data[2]<<8) | (data[1]<<16) | (data[0]<<24);

/* 小端字节序 */
i = (data[0]<<0) | (data[1]<<8) | (data[2]<<16) | (data[3]<<24);

八、字节序与联通的故事

讲到这里,我们再顺便说说一个很著名的奇怪现象:当你在 windows 的记事本里新建一个文件,输入”联通”两个字之后,保存,关闭,然后再次打开,你会发现这两个字已经消失了,代之的是几个乱码!呵呵,有人说这就是联通之所以拼不过移动的原因。

其实这是因为GB2312编码与UTF8编码产生了编码冲撞的原因。

当一个软件打开一个文本时,它要做的第一件事是决定这个文本究竟是使用哪种字符集的哪种编码保存的。软件一般采用三种方式来决定文本的字符集和编码:

检测文件头标识,提示用户选择,根据一定的规则猜测

最标准的途径是检测文本最开头的几个字节,开头字节 Charset/encoding,如下表:

1 EF BB BF UTF-8 
2    
3 FF FE UTF-16/UCS-2, little endian 
4    
5 FE FF UTF-16/UCS-2, big endian 
6    
7 FF FE 00 00 UTF-32/UCS-4, little endian. 
8    
9 00 00 FE FF UTF-32/UCS-4, big-endian.

当你新建一个文本文件时,记事本的编码默认是ANSI(代表系统默认编码,在中文系统中一般是GB系列编码), 如果你在ANSI的编码输入汉字,那么他实际就是GB系列的编码方式,在这种编码下,”联通”的内码是:

1 c1 1100 0001 
2    
3 aa 1010 1010 
4    
5 cd 1100 1101 
6    
7 a8 1010 1000

注意到了吗?第一二个字节、第三四个字节的起始部分的都是”110″和”10″,正好与UTF8规则里的两字节模板是一致的,

于是当我们再次打开记事本时,记事本就误认为这是一个UTF8编码的文件,让我们把第一个字节的110和第二个字节的10去掉,我们就得到了”00001 101010″,再把各位对齐,补上前导的0,就得到了”0000 0000 0110 1010″,不好意思,这是UNICODE的006A,也就是小写的字母”j”,而之后的两字节用UTF8解码之后是0368,这个字符什么也不是。这就是只有”联通”两个字的文件没有办法在记事本里正常显示的原因。

而如果你在”联通”之后多输入几个字,其他的字的编码不见得又恰好是110和10开始的字节,这样再次打开时,记事本就不会坚持这是一个utf8编码的文件,而会用ANSI的方式解读之,这时乱码又不出现了。

造成这个问题的关键原因是:打开记事本写联通两字时,保存好时,默认是ASCII编码,打开文件时,没有BOM作为判断依据,所以系统默认 当成utf-8 解码了。记住:平时我们保存文件时 经常能看到 save as utf-8 和 save as utf-8 + BOM 两种选择。现在应该知道区别了吧。话说,Windows 记事本里直接 打联通 然后 另存为 utf-8  。等下次打开时,都没有问题的。估计是,系统偏向 utf-8 解码的原因。因为前面 ascii 编码后,系统 犹豫 用ASCII 解码还是 utf-8解码时,倾向后者,所以没有用 BOM标记的 utf-8 文件,解码也是正确的。

补充:


当读取(或写入)需要连续读取(或写入)超过一个字节数据时才需要考虑字节序问题。

“计算机的内部处理都是小端字节序”不正确……

虽然x86是小端的,不过也有很多CPU是大端的(比如powerpc)。另外小端虽然对加法器比较友好,但除法还是大端更合适,所以这种取舍其实只是一些历史问题吧……

小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
大端模式 :符号位的判定固定为第一个字节,容易判断正负。

小端模式更适合系统内部,大端模式更适合网络数据传递,加上一些历史引领的原因,
导致现在两种字节序方式并存。


参考:

http://www.ruanyifeng.com/blog/2016/11/byte-order.html

https://www.cnblogs.com/benbenalin/p/6918634.html

https://blog.csdn.net/band_of_brothers/article/details/1819228

 

字节序问题:

https://baike.baidu.com/item/%E5%AD%97%E8%8A%82%E5%BA%8F/1457160

http://imweb.io/topic/57fe263b2a25000c315a3d8a

https://www.cnblogs.com/benbenalin/p/6918634.html

Request body 问题:

https://www.cnblogs.com/chyu/p/5517788.html

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments