通信协议-序列化
序列化的含义
序列化(serialization)就是将对象序列化为二进制形式(字节数组),一般也将序列化称为编码(Encode),主要用于网络传输、数据持久化等;
反序列化(deserialization)则是将从网络、磁盘等读取的字节数组还原成原始对象,以便后续业务的进行,一般也将反序列化称为解码(Decode),主要用于网络传输对象的解码,以便完成远程调用。
序列化协议的需求
- 通用性
- 跨平台、跨语言
- 流行性
- 可读性
对于序列化后的二进制串,需要同时编写反序列化程序来验证序列化的正确性,序列化后的数据如果具备人眼可读性,则会大大降低调试的成本; - 兼容性
业务需求变更会造成数据结构的改变,序列化需要支持自动增加新的业务字段、修改数据类型,而不影响老的服务。
序列化的发展
- 紧凑模式
struct userbase
{
unsigned short cmd;
unsigned char gender;
char name[8];
}
除了数据本身外,没有一点额外冗余信息
- 可扩展性
struct userbase
{
unsigned short cmd;
unsigned char gender;
unsigned int birthday;//新加字段
char name[8];
}
无法区分字段
struct userbase
{
unsigned short version;// 增加版本号控制
unsigned short cmd;
unsigned char gender;
unsigned int birthday;
char name[8];
}
- 进一步扩展
每加一次字段都要维护一个版本号,难以维护
struct userbase
{
1 unsigned short version;
2 unsigned short cmd;
3 unsigned char gender;
4 unsigned int birthday;
5 char name[8];
}
序列化时增加一个额外的tag来记录字段
- TLV编码
字段编码后结构
支持字段的嵌套
通过TLV编码后的结构
没有定义数据类型,会造成空间浪费 - TTLV编码
增加一个type字段定义数据类型,控制value长度
由于不同语言之间的数据类型有不同,需要制定一个通用的type规范以满足跨语言特性
但是这个规范只支持此种协议,如果定义了新的序列化协议,就要重新编写、调试编解码代码,需要一种中间语言来支持不同协议
- IDL语言
IDL(Interface description language),接口描述语言。
IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流
数据类型:基本数据类型、浮点数类型、字符和超大字符类型、逻辑类型、八进制类型、any数据类型(通用类型)
常见的序列化协议
XML
引入依赖
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.4</version>
</dependency>
package cn.com.test.io;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
import java.io.IOException;
public class XmlSerializeDeserializeMain {
/**
* 将Java对象序列化成XML格式
* @param employee
* @return
* @throws IOException
*/
public static String serialize(Employee employee){
// 将employee对象序列化为XML
XStream xStream = new XStream(new DomDriver());
// 设置employee类的别名
xStream.alias("employee", Employee.class);
String personXml = xStream.toXML(employee);
return personXml;
}
/**
* 将XML反序列化还原为Java对象
* @param personXml
* @return
*/
public static Employee deserialize(String personXml) {
// 将employee对象序列化为XML
XStream xStream = new XStream(new DomDriver());
Employee employee = (Employee) xStream.fromXML(personXml);
return employee;
}
public static void main(String [] args) {
Employee employee = new Employee();
employee.setEmployeeId(1);
employee.setEmployeeName("Czl");
employee.setDepartment("研发部");
// 序列化
String serialize = serialize(employee);
System.out.println(serialize);
// 反序列化
Employee deserialize = deserialize(serialize);
System.out.println(deserialize.toString());
}
}
package cn.com.test.io;
import java.io.Serializable;
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private int employeeId;
private String employeeName;
/**
* 使用transient关键字,表示该字段不序列化
*/
private transient String department;
public int getEmployeeId() {
return employeeId;
}
public void setEmployeeId(int employeeId) {
this.employeeId = employeeId;
}
public String getEmployeeName() {
return employeeName;
}
public void setEmployeeName(String employeeName) {
this.employeeName = employeeName;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
@Override
public String toString() {
return "Employee{" +
"employeeId=" + employeeId +
", employeeName='" + employeeName + '\'' +
", department='" + department + '\'' +
'}';
}
}
XML所具有的人眼可读(Human-readable)特性使得其具有出众的可调试性,互联网带宽的日益剧增也大大弥补了其空间开销大(Verbose)的缺点。对于在公司之间传输数据量相对小或者实时性要求相对低(例如秒级别)的服务是一个好的选择。
XML的额外空间开销大,序列化之后的数据量剧增。对于数据量巨大序列持久化应用常景,意味着巨大的内存和磁盘开销,不太适合XML。另外,XML的序列化和反序列化的空间和时间开销都比较大,对于对性能要求在ms级别的服务,不推荐使用。
JSON
JSON起源于弱类型语言Javascript, 它的产生来自于一种称之为”Associative array”的概念,其本质是就是采用”Attribute-value”的方式来描述对象。
- fastjson
// 将Java对象序列化为Json字符串
String objectToJson = JSON.toJSONString(initUser());
System.out.println(objectToJson);
// 将Json字符串反序列化为Java对象
User user = JSON.parseObject(objectToJson, User.class);
System.out.println(user);
- jackson
ObjectMapper objectMapper = new ObjectMapper();
// 将Java对象序列化为Json字符串
String objectToJson = objectMapper.writeValueAsString(initUser());
System.out.println(objectToJson);
// 将Json字符串反序列化为Java对象
User user = objectMapper.readValue(objectToJson, User.class);
System.out.println(user);
- Gson
Gson gson = new GsonBuilder().create();
// 将Java对象序列化为Json字符串
String objectToJson = gson.toJson(initUser());
System.out.println(objectToJson);
// 将Json字符串反序列化为Java对象
User user = gson.fromJson(objectToJson, User.class);
System.out.println(user);
fastjson采用独特的“假定有序快速匹配算法”(?),性能最优,但是在一些复杂的bean转换上会出一些问题,而且由于阿里自定义的json转换算法存在漏洞(虽然目前已知问题已修复),可能会造成安全性问题。
fastjson漏洞详情
Gson拥有最全的JSON解析功能,无依赖,但是性能上略有不足
如果对性能和精确度都有要求,可以在bean转JSON时使用Gson,JSON转bean时使用fastjson
JSON 格式亲民,人眼可读,有良好的可扩展性和兼容性,相对于xml协议数据量较小,解析速度快。但是序列化的额外空间还是很大,不适合大数据量服务或持久化。
Thrift
Thrift是Facebook开源提供的一个高性能,轻量级RPC服务框架,满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。
先定义一个.thrift文件
struct Person {
1: required i32 age;
2: required string name;
}
通过命令行生成java代码
thrift -r --gen java test.thrift
public void testPerson() throws TException {
Person person = new Person().setAge(18).setName("yano");
System.out.println(person);
TSerializer serializer = new TSerializer();
byte[] bytes = serializer.serialize(person);
System.out.println(Arrays.toString(bytes));
Person parsePerson = new Person();
TDeserializer deserializer = new TDeserializer();
deserializer.deserialize(parsePerson, bytes);
System.out.println(parsePerson);
}
Thrift并不仅仅是序列化协议,而是一个RPC框架。Thrift在空间开销和解析性能上有了比较大的提升,但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。
Protobuf
先定义一个.proto文件
option java_outer_classname="UserProtoBuf";
option java_package="com.xxx.serial";
message User{
required int32 id=1;
required string name=2;
optional int32 age=3;
optional string mobile=4;
}
通过命令行生成一个java类
E:\java>protoc --java_out=. com\xxx\serial\User.proto
package com.xxx.serial;
import com.xxx.serial.UserProtoBuf.User;
public class UserProtoMain {
public static void main(String[] args) {
UserProtoBuf.User.Builder builder = UserProtoBuf.User.newBuilder();
builder.setId(1);
builder.setName("protoc-java");
builder.setAge(18);
builder.setMobile("15011186302");
User user = builder.build();
System.out.println(user.toString());
}
}
UserProtoBuf.User.Builder builder = UserProtoBuf.User.parseFrom(result);
Protobuf序列化数据简洁、紧凑,解析速度快,使用也方便。但是Protobuf是纯粹的展示层协议,目前只支持Java,C++,python,且没有专门的RPC框架,在应用受限。
Avro
使用方式与前两种类似
Avro提供两种序列化格式:JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美,JSON格式方便测试阶段的调试。
但是Avro非JSON格式的IDL正在试验阶段,且IDL数据不如JSON直观
性能对比
数据来自
https://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking
时间开销
空间开销
从上图可得出如下结论:
-
XML序列化(Xstream)无论在性能和简洁性上比较差。
-
Thrift与Protobuf相比在时空开销方面都有一定的劣势。
-
Protobuf和Avro在两方面表现都非常优越
参考文献
https://blog.csdn.net/songchuwang1868/article/details/104003193
http://yangbajing.blog.chinaunix.net/uid-27105712-id-3266286.html