从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体系。这尤其适用于需要跨语言、跨团队协作以及面临复杂多变公网环境的场景。
精简的自研协议: 它们更聚焦于榨取内网环境下的极致性能潜力。通过高度定制、可扩展的报文结构设计和灵活的过程控制,自研协议能够针对特定业务场景和硬件环境进行深度优化,满足对低延迟、高吞吐的严苛要求。
这种“公网标准化”与“内网优化”并存的双轨实践,深刻体现了系统设计中的一个根本逻辑:在标准化带来的互操作性、生态繁荣与特定场景下的极致优化之间,寻求一种动态的、弹性的平衡。 技术决策的目标应是既能有力支撑当前业务的快速发展,又能为未来通信潜能的持续释放奠定坚实基础。

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

目录
相关文章
|
2天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1072 0
|
11天前
|
人工智能 运维 安全
|
9天前
|
人工智能 测试技术 API
智能体(AI Agent)搭建全攻略:从概念到实践的终极指南
在人工智能浪潮中,智能体(AI Agent)正成为变革性技术。它们具备自主决策、环境感知、任务执行等能力,广泛应用于日常任务与商业流程。本文详解智能体概念、架构及七步搭建指南,助你打造专属智能体,迎接智能自动化新时代。
|
2天前
|
弹性计算 Kubernetes jenkins
如何在 ECS/EKS 集群中有效使用 Jenkins
本文探讨了如何将 Jenkins 与 AWS ECS 和 EKS 集群集成,以构建高效、灵活且具备自动扩缩容能力的 CI/CD 流水线,提升软件交付效率并优化资源成本。
262 0
|
9天前
|
人工智能 异构计算
敬请锁定《C位面对面》,洞察通用计算如何在AI时代持续赋能企业创新,助力业务发展!
敬请锁定《C位面对面》,洞察通用计算如何在AI时代持续赋能企业创新,助力业务发展!
|
10天前
|
机器学习/深度学习 人工智能 自然语言处理
B站开源IndexTTS2,用极致表现力颠覆听觉体验
在语音合成技术不断演进的背景下,早期版本的IndexTTS虽然在多场景应用中展现出良好的表现,但在情感表达的细腻度与时长控制的精准性方面仍存在提升空间。为了解决这些问题,并进一步推动零样本语音合成在实际场景中的落地能力,B站语音团队对模型架构与训练策略进行了深度优化,推出了全新一代语音合成模型——IndexTTS2 。
754 23
|
2天前
|
缓存 供应链 监控
VVIC seller_search 排行榜搜索接口深度分析及 Python 实现
VVIC搜款网seller_search接口提供服装批发市场的商品及商家排行榜数据,涵盖热销榜、销量排名、类目趋势等,支持多维度筛选与数据分析,助力选品决策、竞品分析与市场预测,为服装供应链提供有力数据支撑。
|
1天前
|
缓存 监控 API
Amazon item_review 商品评论接口深度分析及 Python 实现
亚马逊商品评论接口(item_review)可获取用户评分、评论内容及时间等数据,支持多维度筛选与分页调用,结合Python实现情感分析、关键词提取与可视化,助力竞品分析、产品优化与市场决策。