理解计算机中的字节序

1,707 阅读4分钟

在学习 Socket 编程前,我们需要了解另一个计算机基础知识:字节序(Endianness),字面意思不难理解就是字节的顺序。看了那么多关于字节序的说明,还是比较喜欢维基百科中关于它的定义:"在计算机中,指电脑内存中或在数字通信链路中,组成多字节的字(word)内部字节的排列顺序。" 原文如下:

In computing, endianness is the order or sequence of bytes of a word of digital data in computer memory.

上面的维基给出的中文定义我稍加了修饰,主要想表达说明的是,CPU 通过数据总线读取数据是以字长(word)而不是字节(byte)为单位,在32位的处理器系统中,一个字的长度就是4个字节(1个字 = 32/8 = 4字节)。

那么问题来了。如果这4字节表示的是一个比较大的整数 int,那么这4个字节内部每一字节谁先谁后呢?我们要怎么去解析这4字节才能得到正确的数值呢?

所以说 聊字节序其实是和多字节场景有关 ,如果存储数据都是单字节比如 UTF-8 下的数字、字符都是1字节表示的。那这种场景下字节序其实没什么意义。

大端、小端

多字节数据既然有字节顺序问题,顺序又分为两种:大端(Big Endian)和小端 (Little Endian),大端模式指的是数据在内存中:低地址存储的是高位数据,然后再是低位数据。而小端模式则相反:低地址存储低位数据,然后再是高位数据。

Big-Endian.svgLittle-Endian.svg

如上图的 0x0A0B0C0D 它是16进制表示的,每俩字符代表一字节,大端模式下,先放 0A,然后再是 0B ...

大端模式符合人类的阅读习惯,从左至右一次进入;且符号位放在第一位,所以判断一个数是正数还是负数效率会很高,但逻辑运算会比小端模式低效。

大端模式的劣势正好是小端模式的优势,小端模式下,强制转换类型时不需要调整字节内容,直接取低位数据值,所以转换效率高,如把:int 类型转换为 short 类型数据。

主机字节序、网络字节序

在涉及网络通信时会会有这俩概念主机字节序(host byte order)和网络字节序(network byte order)。

主机字节序说白了,就是在这台机器本地数据的字节顺序,而网络字节序是指在和其他设备进行网络通信时,数据交互采用的字节序,且规定了这个顺序是大端模式。

所以不管你本地采用的是什么顺序,你要经过我的地盘,我接收的数据是大端方式的,你得按照我的方式来组织数据,就是这么任性。

b9fb09a0346b425ebfa3e13fa77fc1e6.jpeg

所以在使用Socket接口时,需要使用相关转换函数把数据转位大端模式,例如端口号:

// 使用htons函数转为网络字节序, htons 意思是:host-to-network short
serv_addr.sin_port = htons("80");

如何判断本地字节

如果想快速查看机器cpu是大端还是小端模式,可以使用 lscpu 命令:

lscpu

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian

一般来说压根儿不用关心本机字节序是啥,但好奇心害死猫,《unix高级编程》中使用一个联合体来实现,我想本质其实就是把一个整型类型(int)转为一个字符数组(char[]),再取字符数组中的第一个字符来比较是存的高位值还是低位值,所以我直接把一个整型转为字符数据:

#include <stdio.h>
#include <netinet/in.h>

int main() {
    unsigned int i = 0x0a0b;
    unsigned char *p = (unsigned char *) &i;
    if (p[0] == 0x0a) { // 判断第一个字节值
        printf("before: Big Endian, val: %02x \n", p[0]);
    } else {
        printf("before: Little Endian val: %02x\n", p[0]);
    }
  
    // 强制转换为网络序大端模式
    uint16_t rel = htons(i);
    p = (unsigned char *) &rel;
    if (p[0] == 0x0a) {
        printf("after: Big Endian val: %02x\n", p[0]);
    } else {
        printf("after: Little Endian val: %02x\n", p[0]);
    }
    return 0;
}

输出结果:

before: Little Endian val: 0b
after: Big Endian val: 0a