序列化和反序列化

定义

  • 序列化: 将数据结构或对象转换成二进制串的过程
  • 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

序列化的原因:

  • 永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
  • 以字节流的形式使对象在网络中进行传递和接收;
  • 进程间传递对象;

典型的C/S序列化和反序列化

  • IDL(Interface description language):参与通讯的各方需要对通讯的内容需要做相关的约定。约定使用与具体开发语言、平台无关的语言来进行描述。这种语言被称为接口描述语言(IDL)
  • IDL Compiler:将 IDL 文件转换成各语言对应的动态库。
  • Stub/Skeleton Lib:负责序列化和反序列化的工作代码。Stub 是一段部署在分布式系统客户端的代码,一方面接收应用层的参数,并对其序列化后通过底层协议栈发送到服务端,另一方面接收服务端序列化后的结果数据,反序列化后交给客户端应用层;Skeleton 部署在服务端,其功能与 Stub 相反,从传输层接收序列化参数,反序列化后交给服务端应用层,并将应用层的执行结果序列化后最终传送给客户端 Stub。

ps:一般而言序列化和反序列化框架需要共享IDL 文件

典型序列化解决方案

如:XML、JSON、Protobuf、Thrift 和 Avro

JSON

注:解析 JSON 和 XML 类似,这里以 JSON 为例

JSON 数据类型:

  • string
  • number:数字,包含整型和浮点型
  • boolean:布尔型
  • null

JSON 构造:

  • key/value pairs. 类比其他语言里面的 object, struct, dictionary, hash table
  • ordered list of values. 类比其他语言的 array, vector, list, or sequence.

JSON 示例:

{
    "key1": 1,
    "key2": ["value2"]
}
复制代码

出于效率考虑,使用流的方式几乎是唯一选择,也就是解析器只从头扫描一遍JSON字符串,就完整地解析出对应的数据结构。

解析步骤:

  • 第一步:字符解析 示例如下:对于 JSON 字符串:{"name": "Mary", "age": 18} 解析结果(Token 流): { " n a m e " : " M a r y " , " a g e " : 1 8 }

  • 第二步:根据 Token 流解析为 JSON 对象/数组

Token 流
token含义
NULLnull
NUMBER数字
STRING字符串
BOOLEANtrue/false
SEP_COLON:
SEP_COMMA,
BEGIN_OBJECT{
END_OBJECT}
BEGIN_ARRAY[
END_ARRAY]
END_DOCUMENTJSON文档结束
JSON 状态机

本质上 JSON 解析器就是一个状态机。

JSON 状态机如下:

解释如下:

  • '{':期待一个 JSON object;

  • ':':期待一个 JSON object 的value;

  • ',':期待一个 JSON object 的下一组 key-value,或者一个 JSON array 的下一个元素;

  • '[':期待一个 JSON array;

  • 't':期待一个 true;

  • 'f':期待一个 false;

  • 'n':期待一个 null;

  • '"':期待一个 string;

  • 0~9:期待一个 number。

Protobuf

官网:developers.google.com/protocol-bu…

示例:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
}
复制代码
T - L - V 的数据存储方式

定义:即 Tag - Length - Value标识 - 长度(可选) - 字段值 存储方式

优点:

  • 不需要分隔符 就能 分隔开字段

  • 存储紧凑

  • 字段没有被设置字段值,那么不需要编码,相应字段在解码时才会被设置为默认值

解析原理
  1. Protocol Buffer 将 消息里的每个字段进行编码后,再利用T - L - V 存储方式 进行数据的存储,最终得到的是一个 二进制字节流

  2. Protocol Buffer对于不同数据类型 采用不同的 序列化方式,如下图:

    注:存储Varint编码数据时不需要存储字节长度 Length,所以实际上Protocol Buffer的存储方式是 T - V

Varint 编码

定义:一种变长的编码方式

编码步骤:

  1. 取出字节串末7位
    • 如果是最后一次取出,则在最高位添加0构成1个字节
    • 否则,在最高位添加1构成一个字节
  1. 通过将字节串整体往右移7位,继续从字节串的末尾选取7位,直到取完为止

  2. 将上述形成的每个字节按序拼接成一个字节串

当使用Varint解码时时,只要读取到最高位为0的字节时,就表示已经是Varint的最后一个字节

作用:值越小的数字,使用越少的字节数表示

eg:小于 128 的数字都可以用 1个字节表示,而对于其他编码, int32 类型的数字,一般需要 4个字节 表示。

图示:

编码:

左:296,右:104

解码:

不足:采用Varint编码会将负数当做很大的整数(最高位是1)处理

解决方案: Protocol Buffer 定义了 sint32 / sint64 类型表示负数,采用 Zigzag 编码(将 有符号数 转换成 无符号数),再采用 Varint编码,从而用于减少编码后的字节数

Zigzag 编码

定义:一种变长的编码方式

原理:使用 无符号数 来表示 有符号数字;

作用:使得绝对值小的数字都可以采用较少 字节 来表示;

sint 32 编码:

(n <<1) ^ (n >>31)

  1. 将二进制表示数 左移1位(左移 = 整个二进制左移,低位补0)

  2. 将二进制表示数 右移31位 首位是1的二进制(有符号数),是算数右移,即右移后左边补1 首位是0的二进制(无符号数),是逻辑左移,即右移后左边补0

  3. 将上述二者进行异或

sint 63 只需要将右移31位换为右移63位即可

注:负数的二进制 = 符号位为1,剩余的位数为 该数绝对值的原码按位取反;然后整个二进制数+1

解码:(n >>> 1) ^ -(n & 1)

注:>>> 不带符号的右移

图示:示例数字 -2

T - V 存储方式

Protocol Buffer采用 Varint & Zigzag 编码后,以 T - V 方式进行数据存储。

Tag:消息字段标识号

  • 存储了字段的标识号(field_number)和 数据类型(wire_type),即

    Tag = (field_number << 3) | wire_type
    复制代码

    field_number:对应于 .proto 文件中消息字段的标识号,表示这是消息里的第几个字段

    wire_type:只有 0~5的取值,只需要3位即可

    enum WireType { 
          WIRETYPE_VARINT = 0, 
          WIRETYPE_FIXED64 = 1, 
          WIRETYPE_LENGTH_DELIMITED = 2, 
          WIRETYPE_START_GROUP = 3, 
          WIRETYPE_END_GROUP = 4, 
          WIRETYPE_FIXED32 = 5
       };
    复制代码

  • 占用 一个字节 的长度(如果标识号超过了16,则占用多一个字节的位置)

  • 解码时,Protocol Buffer根据 Tag 将 Value 对应于消息中的 字段

eg:

message person
{ 
    required int32     id = 1;  // wire type = 0,field_number = 1 
    required string    name = 2;  // wire type = 2,field_number = 2 
}

//  如果一个Tag的二进制 = 0001 0010
// 标识号 = field_number = field_number  << 3 =右移3位 =  0000 0010 = 2
// 数据类型 = wire_type = 最低三位表示 = 010 = 2
复制代码

Value:

经过 Protocol Buffer采用Varint 或者 Zigzag编码后 的消息字段的值。

图示:

message Test
{
    required int32 id1 = 1;
    required int32 id2 = 2;
}

// 在代码中给id1 附上1个字段值:296
// 在代码中给id2 附上1个字段值:296
Test.setId1(300);
Test.setId2(296);

// 编码结果为:二进制字节流 = [8,-84,2,16, -88, 2]
复制代码

编码过程如下:

浮点数的编码

浮点数 64(32)-bit 编码方式较简单:编码后的数据具备固定大小 = 64位(8字节) / 32位(4字节)

采用T - V方式进行数据存储,同上。

Wire Type = 2时的 编码

数据存储方式: T - L - V

Tag 编码同上

Value的三种数据类型:

  • String类型

  • 嵌套消息类型(Message) 消息的 V即为 嵌套消息的字段

  • 通过packed修饰的 repeat 字段(即packed repeated fields) 防止Tag的冗余

总结

应用场景:传输数据量小 、 网络环境不稳定 的数据存储、RPC 数据交换,如:即时IM

注:大数据并不适合使用protobuf存储,主要原因是大数据中 Tag 重复使用是不必要的,解决方案见下文的 Avro

优点:

  • 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10

  • 解析速度非常快,比对应的XML快约20-100倍

  • 标准的IDL和IDL编译器,对工程师非常友好

  • 跨平台、跨语言

  • 加密性好,http抓包只能看到字节码

  • 提供了验证机制,更容易被扩展

缺点:

  • 人类不可读

  • 通用性差,主要用于内部传输

  • 自解释性差,需要借助 .proto 文件才能了解到数据结构

Thrift

官网:thrift.apache.org

Thrift 请求响应模型:

这里可以将Message和Struct类比为TCP中的首部和负载。Message中放的是传递的元信息(metadata),Struct则包含的是具体传递的数据(payload)。

Message:
  1. Name:为调用的方法名

  2. Message Type:有Call, OneWay, Reply, Exception四种,在实际传递的时候,传递的是Type ID,这四种Type对应的Type ID如下

    Call      ---> 1
    OneWay    ---> 2
    Reply     ---> 3
    Exception ---> 4
    复制代码

    其中Call、OneWay用于Request, Reply、 Exception用于Response中。

    四者的含义如下:

    • Call: 调用远程方法,并且期待对方发送响应。
    • OneWay: 调用远程方法,不期待响应。即没有步骤3,4。
    • Reply: 表明处理完成,响应正常返回。
    • Exception:表明处理出错。
  1. Sequence ID : 序列号, 有符号的四字节整数。在一个传输层的连接上所有未完成的请求必须有唯一的序列号,客户端使用序列号来处理响应的失序到达,实现请求和响应的匹配。服务端不需要检查该序列号,也不能对序列号有任何的逻辑依赖,只需要响应的时候将其原样返回即可。这里注意将Thrift序列号和我们常用的用于防止非幂等请求多次提交的unique ID区分开来。
Struct:

示例:

struct Person {
    1: required i32 age;
    2: required string name;
 }
复制代码

Thrift支持多种序列化协议,常用的有: Binary、Compact、JSON。

Binary 序列化
Message

Message 编码的两种方式:

第一种:严格编码

Binary protocol Message, strict encoding, 12+ bytes:
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
|1vvvvvvv|vvvvvvvv|unused  |00000mmm| name length          | name    | seq id    |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
复制代码
  • vvvvvvvvvvvvvvv 表示版本, an unsigned 15 bit number fixed to 1 (in binary: 000 0000 0000 0001). The leading bit is 1.

  • unused is an ignored byte.

  • mmm is the message type, an unsigned 3 bit integer. The 5 leading bits must be 0 as some clients (checked for java in 0.9.1) take the whole byte.

  • name length is the byte length of the name field, a signed 32 bit integer encoded in network (big endian) order (must be >= 0).

  • name is the method name, a UTF-8 encoded string.

  • seq id is the sequence id, a signed 32 bit integer encoded in network (big endian) order.

第二种:不严格编码

Binary protocol Message, old encoding, 9+ bytes:
+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+
| name length                       | name                |00000mmm| seq id          |
+--------+--------+--------+--------+--------+...+--------+--------+--------+--------+
复制代码

Where name length, name, mmm, seq id are as above.

Because name length must be positive (therefore the first bit is always 0), the first bit allows the receiver to see whether the strict format or the old format is used. Therefore a server and client using the different variants of the binary protocol can transparently talk with each other. However, when strict mode is enforced, the old format is rejected.

Message types 四种类型值:

  • Call: 1

  • Reply: 2

  • Exception: 3

  • Oneway: 4

Struct
类型名idl类型名占用字节数类型ID
bytebyte13
shorti1626
inti3248
boolbool12
longi64810
doubledouble84
stringstring4+N11
[]bytebinary4+N
listlist1+4+N15
setset1+4+N14
mapmap1+1+4+NX+NY13
field1+2+X
structstructN*X12
enum
union
exception

定长编码: bool, byte, short, int, long, double采用的都是固定字节数编码

struct        ::= ( field-header field-value )* stop-field
field-header  ::= field-type field-id
复制代码
  • field id the field-id, a signed 16 bit integer in big endian order.

  • field-value the encoded field value.

  • stop-field: 00000000, 标志着一条Thrift消息的结束

长度前缀编码(4+N):

+--------+----------+
|size(4) |content(N)|
+--------+----------+
复制代码

Map (1+1+4+NX+NY):

list和set的编码(1+4+N*X):

注:key 和 value 的类型是一定的

Compact 序列化

大体和Binary 序列化相同,主要在于整数类型采用了 zigzag 和 varint 压缩编码实现,Zigzag 和 Varint 见前文Protobuf 讲解。

数据示例:Person(age:18, name:yano)

生成:[8, 0, 1, 0, 0, 0, 18, 11, 0, 2, 0, 0, 0, 4, 121, 97, 110, 111, 0]

解释:

8 // 数据类型为i32
0, 1 // 字段id为1
0, 0, 0, 18 // 字段id为1(age)的值,占4个字节
11 // 数据类型为string
0, 2 // 字段id为2(name)
0, 0, 0, 4 // 字符串name的长度,占4个字节
121, 97, 110, 111 // "yano"的4个ASCII码(其实是UTF-8编码)
0 // 结束
复制代码

Avro

官网:avro.apache.org/docs/curren…

简介:Avro是Hadoop中的一个子项目,也是Apache中一个独立的项目,Avro是一个基于二进制数据传输高性能的中间件。Avro设计之初就用来支持数据密集型应用,适合于远程或本地大规模数据的存储和交换。

特点:

  • 丰富的数据结构类型;

  • 快速可压缩的二进制数据形式,对数据二进制序列化后可以节约数据存储空间和网络传输带宽;

  • 存储持久数据的文件容器;

  • 可以实现远程过程调用RPC;

  • 简单的动态语言结合功能。

Avro 依赖于模式,动态加载相关数据的模式,Avro 数据的读写操作很频繁,而这些操作使用的都是模式,这样就减少写入每个数据文件的开销,使得序列化快速而又轻巧。这种数据及其模式的自我描述方便了动态脚本语言的使用。当 Avro 数据存储到文件中时,它的模式也随之存储,这样任何程序都可以对文件进行处理。

数据结构:

关于 schema 可参考node mongoose 的使用。

存储模式:

容器文件结构:

比较和应用场景

  • JSON 适用于对性能没有极致的要求,易于调试的基于HTTP的项目,eg:Web平台;

  • PB具有跨平台、解析速度快、序列化数据体积小、扩展性高、使用简单的特点,适用于单词传输数据量小,对延迟、速度有较高要求的场景,eg: 实时通讯;

  • Avro 适用于动态语言场景和大数据传输和存储场景;

  • Thrift 是一个框架,不只是一个序列化解决方案,优势在于语言支持和相对成熟。

解析性能:

序列化之空间开销:

转载于:https://juejin.im/post/5cfa1ca86fb9a07eac05c4f4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值