序列化:数据跨越边界的翻译官
序列化(Serialization)用于描述RPC服务接口和数据结构。在RPC通信中,客户端和服务器之间传输的数据通常是结构化的,如调用方法、请求参数、返回值等。这些结构化数据需要通过序列化过程转换为二进制流,以便在网络中进行传输。
目前,常见的跨语言序列化编码方式包括XML、JSON和Protobuf。尽管XML曾经广泛使用,但现在已经逐渐被淘汰。JSON目前正处于其使用高峰,而Protobuf则是一种新兴并且正在快速发展的序列化方式。值得一提的是,gRPC默认选择使用Protobuf作为其序列化方式。
JSON
JSON(JavaScript Object Notation)是一种轻量级的文本数据格式,以其优秀的可读性、灵活性和跨语言兼容性而广受欢迎。由于其结构简单、规范明确,JSON在Web开发、移动应用、API通信等领域得到了广泛应用。同时,JSON还可以与其他技术和工具集成,如RESTful API、NoSQL数据库等,进一步扩展了其应用范围。
go
// 定义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具有更小的数据体积和更快的数据解析速度,这使得它在处理大量数据和高性能需求的场景中具有显著优势。
go
// 定义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个字节。
go
// 常规int32类型值为1二进制表示
0000 0000 | 0000 0000 | 0000 0000 | 0000 0001
// Varint编码int32类型值为1二进制表示
0000 0001
wire type
Protobuf将每个字段编码后从逻辑上分为三个部分。
go
<tag> <type> [<length>] <data>
其中tag 里面会包含两部分信息:字段序号(field number),字段类型(wire type)。tag,type和 length 都用 VarInts 表示。
Protobuf 在 3 版本中定义了 4 种类型 。
go
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 的方式传输。
使用 tag 的优点是不用重复传输字段名,但也因为没有字段名,所以须维护字段名和 tag 的映射关系。这个映射关系由.proto维护 。
将message通过Protobuf序列化的二进制串,与原始字段名和字段值有如下的对应关系。
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体系。这尤其适用于需要跨语言、跨团队协作以及面临复杂多变公网环境的场景。
精简的自研协议: 它们更聚焦于榨取内网环境下的极致性能潜力。通过高度定制、可扩展的报文结构设计和灵活的过程控制,自研协议能够针对特定业务场景和硬件环境进行深度优化,满足对低延迟、高吞吐的严苛要求。
这种"公网标准化"与"内网优化"并存的双轨实践,深刻体现了系统设计中的一个根本逻辑:在标准化带来的互操作性、生态繁荣与特定场景下的极致优化之间,寻求一种动态的、弹性的平衡。 技术决策的目标应是既能有力支撑当前业务的快速发展,又能为未来通信潜能的持续释放奠定坚实基础。
很高兴与你相遇!如果你喜欢本文内容,记得关注哦!