在现代分布式系统和网络应用中,序列化技术扮演着至关重要的角色。简单来说,序列化就是将内存中的数据结构或对象转换为一种可以存储或传输的格式,而反序列化则是将这个格式重新还原为原始数据的过程。
为什么需要序列化? 想象一下,当我们需要通过网络将数据从一台计算机发送到另一台计算机时,或者需要将数据持久化存储到磁盘上时,内存中的对象是无法直接传输或存储的。序列化技术就像是一个"翻译官",将对象转换为通用的中间格式,使得数据可以在不同系统、不同语言之间自由流动。
序列化协议专注于解决数据的结构化表示问题。它的主要职责包括:
常见的序列化协议包括:
传输协议则专注于解决数据的传输问题。它的主要职责包括:
常见的传输协议包括:
序列化协议和传输协议是解耦的,它们可以自由组合。例如:
这种解耦设计带来了极大的灵活性,使得开发者可以根据具体场景选择最适合的组合。
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,具有以下特点:
让我们通过一个Java示例来深入理解JSON序列化的底层原理:
java复制Person person = new Person("Alice", 25);
String json = objectMapper.writeValueAsString(person);
底层实现步骤:
反序列化过程同样值得关注:
java复制String json = "{\"name\":\"Alice\",\"age\":25}";
Person person = objectMapper.readValue(json, Person.class);
底层实现步骤:
优点:
缺点:
Protocol Buffers(简称Protobuf)是Google开发的一种语言中立、平台中立、可扩展的序列化机制。它的核心特点包括:
proto复制syntax = "proto3";
package cmm.proto;
option java_package = "cmm.proto";
option java_outer_classname = "DemoProto";
message Demo {
int32 id = 1;
string name = 3;
}
使用protoc编译器生成Java类:
bash复制protoc -I=. --java_out=./cmm/proto demo.proto
java复制// 构建对象
DemoProto.Demo.Builder demo = DemoProto.Demo.newBuilder();
demo.setId(100).setName("曹先生");
DemoProto.Demo build = demo.build();
// 序列化
byte[] bytes = build.toByteArray();
// 反序列化
DemoProto.Demo demo1 = DemoProto.Demo.parseFrom(bytes);
Protobuf采用Tag-Length-Value(TLV)格式进行编码:
Protobuf使用Varint编码来压缩整数:
Protobuf不存储字段名,而是使用字段编号:
Protobuf生成的Java类包含以下核心部分:
Protobuf生成的message类是不可变的,这种设计带来了以下好处:
由于message类是不可变的,Protobuf使用Builder模式来构建对象:
java复制DemoProto.Demo.Builder demo = DemoProto.Demo.newBuilder();
demo.setId(100).setName("曹先生");
DemoProto.Demo build = demo.build();
这种模式既保证了对象的不可变性,又提供了灵活的构建方式。
在序列化前,Protobuf会先计算需要的字节数:
java复制int size = 0;
if ((bitField0_ & 0x00000001) != 0) {
size += CodedOutputStream.computeInt32Size(1, id_);
}
if ((bitField0_ & 0x00000002) != 0) {
size += CodedOutputStream.computeStringSize(2, name_);
}
序列化过程将数据写入字节数组:
java复制if ((bitField0_ & 0x00000001) != 0) {
output.writeInt32(1, id_);
}
if ((bitField0_ & 0x00000002) != 0) {
output.writeString(2, name_);
}
反序列化过程解析二进制数据:
Protobuf的二进制编码通常比JSON小50%以上,特别是在以下场景:
| 测试指标 | JSON | Protobuf |
|---|---|---|
| 序列化速度 | 较慢 | 快5-10倍 |
| 反序列化速度 | 较慢 | 快5-10倍 |
| CPU使用率 | 较高 | 较低 |
| 内存占用 | 较高 | 较低 |
适合使用JSON的场景:
适合使用Protobuf的场景:
JSON的优势:
Protobuf的优势:
问题1:字段添加后旧客户端无法识别
问题2:数据类型不匹配
问题3:性能不如预期
Protobuf支持通过扩展机制在不修改原始消息定义的情况下添加新字段:
proto复制message BaseMessage {
extensions 100 to 199;
}
extend BaseMessage {
optional int32 extension_field = 100;
}
Oneof允许在多个字段中同时只有一个会被设置:
proto复制message SampleMessage {
oneof test_oneof {
string name = 1;
int32 id = 2;
}
}
Protobuf支持map类型,方便表示键值对:
proto复制message Product {
map<string, string> attributes = 1;
}
gRPC使用Protobuf作为默认的接口定义语言和数据序列化格式:
proto复制service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
在实际项目中,选择序列化协议需要考虑以下因素:
对于大多数现代分布式系统,我建议:
Protobuf特别适合以下场景:
JSON则更适合:
无论选择哪种技术,理解其底层原理都能帮助我们更好地使用它们,并在遇到问题时能够快速定位和解决。