什么是 little-endian 和 big-endian
字节序(byte order)又称端序或尾序(Endianness),多字节对象都被存储为连续的字节序列,存储地址内的排列有两个通用规则:
- little-endian: 小端序,就是低位字节放在内存的起始地址,即最低有效位在最高有效位的前面。
- big-endian: 大端序,就是高位字节放在内存的起始地址,即最高有效位在最低有效位的前面。
示例
比如 16 进制数字0x12345678(10 进制为305419896)在内存中的表示形式为:
大端模式
低地址 ------------------------> 高地址0x12 | 0x34 | 0x56 | 0x7800010010 | 00110100 | 01010110 | 01111000 |
小端模式
低地址 ------------------------> 高地址0x78 | 0x56 | 0x34 | 0x1201111000 | 01010110 | 00110100 | 00010010 |
- 上面 2 者比较可见,大端模式和直接阅读数字的习惯一致。
- 这 2 个模式中,共同点就是每个字节
byte都是由8bit组成,而bit只有 2 个值,即0和1。 - 比如上面
0x78在内存中保存在一个字节中,其表现是一样的,其 8 个 bit 都是自右向左,和数学数字表示法一样,高位在左边,即01111000。
big-endian 还是 little-endian
- x86 架构的是小端模式的,所以当前大多数的 cpu 是小端模式的。
- 多数的 powerpc 架构的 cpu 是大端模式的。
- arm 架构的 cpu 可以设置使用大端还是小端模式。
- jpeg 是大端模式,而 gif 则是小端模式。
网络字节序是 tcp/ip 中规定好的一种数据表示格式,它与具体的 cpu 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节序采用big-endian排序方式。
x86 Endianness 示例
x86 是小端序而网络字节序是大端序,以下是一个 IPv4 (4 字节) 0x366410AC 的小端序示例:
0x36 0x64 0x10 0xAC---- ---- ---- ---- | | | |__ 0xAC = 172 | | |_______ 0x10 = 16 | |____________ 0x64 = 100 |_________________ 0x36 = 54 --------------------- 172.16.100.54 |
UNIX Networking Programming 书中的例子
c 语言中可以利用联合体union判断 cpu 对内存采用little-endian还是big-endian模式读写。
int main(void) { union { short s; char c[sizeof(short)]; } un; un.s = 0x0102; if (sizeof(short) == 2) { if (un.c[0] == 1 && un.c[1] == 2) { printf("big-endian\n"); } else if (un.c[0] == 2 && un.c[1] == 1) { printf("little-endian\n"); } else { printf("unknown\n"); } } else { printf("sizeof(short) = %d\n", sizeof(short)); } return 0;} |
通过类型强转进行判断
int big_endian() { int a = 0x1234; // 通过将 int 强制类型转换成 char 单字节,通过判断起始存储位置。 // 即取 b 等于 a 的低地址部分 char b = *(char *) &a; if (b == 0x12) { // 高位存储在起始地址,即为大端 return 1; } return 0;} |
网络字节序与主机字节序转换函数
htons(): host to network short,将 short 类型数据从主机字节序转换为网络字节序。ntohs(): network to host short,将 short 类型数据从网络字节序转换为主机字节序。htonl(): host to network long,将 long 类型数据从主机字节序转换为网络字节序。ntohl(): network to host long,将 long 类型数据从网络字节序转换为主机字节序。
通常,以s为后缀的函数中,s代表2个字节short,因此用于端口号转换;以l为后缀的函数中,l代表4个字节的long,因此用于 IP 地址转换。
网络字节序与主机字节序转换函数测试代码
int main(void) { unsigned short host_port = 0x1234, net_port; unsigned long host_addr = 0x12345678, net_addr; printf("0x12345678: %d\n", host_addr); // 305419896 net_port = htons(host_port); net_addr = htonl(host_addr); printf("Host ordered port: %#x\n", host_port); printf("Network ordered port: %#x\n", net_port); printf("Host ordered address: %#lx\n", host_addr); printf("Network ordered address: %#lx\n", net_addr); return 0;} |
0x12345678: 305419896
Host ordered port: 0x1234
Network ordered port: 0x3412
Host ordered address: 0x12345678
Network ordered address: 0x78563412
UTF-8 编码和 UTF-16 编码
- UTF-8 编码的规则决定了其存储与字节序没有关系,所以 UTF-8 文件头可以有
BOM(byte order mark),也可以没有,一般 linux 下面的创建的 UTF-8 文件默认是没有BOM的。 - UTF-16 编码则是用 2 个字节来保存一个字符,一个字母
a和一个中文字中都用 2 个字节保存,所以必须要知道文件保存的字节序,才能正确的读写相应的内容。 - 对于 UTF-16 编码的文件,不论是大端模式还是小端模式,存储的第一个字符都是
BOM,占 2 个字节,BOM的 unicode 码是0xFEFF,存储在大端模式 unicode 文件里是0xFEFF,存储在小端模式 unicode 文件里是0xFFFE。
java 中的字节序
java 中字节存储默认为大端模式,可以查看包含多字节字符的字符串方法String#getBytes()返回的数组内容进行确认。
下面示例中以UTF-8编码的字符串中有个多字节的字符中字,将其对应的unicode编码0100111000101101(10 进制20013)用填充到UTF-8编码规则指定的这 3 个字节中:1110**** 10****** 10******,即11100100 10111000 10101101,这 3 字节码对应的 10 进制数为[-28, -72, -83]。
C 语言中"11100100"的有符号数和无符号数转化
以第 1 个字节11100100为例,说明其数值与编码的关系,从下面 C 和 java 代码的运行结果中可以看出:
- java 中的 byte 是
有符号数,即取值范围为-128 ~ 127。 - UTF-8 编码是每个字节的
无符号数,所以数值都是大于 0 的。
int main(void) { void print_binary(char *, int); signed char i_28 = 0b11100100; unsigned char i228 = 0b11100100; printf("signed char 0b11100100 is : %d \n", i_28); // -28 printf("unsigned char 0b11100100 is : %x \n", i228); // e4 printf("unsigned char 0b10111000 is : %x \n", 0b10111000); // b8 printf("unsigned char 0b10101101 is : %x \n", 0b10101101); // ad} |
而下面 java 例子中字输出的编码就是[-28, -72, -83],因此 java 中默认的字节序是高位在前,即大端序。另外这 3 个字节在 16 进制的表现形式为[e4, b8, ad],即中字UTF-8编码:%E4%B8%AD,具体可以看下面代码的备注说明,之前我在这篇文章里用 javascript 演示了UNICODE码和UTF-8编码之间的转换关系。
使用 UTF-8 编码并使用 java 默认字节序
public void testUTF8ByteSort() throws UnsupportedEncodingException { byte[] dst; ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); // 创建字节缓冲区,java 中字节存储默认为大端模式 String abcdef = "abcde 中"; // “中”字 16 进制为: 0x4e2d ,其 unicode 码为: 20013 byteBuffer.put(abcdef.getBytes()); // default charset UTF-8 System.out.println(Arrays.toString(abcdef.getBytes())); // [97, 98, 99, 100, 101, -28, -72, -83] // 计算机中负数用补码表示: // -28 的 2 进制表示: ~(28) + 1,即 11100100,而 11100100 再转成 16 进制无符号数,即为 e4 // 同样得到 -72 对应的 16 进制 b8 // -83 对应的 16 进制 ad String b1 = Integer.toBinaryString(-28); String b0 = Integer.toBinaryString(-28 & 0xFF); System.out.println(b1); // 11111111111111111111111111100100 System.out.println(b0); // 11100100 System.out.println(Integer.parseInt(b0, 2)); // 228 System.out.println(Integer.toHexString(Integer.valueOf(b0, 2))); // e4 System.out.println(Integer.toHexString(Integer.valueOf(Integer.toBinaryString(-28 & 0xFF), 2))); // e4 // 每个 byte 的 16 进制数 System.out.println("=========================="); System.out.println(Integer.toHexString(-28 & 0xFF)); // e4 System.out.println(Integer.toHexString(-72 & 0xFF)); // b8 System.out.println(Integer.toHexString(-83 & 0xFF)); // ad System.out.println("=========================="); // 因此 "中" 字 uri encode 的编码即为: "%e4%b8%ad" String encode = URLEncoder.encode("中", Charsets.UTF_8.name()); System.out.println(encode); String decode = URLDecoder.decode("%e4%b8%ad", Charsets.UTF_8.name()); System.out.println(decode); System.out.println("=========================="); System.out.println(byteBuffer); // java.nio.HeapByteBuffer[pos=8 lim=16 cap=16] byteBuffer.flip(); // 注意 flip()之后的 pos/limit 值的变化 System.out.println(byteBuffer); // java.nio.HeapByteBuffer[pos=0 lim=8 cap=16] System.out.println("=========================="); dst = new byte[byteBuffer.limit()]; byteBuffer.get(dst); // byteBuffer 中有效的字节 System.out.println(Arrays.toString(dst)); // [97, 98, 99, 100, 101, -28, -72, -83] System.out.println(new String(dst)); // abcde 中 // 下面打印 ByteBuffer 中所有的字节内容 System.out.println(Arrays.toString(byteBuffer.array())); // [97, 98, 99, 100, 101, -28, -72, -83, 0, 0, 0, 0, 0, 0, 0, 0] System.out.println(new String(byteBuffer.array()));} |
javascript 中"-28"的无符号数 2 进制和 16 进制转化
上面的代码用 java 对-28的 16 进制无符号数的转化过程做了一个演示,下面再用 javascript 演示一下转化过程如下:
// a zero fill right shift converts it's operand to a 32 signed bit integer.var binary = (-28 >>> 0).toString(2); // coerced to uint32console.log(binary); // 11111111111111111111111111100100console.log(parseInt(binary, 2) >> 0); // -28, binary to int32var b0 = -28 & 0xFF; // -28 用 1 个字节来表示其无符号数即为 228console.log(b0); // 228console.log(b0.toString(16)); // e4encodeURIComponent("中"); // '%E4%B8%AD'decodeURIComponent("%e4%b8%ad"); // '中' |
使用 UTF-16 编码并使用 java 默认的字节序
当上述字符串使用UTF-16编码格式进行存储时,仍然是高位在前,即大端模式,而且因为 16 进制有字节序的问题,必须声明当前存储的内容的字节序是什么模式的,所以最前面有0xFEFF的 2 个字节来表示BOM。
UTF-16编码中所有的字符都是用 2 个字节来表示的,因为字母a也是用[0, 97]这 2 个字节来表示,并且高位0(0x00)在低位97(0x61)之前。因为UTF-16用2 个字节表示一个字符,那么其最多能表示1 << 16 - 1 = 65535个字符。
因为 java 中不存在无符号数,即没有C语言中unsigned类型的整数,java 中一个字节byte能表示的数值范围为-128 ~ 127,因此 1 个字节0xFE即为有符号数的-2,而0xFF则为有符号数的-1。
public void testUTF16ByteOrder() { String abcdef = "abcde 中"; // 中 16 进制为: 0x4e2d ,其 unicode 码为: 20013 // 下面这句输出为:[-2, -1, 0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 78, 45] // UTF_16 编码中每个字符占 2 个字节存储空间,“中”字的 16 进制为 0x4e2d,按大端模式,高位先存储在第 1 个字节 // 1. 即 0x4e2d 中的 4e 存储在第 1 个字节,4e 的 10 进制数即为 78,2 进制数为: 01001110 // 2. 而 0x4e2d 中的 2d 存储在第 2 个字节,2d 的 10 进制数即为 45,2 进制数为: 00101101 // 3. 内存中的形式如: 0100111000101101 System.out.println(Arrays.toString(abcdef.getBytes(Charsets.UTF_16))); // 4. 转化 0100111000101101 为 10 进制数: 20013 int charCode = Integer.parseInt("0100111000101101", 2); System.out.println(charCode); // 20013 System.out.println((int) "中".charAt(0)); // 20013} |
java 中设置字符串存储的字节序
通过设置ByteBuffer#order(ByteOrder)方法,设置其以CharBuffer使用时,控制每个char存储的字节序。如果是按byte存储,则使用默认的大端模式存储,这个方法设置就没有意义了。
public void testBigEndian() { ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); // byteBuffer.order(ByteOrder.BIG_ENDIAN); // 默认值,不设置 String abcdef = "abcde 中"; CharBuffer charBuffer = byteBuffer.asCharBuffer(); charBuffer.put(abcdef); charBuffer.rewind(); System.out.println(charBuffer.position()); // 0 System.out.println(charBuffer.limit()); // 8 System.out.println(charBuffer.capacity()); // 16 / 2 = 8; 1 个 char 占 2 个 bytes // 大端字节序输出为:[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 78, 45] System.out.println(Arrays.toString(byteBuffer.array())); System.out.println(new String(byteBuffer.array()));}public void testLittleEndian() { ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // 设置字节序 String abcdef = "abcde 中"; CharBuffer charBuffer = byteBuffer.asCharBuffer(); charBuffer.put(abcdef); charBuffer.rewind(); // 小端字节序输出:[97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 45, 78, 0, 0, 0, 0] System.out.println(Arrays.toString(byteBuffer.array())); System.out.println(new String(byteBuffer.array()));} |
关于 java 和 C 语言中的 char 类型
简单说一下这 2 个语言中都有char类型,但使用方式不一样,C 语言中的char类型:
- 只使用
1 个字节,1 个字节也就没有字节序的问题。 unsigned char,数值表示范围为0 ~ 255。signed char,有符号数的表示范围为-128 ~ 127,这个与 java 中的byte相同。
java 语言中的char类型:
- 使用
2 个字节,所以有字节序的问题,java 默认为大端序。 - 类似于
C语言的signed short int,可表示的数值范围为0 ~ 65535。 - java 中没有实现无符号数。
public void sizeOfJavaChar() { // java 中的 char 有点像 C 语言中的 signed short int,256 以内的字符实际上只要一个字节就可以 char ff = 0xFF; System.out.println((int) ff); // 255 char ff2 = 0xFFFF; // 255 以上,65536 以下,则需要 2 个字节 System.out.println((int) ff2); // 65535 // 超过 0xFFFF 就走出数值表示范围了 int i0000 = 0x10000; System.out.println(i0000); // 65536 char ff3 = (char) 0x10000; // 不强转,则已经超出 char 的数值范围了,强转就丢精度了 System.out.println((int) ff3); // 0 short s = 0x0102; System.out.println(s); // 258 System.out.println(s >> 8); // 1 高位 System.out.println(s & 0xFF); // 2 低位 System.out.println((byte) s); // 2 丢失高位} |
javascript 中的字节序
下面的 ES6 代码对同一段内存ArrayBuffer,分别建立两种视图TypedArray:
- 8 位无符号整数(Uint8Array 构造函数)。
- 16 位带无符号整数(Uint16Array 构造函数)。
ArrayBuffer是不能直接被访问的,因此需要借助TypedArray。由于两个视图对应的是同一段内存,一个视图修改底层内存,会影响到另一个视图。这个过程有点像C语言中的union联合类型。
var arrayBuffer = new ArrayBuffer(2);var u8 = new Uint8Array(arrayBuffer); // 同一段内存地址var u16 = new Uint16Array(arrayBuffer); // 同一段内存地址// Determine whether Uint16 is little-endian or big-endian.u16[0] = (1 << 8) + 2; // 即 0x0102,必须加括号,<< 左移操作符优先级低于 +console.log(u8); // [2, 1]console.log(u16); // [258]var isLittleEndian = true;if (u8[0] === 0x01 && u8[1] === 0x02) { isLittleEndian = false;}console.log("is little endian : " + isLittleEndian);u8[0] = 0xFF; // 修改内存低地址的值,查看对 u16 的影响console.log(u8); // [255, 1]console.log(u16); // [511],即 0x01FFconsole.log(0x01FF); // 低位由 0x02 改成 0xFF |
以上代码在浏览器里运行的结果如下:
Uint8Array(2) [2, 1]
Uint16Array [258]
is little endian : true
Uint8Array(2) [255, 1]
Uint16Array [511]
511
在node的REPL中的运行结果如下:
{ '0': 2, '1': 1 }
{ '0': 258 }
is little endian : true
{ '0': 255, '1': 1 }
{ '0': 511 }
511
以上 javascript 代码都是在x86的 CPU 上运行的,无论是浏览器和还是REPL,表现的结果都是小端序。
在MDN上Typed array views说明了其字节序是平台相关的,摘录部分内容如下:
Typed array views are in the native byte-order (see Endianness) of your platform. With a DataView you are able to control the byte-order.
使用 DataView 检测平台的字节序
function isLittleEndian() { var arrayBuffer = new ArrayBuffer(2); var view = new DataView(arrayBuffer); view.setInt16(0, 256, true); // 显式以 little endian 写入数据 256,即 0x0100 // 此时 arrayBuffer 里的内存布局应该是 0x00 0x01 var i16 = new Int16Array(arrayBuffer); // 如果以 little endian 读取,它就是 0x0100,即 256;以 big endian 读取,则是 0x0001,即为 1 return i16[0] === 256;} |
javascipt 中有符号数与无符号数转换
通过使用逻辑右移运算符,移动位数为 0,可以将 32 位有符号整数,转化为 32 位无符号整数。
unsigned = signed >>> 0;
通过使用左移运算符,位动位数为 0,可以将 32 位无符号整数,转化为 32 位有符号整数。
signed = unsigned << 0;
javascript 有符号数和无符号数转换测试代码
var signed, unsigned;signed = -1;unsigned = signed >>> 0;console.log("unsigned = " + unsigned); // unsigned = 4294967295signed = unsigned << 0;console.log("signed = " + signed); // signed = -1 |