天天看点

大小端字节序与序列化

大端字节序

这是一种更适合人类读取数据的方式

举个例子,有如下数据:

0x12345678

高—————>低 位

按8位为一个字节逻辑(byte进行网络传输时不需要进行网络字节序转换)把上面数据拆分成4部分:

0x12   0x34   0x56   0x78

地址: 0x100 0x101 0x102 0x103

|———|—12—|—34—|—56—|—78—|——|

从左到右:低地址到高地址

从左到右:高位到低位

记忆方法:低高高低,这是一种便于人类读取的方式。像120,百位十位个位,也是从高位到低位进行读的:一百二十。

小端字节序

这是一种更适合机器读取的方式,反人类的方式。

还是如上的数据再这边的地址数据显示是这样的:

地址: 0x100 0x101 0x102 0x103

|———|—78—|—56—|—34—|—12—|——|

从左到右:低地址到高地址

从左到右:低位到高位

记忆方法:低低高高(对于大小端的记忆其实理解最为重要,理解后就可以很深刻的记住了)

计算机都是从低位开始计算的,所以计算机电路是先处理低位字节的,效率也会高些。

计算机字节序,通常都会使用小端字节序,当然有些系统例外。

网络字节序的转换

通常网络传输惯于使用大端字节序,也就是人类习惯的一种字节序。当然进行网络字节序的转换还有个目的是为了保证传输格式的统一。

因为虽然主机字节序大多数使用小端字节序,但是还是有一小部分的使用大端字节序的,为了保证不同字节序的两台机子能够进行通信,则可以都先转换为统一的网络字节序进行数据的接收和传输。

如C++中的: htonl、ntohl等,都是进行字节序的转换。

前者主机字节序到网络字节序,后者网络字节序到主机字节序

如何用代码判断大小端

#include <iostream>

// 判断大端字节序 方法一
bool IsBigEndian()
{
    int data = 0x12345678;
    char *tar = (char *)&data;
    return (tar[0] == 0x12);
}

// 判断小端字节序 方法二:利用联合体
union UEndian
{
    char a[4];
    int val;
};

bool IsLittleEndianByUnion()
{
    UEndian data;
    data.val = 1;
    return (data.a[0] == data.val);
}

int main(void)
{

    std::cout << "fun1 is big endian =======" << (int)IsBigEndian() << std::endl;
    std::cout << "fun2 is little endian =======" << (int)IsLittleEndianByUnion() << std::endl;

    return 0;
}


           

输出结果如下:

fun1 is big endian =======0
fun2 is little endian =======1
           

从如上的输出可以看出判断大端字节序返回的是false,而判断小端字节序返回的是true,由此可以证明我本地(macOS)的字节序为小端字节序。

那么字节序转换跟序列化有什么关系呢

对于这两者的概念,其实他们俩没有直接的关系,挺多人可能会搞混,先搞清楚什么是序列化?

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程

从我个人理解来看

如果学过计算机的童鞋都知道,其实计算机只能识别0跟1(一个字节就是八个0或1,对于C而言一个字节=一个字符,多个字符就形成了字符串;相比起数值啥的,他们只是多了个规定多少个字节为一个数值(语言层面上的东西,对于计算机来说它是不懂的),但他们本质是一样的,只需要把数值转换成多个字节那么计算机就可以识别了。)

目前的序列化协议其实挺多的,二进制流、protobuf、xml、json等等,这些传输方式存在是为了让变量啥的能够通过某种逻辑转换能传输到其他终端,再通过相反的逻辑进行解析,并在其他端可以使用。

总结:我们看到的形形色色的变量呀对象啥的,其实计算机是不认识的,所以这中间需要翻译,那么翻译其实就是序列化/反序列化。

还不明白也不要紧,以下是使用二进制的形式进行序列化,先来看看下面一段代码就明白了。

void Push(uint32 val)
{
    append<uint32>(htonl(val));
}

// 这里省略了一些调用
void append(const uint8 *src, size_t cnt)
{
	if (!cnt) return;
	
	if(_storage.size() < _wpos + cnt)
		_storage.resize(_wpos + cnt);
	if (_storage.size() > _wpos)
	{
		memcpy(&_storage[_wpos], src, cnt);
		_wpos += cnt;
	}
}

           

从上面的代码中我们可以看出,对于Push方法,我们在插入 val 的时候,其实是先将 val 通过htonl进行了本地字节序到网络字节序的转换(这个过程主要是防止不同端因为本地字节序的不同导致接收后数据错误),后通过append方法,将字节序转换后的值插入到storage这个数组里面(当然我这里用的std::vector),其实这个插入过程就是一个序列化的过程。

如有说的不对的地方,欢迎批评指错~

继续阅读