从JSON到Protobuf,深入序列化方案的选型与原理
序列化:数据跨越边界的翻译官序列化(Serialization)用于描述RPC服务接口和数据结构。在RPC通信中,客户端和服务器之间传输的数据通常是结构化的,如调用方法、请求参数、返回值等。这些结构化数据需要通过序列化过程转换为二进制流,以便在网络中进行传输。
目前,常见的跨语言序列化编码方式包括XML、JSON和Protobuf。尽管XML曾经广泛使用,但现在已经逐渐被淘汰。JSON目前正处于其使用高峰,而Protobuf则是一种新兴并且正在快速发展的序列化方式。值得一提的是,gRPC默认选择使用Protobuf作为其序列化方式。
JSON
JSON(JavaScript Object Notation)是一种轻量级的文本数据格式,以其优秀的可读性、灵活性和跨语言兼容性而广受欢迎。由于其结构简单、规范明确,JSON在Web开发、移动应用、API通信等领域得到了广泛应用。同时,JSON还可以与其他技术和工具集成,如RESTful API、NoSQL数据库等,进一步扩展了其应用范围。
// 定义Message 结构体
type Message struct {
Intint32`json:"int"`
Strstring `json:"str"`
Bool bool `json:"bool"`
}
// 将message通过JSON序列化
message := Message{}
message.Int = 12345
message.Str = "hello"
message.Bool = true
marshal, _ := json.Marshal(&message)
fmt.Println(fmt.Sprintf("JSON:%s ", string(marshal)))
fmt.Println(fmt.Sprintf("长度:%d 字节 ", len(marshal)))
fmt.Println(fmt.Sprintf("二进制流:%08b", marshal))
// 打印的二进制流
JSON:{"int":12345,"str":"hello","bool":true}
长度:39 字节
二进制流:假设用UTF-8编码,每个字符占用1个字节。估算上面JSON占有的内存数据。
1)字段名占用的内存空间: int (3字节)+ str (3字节)+ str (4字节)= 10字节。
2)字段值占用的内存空间:12345 (5字节)+ hello (5字节)+ true (4字节)= 14字节。需注意JSON中数值和布尔类型会被编码为文本字符串。
3)分隔符和其他符号占用的内存空间::(3字节)+ ,(2字节)+ {}(2字节)+ "(8字节)= 15字节
JSON的内存占用为:10 + 14 + 15 = 39个字节,其中有效的字段值只占14个字节。 可见JSON的内存占有比较大且效率低,这个问题的主要有如下原因。
1)非字符串编码低效:int 字段值,转成 JSON 要五个字节。 bool 字段值占了四个字节。
2)字段名信息冗余:同一个对像,只是字段值不同,每次都要传输相同的字段名。
Protobuf
Protobuf(Protocol Buffers)是由Google开发的一种高效的二进制序列化格式。它设计精巧,旨在提供一种简单、动态、可扩展且性能高效的数据序列化方案。相比于XML和JSON等其他序列化编码方式,Protobuf具有更小的数据体积和更快的数据解析速度,这使得它在处理大量数据和高性能需求的场景中具有显著优势。
// 定义Message .proto文件
message Message {
int32 int = 1;
string str = 2;
bool bool = 3;
}
// 将message通过Protobuf序列化
message := pb.Message{}
message.Int = 12345
message.Str = "hello"
message.Bool = true
marshal, _ := proto.Marshal(&message)
fmt.Println(fmt.Sprintf("长度:%d 字节 ", len(marshal)))
fmt.Println(fmt.Sprintf("二进制流:%08b", marshal))
// 打印的二进制流
长度:12 字节
二进制流:Varint
Varint是一种变长的整数类型,相较于定长的编码方式,更能节省空间。Varint使用每个字节的最高位(Most Significant Bit,MSB),记录字节读取是否结束。 如果MSB 为1 ,表示还有后序字节,一直读到 MSB 为 0 的字节为止。一个int32整型通常占据4个字节也就是32位,但使用Varint编码只需1个字节。
// 常规int32类型值为1二进制表示
0000 0000 | 0000 0000 | 0000 0000 | 0000 0001
// Varint编码int32类型值为1二进制表示
0000 0001wire type
Protobuf将每个字段编码后从逻辑上分为三个部分。
<tag> <type> [<length>] <data>其中tag 里面会包含两部分信息:字段序号(field number),字段类型(wire type)。tag,type和 length 都用 VarInts 表示。
Protobuf 在 3 版本中定义了 4 种类型 。
0 VarInt 表示int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit 表示fixed64, sfixed64, double
2 Length-delimited 表示 string, bytes, embedded messages, repeated 字段
5 32-bit 表示fixed32, sfixed32, float
由于3 和 4 表示的类型已经废弃,类型比较少,所以Protobuf 在编码时候只用了 3 bit,实际传输以 (tag
页:
[1]