一、RPC调用原理图
下面这张图是我们微服务一次Http调用请求图:
首先在请求的过程中我们知道是有三次握手,四次挥手的流程,具体流程如下:
1.浏览器请求服务器(订单服务),请求建立连接,首先客户端向服务器端发送一段 TCP 报文
2.服务器(订单服务)相应浏览器,可以建立连接,并且询问浏览器是否马上建立连接。
3.浏览器相应服务器(订单服务),可以建立连接。
-----开始传输数据------------
###下面要断开连接就进入四次挥手环节
1.浏览器向服务器(订单服务)发起请求,需要断开连接(分手)
2.服务器(订单服务)相应浏览器,说我收到断开请求了,但是需要在想一下(一般是数据传输)
3.服务器(订单服务)接受完数据之后,向浏览器发送请求,说可以断开连接了(想好了,可以分手了)
4.浏览器接受到服务器(订单服务),回复服务器,我断开连接了。
1.1RPC概述
RPC(Remote Procedure Call Protocol)远程过程调用协议。RPC的主要功能是让构建分布式计算更加容易,再提供强大的远程调用能力的同时而不损失本地调用的语义简洁性。为实现该目标,RPC框架需要提供一套透明的调用机制,使使用者不用显示的区分本地调用还是分布式调用。
RPC的优点:
- 分布式设计
- 部署灵活
- 解耦服务
- 扩展性强
RPC框架的优势:
- RPC框架一般使用长连接,不必要每次通信都三次握手、四次挥手。
- RPC框架一般有注册中心,有丰富的监控管理、发布、下线接口、动态扩展等。对调用方来说是无感知的、统一化的操作、协议私密,安全性高。
- RPC更简单内容更小、效率更高、服务化架构、服务化治理、RPC框架是一个强力的支撑。
- RPC框架基于TCP实现,也可以基于Http2实现。
1.2RPC框架
主流的RPC框架:
- Dubbo:国内最早开源的RPC框架,由阿里巴巴公司开发并与2011年对外发布,不过仅支持Java语言。
- Motan:新浪微博内部使用的RPC框架,与2016年对外开源,仅支持Java语言。
- Tars:腾讯内部使用的RPC框架,与2017年对外开源,仅支持C++语言。
- Spring Cloud:国外 Pivotal 公司 2014 年对外开源的 RPC 框架,提供了丰富的生态组件。
- gRPC:Goolge与2015年对外开源的跨语言rpc框架,2007年贡献给了Apache基金,成为了Apache开源项目之一,支持多种语言。
1.3应用场景
应用举例:
- 分布式操作系统的进程间通讯:进程间通讯是操作系统必须提供的基本设施之一,分布式操作系统必须提供分布于异构的结点机上进 程间的通讯机制,RPC是实现消息传送模式的分布式进程间通讯方式之一。
- 构造分布式设计的软件环境:由于分布式软件设计,服务与环境的分布性, 它的各个组成成份之间存在大量的交互和通讯, RPC是 其基本的实现方法之一。Dubbo分布式服务框架基于RPC实现,Hadoop也采用了RPC方式实现客 户端与服务端的交互。
- 远程数据库服务:在分布式数据库系统中,数据库一般驻存在服务器上,客户机通过远程数据库服务功能访问数据库 服务器,现有的远程数据库服务是使用RPC模式的。例如,Sybase和Oracle都提供了存储过程机 制,系统与用户定义的存储过程存储在数据库服务器上,用户在客户端使用RPC模式调用存储过 程。
- 分布式应用程序设计:RPC机制与RPC工具为分布式应用程序设计提供了手段和方便, 用户可以无需知道网络结构和协议细 节而直接使用RPC工具设计分布式应用程序。
- 分布式程序调试:RPC可用于分布式程序的调试。使用反向RPC使服务器成为客户并向它的客户进程发出RPC,可以 调试分布式程序。例如,在服务器上运行一个远端调试程序,它不断接收客户端的RPC,当遇到一 个调试程序断点时,它向客户机发回一个RPC,通知断点已经到达,这也是RPC用于进程通讯的例 子。
二、深入RPC原理
2.1设计与调用原理
具体调用过程:
- 服务消费者(client客户端)通过本地调用的方式调用服务。
- 客户端存根(client stub)接收到请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。
- 客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
- 服务端存根(server stub)收到消息后进行解码(反序列化操作)。
- 服务端存根(server stub)根据解码结果调用本地的服务进行相关处理。
- 本地服务执行具体的业务逻辑并将处理结果返回给服务端存根(server stub)。
- 服务端存根(server stub)将返回的结果重新打包成消息(序列化),并通过网络发送到消费方。
- 客户端存根(client stub)接收到消息,并进行解码(反序列化)。
- 服务消费方得到最终的结果。
所涉及到的技术:
- 动态代理:生成Client stub(客户端存根)和Server Stub(服务端存根)的时候需要用到java动态代码技术。
- 序列化:在网络中,所有的数据都将会转化为字节进行传送,需要对这些参数进行序列化和反序列化操作。目前主流的开源序列化框架有Kryo、fastJson、Hessian、Protobuf等。
- NIO通信:Java提供了NIO的解决方案,Java7也提供了更优秀的NIO.2支持。可以采用Netty或者mina框架来解决数据传输的问题。开源的RPC框架Dubbo就是采用NIO通信,集成netty、mina、grizzly。
- 服务注册中心:通过注册中心,让客户端连接调用服务端所发布的服务。主流的注册中心组件:redis、Nacos、Zookeeper、Consul、Etcd。Dubbo采用的是ZooKeeper提供服务注册与发现功能。
- 负载均衡:在高并发的场景下,需要多个节点或者集群来提升整体的吞吐能力。
- 健康检查:健康检查包括,客户端心跳和服务主动探测两种方式。
2.2 RPC深入解析
2.2.1序列化技术
- 序列化的作用:
在网络传输中,数据必须采用二进制形式,所以在RPC调用过程中,需要采用序列化技术,对入参对象和返回值对象进行序列化和反序列化。
- 序列化原理:
自定义二进制协议来实现序列化:
- 序列化处理要素:
- 解析效率:序列化协议应该首要考虑的因素,像json/xml解析起来比较耗时,需要解析dom树,二进制自定义协议解析起来效率要快很多。
- 压缩率:同样一个对象,xml/json传输起来有大量的标签冗余信息,信息有消息低,二进制自定义协议占用的空间相对来说会小很多。
- 扩展性与可调式性:xml/json可读性会比二进制协议好很多,并且通过网络抓包是可以直接读取,二进制则需要反序列化才能查看能内容。
- 跨语言:有些序列化协议与开发语言紧密相关的,例如dobbo的Hessian序列化协议只能支持Java的RPC调用。
- 通用性:xml/json非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,二进制数据的处理方面也有Protobuf和Hessian等插件,在做设计的时候尽量做到较好的通用性。
- 常用的序列化技术
1.JDK原生序列化,代码如下:
public class JDKSerialization {
public static void serialize() throws Exception {
//将序列化后的数据存入到D:/TestCode/tradeUser.clazz中
String basePath = "D:/TestCode/";
FileOutputStream fos = new FileOutputStream(basePath + "tradeUser.clazz");
//创建tradeUser对象
TradeUser tradeUser = new TradeUser();
tradeUser.setName("Mirson");
//将tradeUser写入到tradeUser.clazz中
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(tradeUser);
oos.flush();
oos.close();
//读取D:/TestCode/tradeUser.clazz
FileInputStream fis = new FileInputStream(basePath + "tradeUser.clazz");
ObjectInputStream ois = new ObjectInputStream(fis);
//将读取的数据反序列化到对象中
TradeUser deTradeUser = (TradeUser) ois.readObject();
ois.close();
System.out.println("=== 反序列化结果 ==== ");
System.out.println(deTradeUser);
}
public static void main(String[] args) throws Exception {
serialize();
}
}
@Data
public class TradeUser implements Serializable {
/**
* 用户编号
*/
private String userNo;
/**
* 用户名称
*/
private String name;
/**
* 用户密码
*/
private String userPwd;
/**
* 电话号码
*/
private String phone;
/**
* 公司ID
*/
private Long companyId;
/**
* 邮箱
*/
private String email;
/**
* 地址
*/
private String address;
/**
* 最近一次用户登陆IP
*/
private String lastLoginIp;
/**
* 最近一次登陆时间
*/
private Date lastLoginTime;
/**
* 状态(0:有效, 1:锁定, 2:禁用)
*/
private int status;
/**
* 创建时间
*/
private Date createTime;
}
- 在Java中,序列化必须要实现java.io.Serializable接口。
- 通过ObjectOutputStream和ObjectInputStream对象进行序列化以及反序列化操作。
- 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的点事两个类的序列化ID是否一致(也就是在代码中定于的序列ID private static final long serialVersionUID);
- 序列化并不会保存静态变量。
- 想要父类对象也序列化,就需要父类也实现Serializable接口;
- Transient关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值被设为初始值,如基本类型int为0,封装对象型则为null;
- 服务器给客户端发哦送你个序列化对象数据并非加密的,如果对象中有一些敏感数据比如密码等,那么在对密码字段序列化之前,最好做加密处理,这样子可以一定程度保证序列化对象的数据安全。
2.json序列化
一般在HTTP协议的RPC框架通信中,会选择JSON方式,因为JSON有较好的扩展性、可读性和通用性。
缺陷:JSON序列化占用空间开销较大,没有JAVA的强类型区分,需要通过反射解决,解析效率和压缩率都较差。
如果对并发和性能要求较高,或者是传输数据量较大的场景,不建议采用JSON序列化方式。
3.Hessian2序列化
Hessian是一个动态类型,二进制序列化,并且支持跨语言特性的序列化框架。Hessian性能上要比JDK、JSON序列化高效很多,并且生成的字节数更小,有非常好的兼容性和稳定性,所以Hessian更加适合作为RPC框架远程通信的序列化协议。
代码示例:
public class Hessian2Serialization {
public static void main(String[] args) throws Exception {
serialize();
}
public static void serialize() throws Exception {
TradeUser tradeUser = new TradeUser();
tradeUser.setName("Mirson");
tradeUser.setUserNo("100001");
//tradeUser对象序列化处理
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bos);
output.writeObject(tradeUser);
output.flushBuffer();
byte[] data = bos.toByteArray();
System.out.println("=== 序列化结果 ==== ");
System.out.println(data);
bos.close();
//tradeUser对象反序列化处理
ByteArrayInputStream bis = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(bis);
TradeUser deTradeUser = (TradeUser) input.readObject();
input.close();
System.out.println("=== 反序列化结果 ==== ");
System.out.println(deTradeUser);
}
}
Dobbo Hessian Lite序列化流程:
Dubbo Hessian Lite反序列化流程:
Hessian自身也存在一些缺陷,大家在使用过程中要注意:
- 对Linked系列对象不支持,比如LinkedHashMap、LinkedHashSet等,但可以通过CollectionSerializer类修复。
- Local类不支持,可以通过拓展ContextSerizlizerFactory类修复。
- Byte/Short在反序列化的时候会转成Integer。
Dubbo2.7.3通讯序列化源码实现分析:
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
if (msg instanceof Request) {
this.encodeRequest(channel, buffer, (Request)msg);
} else if (msg instanceof Response) {
this.encodeResponse(channel, buffer, (Response)msg);
} else {
super.encode(channel, buffer, msg);
}
}
反序列化流程:
源码如下:ExchangeCodec的decode方法
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
int readable = buffer.readableBytes();
byte[] header = new byte[Math.min(readable, 16)];
buffer.readBytes(header);
return this.decode(channel, buffer, readable, header);
}
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2];
byte proto = (byte)(flag & 31);
long id = Bytes.bytes2long(header, 4);
if ((flag & -128) == 0) {
Response res = new Response(id);
if ((flag & 32) != 0) {
res.setEvent(true);
}
byte status = header[3];
res.setStatus(status);
try {
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
if (status == 20) {
Object data;
if (res.isHeartbeat()) {
data = this.decodeHeartbeatData(channel, in);
} else if (res.isEvent()) {
data = this.decodeEventData(channel, in);
} else {
data = this.decodeResponseData(channel, in, this.getRequestData(id));
}
res.setResult(data);
} else {
res.setErrorMessage(in.readUTF());
}
} catch (Throwable var12) {
res.setStatus((byte)90);
res.setErrorMessage(StringUtils.toString(var12));
}
return res;
} else {
Request req = new Request(id);
req.setVersion(Version.getProtocolVersion());
req.setTwoWay((flag & 64) != 0);
if ((flag & 32) != 0) {
req.setEvent(true);
}
try {
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
Object data;
if (req.isHeartbeat()) {
data = this.decodeHeartbeatData(channel, in);
} else if (req.isEvent()) {
data = this.decodeEventData(channel, in);
} else {
data = this.decodeRequestData(channel, in);
}
req.setData(data);
} catch (Throwable var13) {
req.setBroken(true);
req.setData(var13);
}
return req;
}
}
4.Protobuf序列化
Protobuf是Google推出的开源的序列库,它是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持Java、Python、C++、Go等多种语言,
Protobuf使用的时候需要定义IDL(inteface description language),然后使用不同语言的IDL编译器,生成序列化工具类,它具备一下有点:
- 压缩比高,体积小,序列化体积相比JSON、Hessian小很多;
- IDL能清晰的描述语义,可以帮助保证应用程序之间的类型不会丢失,无需类似XML解析器;
- 序列化反序列化速度很快,不需要通过反射获取类型;
- 消息格式的拓展、升级和兼容性都不错,可以做到向后兼容;
脚本示例:
// 定义Proto版本
syntax = "proto3";
// 是否允许生成多个JAVA文件
option java_multiple_files = false;
// 生成的包路径
option java_package = "com.itcast.rpc.samples.serial.proto";
// 生成的JAVA类名
option java_outer_classname = "TradeUserProto";
// 预警通知消息体
message TradeUser {
/**
* 用户ID
*/
int64 userId = 1 ;
/**
* 用户名称
*/
string userName = 2 ;
}
代码操作:
public class ProtoSerialization {
public static void serialize() throws Exception{
// 创建TradeUser的Protobuf对象
TradeUserProto.TradeUser.Builder builder = TradeUserProto.TradeUser.newBuilder();
builder.setUserId(100001);
builder.setUserName("Mirson");
//将TradeUser做序列化处理
TradeUserProto.TradeUser msg = builder.build();
byte[] data = msg.toByteArray();
System.out.println("=== 序列化结果 ==== ");
System.out.println(data);
//反序列化处理, 将刚才序列化的byte数组转化为TradeUser对象
TradeUserProto.TradeUser deTradeUser = TradeUserProto.TradeUser.parseFrom(data);
System.out.println("=== 反序列化结果 ==== ");
System.out.println(deTradeUser);
}
}