找回密码
 立即注册
首页 业界区 业界 从JSON到Protobuf,深入序列化方案的选型与原理 ...

从JSON到Protobuf,深入序列化方案的选型与原理

左优扬 昨天 10:10
序列化:数据跨越边界的翻译官
序列化(Serialization)用于描述RPC服务接口和数据结构。在RPC通信中,客户端和服务器之间传输的数据通常是结构化的,如调用方法、请求参数、返回值等。这些结构化数据需要通过序列化过程转换为二进制流,以便在网络中进行传输。
目前,常见的跨语言序列化编码方式包括XML、JSON和Protobuf。尽管XML曾经广泛使用,但现在已经逐渐被淘汰。JSON目前正处于其使用高峰,而Protobuf则是一种新兴并且正在快速发展的序列化方式。值得一提的是,gRPC默认选择使用Protobuf作为其序列化方式。
JSON
JSON(JavaScript Object Notation)是一种轻量级的文本数据格式,以其优秀的可读性、灵活性和跨语言兼容性而广受欢迎。由于其结构简单、规范明确,JSON在Web开发、移动应用、API通信等领域得到了广泛应用。同时,JSON还可以与其他技术和工具集成,如RESTful API、NoSQL数据库等,进一步扩展了其应用范围。
  1. // 定义Message 结构体
  2. type Message struct {
  3.         Int  int32  `json:"int"`
  4.         Str  string `json:"str"`
  5.         Bool bool   `json:"bool"`
  6. }
  7. // 将message通过JSON序列化
  8. message := Message{}
  9. message.Int = 12345
  10. message.Str = "hello"
  11. message.Bool = true
  12. marshal, _ := json.Marshal(&message)
  13. fmt.Println(fmt.Sprintf("JSON:%s ", string(marshal)))
  14. fmt.Println(fmt.Sprintf("长度:%d 字节 ", len(marshal)))
  15. fmt.Println(fmt.Sprintf("二进制流:%08b", marshal))
  16. // 打印的二进制流
  17. JSON:{"int":12345,"str":"hello","bool":true}
  18. 长度:39 字节
  19. 二进制流:[01111011 00100010 01001001 01101110 01110100 00100010 00111010 00110001 00110010 00110011 00110100 00110101 00101100 00100010 01010011 01110100 01110010 00100010 00111010 00100010 01101000 011 01101100 01101100 01101111 00100010 00101100 00100010 01000010 01101111 01101111 01101100 00100010 00111010 01110100 01110010 01110101 01100101 01111101]
复制代码
假设用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具有更小的数据体积和更快的数据解析速度,这使得它在处理大量数据和高性能需求的场景中具有显著优势。
  1. // 定义Message .proto文件
  2. message Message {
  3.   int32 int = 1;
  4.   string str = 2;
  5.   bool bool = 3;
  6. }
  7. // 将message通过Protobuf序列化
  8. message := pb.Message{}
  9. message.Int = 12345
  10. message.Str = "hello"
  11. message.Bool = true
  12. marshal, _ := proto.Marshal(&message)
  13. fmt.Println(fmt.Sprintf("长度:%d 字节 ", len(marshal)))
  14. fmt.Println(fmt.Sprintf("二进制流:%08b", marshal))
  15. // 打印的二进制流
  16. 长度:12 字节
  17. 二进制流:[00001000 10111001 01100000 00010010 00000101 01101000 01100101 01101100 01101100 01101111 00011000 00000001]
复制代码
Varint
Varint是一种变长的整数类型,相较于定长的编码方式,更能节省空间。Varint使用每个字节的最高位(Most Significant Bit,MSB),记录字节读取是否结束。 如果MSB 为1 ,表示还有后序字节,一直读到 MSB 为 0 的字节为止。一个int32整型通常占据4个字节也就是32位,但使用Varint编码只需1个字节。
  1. // 常规int32类型值为1二进制表示
  2. 0000 0000 | 0000 0000 | 0000 0000 | 0000 0001
  3. // Varint编码int32类型值为1二进制表示
  4. 0000 0001
复制代码
wire type
Protobuf将每个字段编码后从逻辑上分为三个部分。
  1. <tag> <type> [<length>] <data>
复制代码
其中tag 里面会包含两部分信息:字段序号(field number),字段类型(wire type)。tag,type和 length 都用 VarInts 表示。
Protobuf 在 3 版本中定义了 4 种类型 。
  1. 0 VarInt 表示int32, int64, uint32, uint64, sint32, sint64, bool, enum
  2. 1 64-bit 表示fixed64, sfixed64, double
  3. 2 Length-delimited 表示 string, bytes, embedded messages, repeated 字段
  4. 5 32-bit 表示fixed32, sfixed32, float
复制代码
由于3 和 4 表示的类型已经废弃,类型比较少,所以Protobuf 在编码时候只用了 3 bit,实际传输以 (tag

相关推荐

您需要登录后才可以回帖 登录 | 立即注册