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

简介: 序列化是数据跨边界传输的“翻译官”,将结构化数据转为二进制流。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 {
   
    Int  int32  `json:"int"`
    Str  string `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 字节 
二进制流:[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具有更小的数据体积和更快的数据解析速度,这使得它在处理大量数据和高性能需求的场景中具有显著优势。

// 定义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 字节 
二进制流:[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个字节。

// 常规int32类型值为1二进制表示
0000 0000 | 0000 0000 | 0000 0000 | 0000 0001 

// Varint编码int32类型值为1二进制表示 
0000 0001

wire 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<<3)|type 的方式传输。

image.png

使用 tag 的优点是不用重复传输字段名,但也因为没有字段名,所以须维护字段名和 tag 的映射关系。这个映射关系由.proto维护 。
将message通过Protobuf序列化的二进制串,与原始字段名和字段值有如下的对应关系。

image.png

Protobuf在多个方面都展现出与JSON相比的优势。首先,Protobuf的数据更为紧凑,相较于JSON的文本格式,它可以大幅减少数据的存储和传输开销。其次,Protobuf的处理速度更快,由于采用了二进制编码,它能够更快地将二进制数据转换为内存对象。此外,Protobuf还提供了类型安全的保障,通过预先定义消息结构,确保数据的一致性和正确性。
然而,与JSON相比,Protobuf由于采用了二进制编码,Protobuf的数据在可读性方面稍逊一筹。此外,Protobuf需要预先定义消息结构,这增加了一些额外的工作量,并且在消息结构发生变化时,需要同步进行更新。
需要明确的是,序列化并非RPC协议本身,而是将RPC传输的结构化数据(如请求参数、返回值)序列化成二进制流的过程。因此,RPC协议中需要包含序列化标识,以便接收端根据序列化标识将二进制流反序列化成结构化数据。然而,像HTTP/1协议直接将文本数据转换成二进制流,因此不需要额外的序列化标识。
序列化的性能直接影响到RPC协议的性能。一个优秀的序列化编码方式应该在占用更低的内存空间的同时,保持更高的编解码效率。除了JSON和Protobuf之外,还有一些特定语言的序列化编码方式,如Java的Hessian、Kryo等,它们在特定的场景中也可以作为优秀的选择。

总结:没有银弹,只有最合适的选择
构建高效、健壮的服务通信体系,其核心在于制定一套能够有效协调跨服务、跨边界协作的规范与机制。在复杂的异构系统交互中,必须系统性地解决数据格式的统一性、信息传输的高效性以及方法定义的明确性这三大基础问题。
标准化框架(如gRPC): 它们通过整合HTTP/2的流式交互能力和ProtoBuf的统一编解码方案,构建了一个功能完备、开箱即用且具有广泛生态支持的开放RPC体系。这尤其适用于需要跨语言、跨团队协作以及面临复杂多变公网环境的场景。
精简的自研协议: 它们更聚焦于榨取内网环境下的极致性能潜力。通过高度定制、可扩展的报文结构设计和灵活的过程控制,自研协议能够针对特定业务场景和硬件环境进行深度优化,满足对低延迟、高吞吐的严苛要求。
这种“公网标准化”与“内网优化”并存的双轨实践,深刻体现了系统设计中的一个根本逻辑:在标准化带来的互操作性、生态繁荣与特定场景下的极致优化之间,寻求一种动态的、弹性的平衡。 技术决策的目标应是既能有力支撑当前业务的快速发展,又能为未来通信潜能的持续释放奠定坚实基础。

很高兴与你相遇!如果你喜欢本文内容,记得关注哦!

目录
相关文章
|
28天前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
95 1
|
28天前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
97 1
|
4月前
|
JSON 人工智能 Go
在Golang中序列化JSON字符串的教程
在Golang中,使用`json.Marshal()`可将数据结构序列化为JSON格式。若直接对JSON字符串进行序列化,会因转义字符导致错误。解决方案包括使用`[]byte`或`json.RawMessage()`来避免双引号被转义,从而正确实现JSON的序列化与反序列化。
182 7
|
5月前
|
JSON 前端开发 应用服务中间件
配置Nginx根据IP地址进行流量限制以及返回JSON格式数据的方案
最后,记得在任何生产环境部署之前,进行透彻测试以确保一切运转如预期。遵循这些战术,守卫你的网络城堡不再是难题。
223 3
|
5月前
|
XML JSON Java
go语言之JSON序列化
本文介绍了Go语言中的JSON序列化与反序列化,其操作与Java类似。需要注意的是,由于Go语言的包管理机制,变量和引入包的首字母需大写,以便其他包引用。示例代码展示了如何将`Student`结构体进行JSON序列化(返回字节数组,需转为字符串)及反序列化。此外,文章还说明了通过tag(如`json`和`xml`)指定序列化变量的重要性,以避免因包间访问限制导致反序列化失败或值为null的问题。
|
6月前
|
JSON JavaScript 前端开发
Go语言JSON 序列化与反序列化 -《Go语言实战指南》
本文介绍了 Go 语言中使用 `encoding/json` 包实现 JSON 与数据结构之间的转换。内容涵盖序列化(`Marshal`)和反序列化(`Unmarshal`),包括基本示例、结构体字段标签的使用、控制字段行为的标签(如 `omitempty` 和 `-`)、处理 `map` 和切片、嵌套结构体序列化、反序列化未知结构(使用 `map[string]interface{}`)以及 JSON 数组的解析。最后通过表格总结了序列化与反序列化的方法及类型要求,帮助开发者快速掌握 JSON 数据处理技巧。
|
5月前
|
存储 Java 编译器
说一说关于序列化/反序列化中的细节问题
我是小假 期待与你的下一次相遇 ~
|
5月前
|
JSON Java 数据库连接
|
6月前
|
存储 安全 IDE
说一说序列化与反序列化中存在的问题
本文详细解析了Java中的序列化机制,包括序列化的概念、实现方式及应用场景。通过Student类的实例演示了对象的序列化与反序列化过程,并分析了`Serializable`接口的作用以及`serialVersionUID`的重要意义。此外,文章还探讨了如何通过自定义`readObject()`方法增强序列化的安全性,以及解决可序列化单例模式中可能产生的多实例问题。最后提供了代码示例和运行结果,帮助读者深入理解序列化的原理与实践技巧。
145 2
|
12月前
|
JSON 数据格式 索引
Python中序列化/反序列化JSON格式的数据
【11月更文挑战第4天】本文介绍了 Python 中使用 `json` 模块进行序列化和反序列化的操作。序列化是指将 Python 对象(如字典、列表)转换为 JSON 字符串,主要使用 `json.dumps` 方法。示例包括基本的字典和列表序列化,以及自定义类的序列化。反序列化则是将 JSON 字符串转换回 Python 对象,使用 `json.loads` 方法。文中还提供了具体的代码示例,展示了如何处理不同类型的 Python 对象。
424 1

热门文章

最新文章