手动实现RPC系列文章
前面三篇章的文章,我们已经了解学习了RPC是什么,以及RPC的原理。实现一个RPC框架需要用到哪些技术。有兴趣的小伙伴们可以点击以下链接看见这部分的所有内容
另在此申明,我是基于GitHub作者 Java Guide的开源作品来实现的,请大家直接点击链接在GitHub上进行学习。
前言
前面的文章我们提到了序列化这个概念,我们知道,Java的对象要在网络中进行传输就离不开序列化,因为网络传输只能够进行二进制字节流的传输。而通过序列化我们就可以把数据结构或者对象转换成二进制字节流,而反序列化可以再把这些二进制字节流转换成对象。
在我们设计的RPC框架中,由于要通过网络实现远程调用,就需要通过代理对象,将方法,参数等信息封装成能够在网络中传输的消息体,然后这个消息体要进行序列化操作以后,才能够在我们的服务消费端和服务提供方之间进行传输使用。
所以下面的内容,就详细讲解一下序列化操作和Java中序列化方式,以及我们的序列化协议选择。
一、序列化的使用场景?
在我们的实际开发过程中,有以下几个方面会用到序列化和反序列化的场景
1.对象在进行网络传输的过程中需要先被序列化,接收到序列化对象之后再进行反序列化。
2.将对象存储到文件中需要先进行序列化,对象从文件中取出进行反序列化。
3.将对象存储到缓存数据库(Redis,MongoDB)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。
序列化协议对应TCP/IP四层模型的那一层
我们知道,序列化和反序列化是用于网络传输过程中的操作,那么规定序列化操作的我们可以称之为序列化协议,我们知道网络传输中不同协议对应不同的层面,那么序列化协议在哪一层呢。
在OSI七层协议模型中
OSI七层模型中的表示层负责对应用层的用户数据进行处理,转换成二进制流往下进行传输,而反过来,它接受二进制字节流又转换成用户数据提供给应用层。这是不是就很像我们的序列化和反序列化操作呢。
在OSI七层模型中,应用层,表示层和会话层都对应的是TCP/IP模型中的应用层,所以序列化和反序列化在TCP/IP模型中就是位于应用层的协议。
二、常见的序列化协议
前面我们提到了,序列化方式最简单的一种就是直接实现JDK自带的序列化接口Serializable就可以了,但是这种方式不支持跨语言调用,而且性能比较低。现在常用的序列化协议有 hessian,kyro,protostuff。
另外还有JSON和XML这种文本类序列化方式,可读性比较好,但是性能也比较差。
Serializable接口序列化
以一个代码例子来进行说明
public class RpcRequest implements Serializable{
private static final long serialVersionUID = 1L;
private String requestId;
private String interfaceName;
private String methodName;
}
这里的 serialVersionUID 是我们指定的序列化数据的版本,当对这个类的对象进行序列化操作的时候,serialVersionUID 会被写入到二进制序列中,当反序列化的时候会检查这个二进制序列的serialVersionUID 是否和当前类的 serialVersionUID 相同,如果相同才会正常进行,所以我们把它定义为 静态final 常量。
而如果二进制序列的serialVersionUID 和当前类的 serialVersionUID不同,就会抛出 InvalidClassException 异常。一般我们会手动指定serialVersionUID,如果没有手动指定,编译器会自动生成默认的serialVersionUID。
现实中我们比较少用Java自带的序列化接口进行序列化。
原因是1.不支持跨语言调用,其他语言无法使用
2.相比其他序列化框架封装的序列化功能性能较低,主要原因是序列化后的字节数组体积较大,传输成本高。
Kryo
Kryo是一个高性能的序列化/反序列化工具,由于其变长存储的特性,并且使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。
Kryo作为一个成熟的序列化工具,在Twitter,Groupon,Yahoo等多个著名开源项目中都有广泛使用。
而本次我动手实现的RPC框架(GitHub作者 Java Guide的开源作品)也是使用Kyro进行序列化操作的。GitHub地址:Netty+Kyro+Zookeeper.(一款基于 Netty+Kyro+Zookeeper 实现的自定义 RPC 框架-附详细实现过程和相关教程。) (github.com)
本次RPC框架使用到的Kyro序列化相关代码如下,具体如何进行使用,会在后面实现的过程中进行讲解。
@Slf4j
public class KryoSerializer implements Serializer{
private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
kryo.register(RpcResponse.class);
kryo.register(RpcRequest.class);
return kryo;
});
@Override
public byte[] serialize(Object obj){
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream)){
Kryo kryo = KryoThreadLocal.get();
//将对象序列化成byte数组
kryo.writeObject(output,obj);
kryoThreadLocal.remove();
return output.toBytes();
}catch (Exception e){
throw new SerialException("序列化失败");
}
}
@Override
public <T> T deserialize(byte[] bytes,Class<T> clazz){
try(ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream)){
Kyro kyro = kryoThreadLocal.get();
//反序列化
Object o = kryo.readObject(input,clazz);
kryoThreadLocal.remove();
return clazz.cast(o);
}catch (Exception e){
throw new SerialException("反序列化失败");
}
}
}
Kryo的Github地址:EsotericSoftware/kryo: Java binary serialization and cloning: fast, efficient, automatic (github.com)
Protobuf
Protobuf 产于Google,性能不错,支持多种语言,并且支持跨平台,但是使用过程比较繁琐,需要我们自定义IDL文件和生成对应的序列化代码。虽然繁琐,但是可以避免序列化漏洞的风险
Protobuf 包含序列化格式的定义,各种语言的库以及一个IDL编译器。正常情况下需要自定义proto文件,然后使用IDL编译器编译成我们使用的语言。
一个简单的proto文件如下:
//protobuf版本
syntax = "proto3";
//SerachRequest 会被编译成不同编程语言对应的对象,比如Java中的class
message Person{
string name = 1;
int age = 2;
}
GitHub地址:protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com)
ProtoStuff
protobuf的哥哥,也是Google出品的,提供了更多的功能,易用性更高。然而效率并不比他的弟弟差 Github地址:protostuff/protostuff: Java serialization library, proto compiler, code generator (github.com)
hession
hession是一个轻量级的自定义二进制RPC协议。也支持跨语言操作,是Dubbo默认的序列化方式
总结
Kryo是专门针对Java语言使用的序列化方式,性能良好,如果我们的应用只针对Java语言的编写的话,可以考虑使用。在Dubbo官网的文章中也有推荐使用Kryo作为生产环境的序列化方式。
而其他的序列化协议都是支持跨语言的序列化方式,如果有跨语言需求的话,可以考虑使用。