什么是序列化 - 弦外之音

/ 0评 / 0

本书 《网络协议栈入门》 采用的代码是 基于 linux 内核 4.4.4 版本的。linux 内核源码下载地址: mirrors.edge.kernel.org


我自己做了多年的PHP 开发,序列化函数 serialize() 也经常用,我只是知道把一个 php 变量存储进去文件,需要先序列化,就能存了。

但是我一直没去了解 序列化的原理。

最近在看协议实现,C 语言是可以直接把 一个 int num = 0x11223344 的变量,直接从内存写到 文件 a.txt 存储。int 占 4个字节,如果是本机读取,大小端一致,就可以读 文件 a.txt 4个字节,然后内存拷贝给 int num 使用。代码如下:

//write.c 写入数据文件。
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
    int fd = open("a.txt",O_RDWR|O_CREAT);
    if( -1 == fd ){
      perror("Error:"),exit(-1);
    }
    printf("打开文件成功 fd = %d \r\n",fd);
​
    int age = 0x11223344;
​
    int write_num = write(fd,&age,sizeof(int));
    return 0;
}

上面的代码可以用 qt creator + mingw32 运行,或者在 linux 下用 gcc 编译,编译命令如下:

# 生成 .o 文件
gcc -c write.c
# 把 .o 文件链接 成 可执行文件,这里只有一个 .o
gcc -o write write.o

运行上面的 write 程序之后,会生成 a.txt,我们用 notepad++ 32位 的16进制插件打开 a.txt 查看他的内容,如下:

可以看到,数据是逆向排列的,所以我的电脑 是小端序,字节序相关知识请看《网络通信中的字节序》。

这样,就成功把一个 变量存储 进去文件里面了。那是不是用另一个程序就能把这个 文件的内容拿出来?

是的,代码如下:

//read.c 读取数据文件。
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
   int fd = open("a.txt",O_RDONLY);
    if( -1 == fd ){
      perror("Error:"),exit(-1);
    }
    printf("打开文件成功 fd = %d \r\n",fd);
​
    int age = 0;
    printf("age 初始值是 0x%x \r\n",age);
    int read_num = read(fd,&age,sizeof(int));
    printf("读取数据 %d 字节, age 等于 0x%x \r\n",read_num,age);
    return 0;
}

read 程序的运行结果如下:

可以看到,是可以正常读取的,没有问题,但是由于不同的电脑设备有大小端区别,不同的编译器在处理一些复杂结构,例如 struct 的时候,内存对齐等原因的影响,导致上面的存储的方式跨平台不行。C语言结构体内存对齐知识请看《C语言内存对齐详解

上面这样存,如果把 a.txt 拿到大端的机器就解析错误了。因为大端序机器的 解析 是跟小端相反的。



所以为了解决 大小端字节序,不同编译器的差异,内存对齐 等问题,就需要用到 序列化技术,本文就来研究一下 序列化的 原理,为什么用了序列化就能解决大小端问题,一个简单的 int 序列化 如何用 C 语言实现,这些都是本文的重点。


序列化 技术 其实是一种与编程语言关联比较 深的技术,

例如 phpserialize()$a = 0x11223344 序列化之后,变成字符串,i:287454020; ,但这种格式语法是 PHP 自己的。

JavaScript 用的是 JSON.parse()php 也有一个 json_encode() 函数,所以 PHPJavaScript 可以用 json 格式来交换数据通信。

所以 json 的跨语言特性比较好。相应的,还有 xml 也是序列化的一种语法,还有 JDK(不支持跨语言),HessianKryo(不支持跨语言),ThriftProtobufFST

跨语言的原理,我自己也不太清楚,这个应该是程序语言设计的领域知识。

所以,可以看出来,序列化 实际上 就是一种 描述 数据类型的 存储方式的 一种语法。每种编程语言都有自己的特定的数据类型,所以 如果某种 序列化的语法 可以解析 成 PHP 的对象,也可以解析 成 JAVA 的对象,那这种 序列化 的语法 就是 跨语言的。

跨语言的序列化 实现太复杂,本文主要实现一个 C语言的 序列化语法。

就拿 php 的 的 serialize() 来举例演示。php 是C 语言写的,所以C 语言也能实现一个 serialize(),如下:

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
char* serialize(int num){
    char str[] = "i:287454020;";
    int len = strlen(str)+1;
    char* str2 = malloc(len);
    memcpy(str2,str,len);
    return str2;
}
int main()
{
​
    int fd = open("a.txt",O_RDWR|O_CREAT);
    if( -1 == fd ){
      perror("Error:"),exit(-1);
    }
    printf("打开文件成功 fd = %d \r\n",fd);
​
    int num = 0x11223344;
    char* str = serialize(num);
​
    int write_num = write(fd,str,strlen(str));
    close(fd);
    return 0;
​
}

上面的 serialize() 我是用伪代码实现的,返回一个字符串。然后写进去文件里面。打开 a.txt 查看内容,如下:

可以看到,存储的内容是 ASCII码 ,也就是存储的是 字符串对应的 二进制 ASCII码,这种方式 叫字符串存储,虽然最后写到硬盘还是二进制 的 ASCII码。

那为什么用字符串 存储就能解决大小端问题?接下来 就是精髓。

什么是大小端问题?多字节的数据类型 在不同 设备的 内存数据排列顺序不一样,怎么解决?这里注意,只有多字节数据类型才有大小端问题。

如果,我只存 char 进去文件,char 是单字节,高位是他,低位也是他。就不存在什么大小端字节的问题。

字符串就是 char 存储的,一个一个char 连起来,所以 字符串是单字节存储,不存在高低位。

所以 字符串的存储方式,实际上 就是用 单字节数据类型 来描述 多字节数据类型。这个 描述 就是序列化的语法。

然后再看看 怎么解析 序列化的 语法,这个解析的过程 也叫 parse,代码如下:

C语言代码逻辑
1,把 文件i:287454020 读进来 char[] 数组,结束符 是";",所以遇到 ";" 就停止读取。
2, 对 字符串 进行 parse,读第一个字节,发现是 i ,那就说明后面的 字符串是一个 int 的数据类型。
3,创建 一个 int 变量, int num;
4, 调用 atoi 函数即可把字符串 “287454020”转成 int。
5,int num = atoi(“287454020”) 

这样就完成一个 parse 了。所以,序列化技术 如果 是基于字符串实现的,那就是用 单字节 来描述 多字节数据。这样就能解决大小端问题。


PHP 的 serialize()JSONXML,这些 都是基于字符串实现的序列化,只需要对字符串进行 parse,就能还原回去之前的数据结构。

Google 的 Protobuf 序列化技术 不是 用字符串存储实现的,他这个东西我用过一下,序列化之后是二进制存储,不是字符串的ASII码的二进制。

具体 Protobuf 序列化技术 是如何实现的,请看《protobuf编码原理》


在UDP TCP 编程中,解决大小端问题 有两种做法。

1,规定 TCP body,UDP body 里面的各个数据字段,占多少字节,然后确定是大端还是小端排列。这个是设计一个新的协议的做法。

2,TCP 通常会有粘包问题,会设计一个4个字节的size,size规定是大端排序,然后 size 后面的数据序列化 之后再 传输。

然后对面的TCP先读 4个字节的size,把大端字节序转成主机字节序,拿到后面的数据长度之后,再循环读取数据,读够数据之后再反序列化。

序列化的方式比较容易扩展,加字段非常方便。


相关阅读:

  1. 《c语言序列化和反序列化》
  2. 《序列化理解起来很简单》
  3. 《C语言序列化库 tpl》

由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。QQ:2338195090。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注