1. 引言
进行网络通信时,很重要的一点,就是要把想传输给其它主机的数据,进行序列化之后,才能调用如write这样的接口,发送到网络中。
对方接收到后,解析所接收到的数据,还原为原始数据,这就是反序列化的过程。
2. 理解
如何理解序列化呢?
主机A与主机B要进行网络通信,主机A有一份数据,想通过网络,发送给B,首先,A要把原始数据,转换成一种更通用,且便于解析的格式的数据。就是要把原始数据转换为另一种格式的数据,这种格式的数据能够很方便地被其它主机,正确地解析为A最初想要发送的数据。这就是序列化。
2.1 序列化的必要性
网络通信前,为什么要序列化?
因为一台主机上的数据,它不能直接以它在当前主机中,在内存存储的格式,去直接传输给其它主机,原因如结构体的对齐规则不同、指针数据在对方主机中无法使用等。
这就使得其它主机如果接收到的是直接传递的原始数据时,无法正确地去把它解析到当前主机,所以序列化的作用就是,把我当前主机想要传输的数据,转换为一种更通用、方便解析的格式的数据,这个转换的过程就是序列化。
其它主机接收到这个转换后的数据,可以进行正确地解析,从而把其它主机真正想要传输的数据,去存放在当前主机的内存、磁盘中。
2.1.1 结构体的内存对齐规则不同
C/C++ 编译器为了提高内存访问效率,会给结构体加填充字节(padding),但填充规则不是统一的:
一、不同编译器(GCC vs MSVC)对齐规则不同;计算结构体的大小时,首先要计算出,每个成员的对齐数 。对齐数 = min(该成员的大小,编译器默认的对齐数)
比如VS下的默认对齐数是8
二、默认对齐数是可以修改的,#pragma pack(4) ,可以把默认对齐数修改为4
代码:
综上,如果不对结构体对象进行处理,而选择直接发送的话,
比如直接使用send接口向网络中发送数据:
cpp
struct Student
{
int id; // 4字节
char name[7]; // 7字节
double score; // 8字节
};
Student stu = {1001, "zhangs", 95.5};
send(fd, &stu, sizeof(stu), 0);
主机A的默认对齐数为4,此时sizeof(stu)为20;
主机B的默认对齐数为8,此时sizeof(stu)为24。
A调用recv,接收到B发来的数据,只会对前20个字节进行处理,解析出的数据,一定是错误的!
2.1.2 大、小端字节序问题
小端机:存储数值型数据时,低权值的字节,存放在低地址处。
如图:

大端机:存储数值型数据时,低权值的字节,存放在高地址处。
一些主机是大端机,一些主机是小端机。所以在内存中存储变量时,存储的格式是不同的。
网络通信为了解决这个问题,规定了网络字节序,采用大端字节序。所以向网络中发送数据前,必须要把保证数据转换为大端字节序。
如果两台主机分别是大端机和小端机,此时不加转换地直接发送原始数据,对方解析后,就是完全错误的!
2.1.3 数据类型长度不一致

而且在16位机器上,int类型 通常占用2个字节。
2.1.4 传递指针数据
指针是本地内存的地址编号 ,这个编号只在当前主机的内存空间有效,对方主机的内存地址完全独立,所以传输指针地址毫无意义。
A主机传递一个指针数据,这个指针指向的是A主机中的一段合法的、申请成功的内存,但是如果直接传递给B主机,对于B主机来说,这个指针指向的内存数据,
- 要么是无效地址(访问崩溃);
- 要么是主机B自己的内存数据,解析出一堆乱码,完全不是A主机原本想传输的数据。
而序列化要做的,就是「抛弃指针地址,传输指针指向的真实数据内容」。
2.1.5 无法适配TCP 面向字节流的特性
TCP 是 "无边界的字节流",直接 send 结构体会遇到两个问题:
一、粘包:如果连续 send 两个 Student 结构体,服务端无法区分 "第一个结构体的末尾" 和 "第二个结构体的开头";
二、拆包:如果结构体数据被 TCP 拆成两段发送,服务端只收到一半数据,直接解析会导致内存越界、程序崩溃。
而序列化时,我们可以给字节流加长度前缀 (比如[总长度(4字节)][id(4)][name(8)][score(4)]),服务端先读长度,再读对应字节数,完美解决粘包 / 拆包 ------ 直接 send 结构体完全做不到这一点。
