1.什么是RPC:
RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。
RPC 是一种技术思想而非一种规范或协议,常见 RPC 技术和框架有:
- 应用级的服务框架:阿里的 Dubbo/Dubbox、Google gRPC、Spring Boot/Spring Cloud。
- 远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。
- 通信框架:MINA 和 Netty。
目前流行的开源 RPC 框架还是比较多的,有阿里巴巴的 Dubbo、Facebook 的 Thrift、Google 的 gRPC、Twitter 的 Finagle 等。
完整的 RPC 框架
在一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。

如下是 Dubbo 的设计架构图,分层清晰,功能复杂:

RPC 功能目标
RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。 为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。 下面我们将具体细化 stub 结构的实现。RPC 调用分类
RPC 调用分以下两种:
同步调用
客户方等待调用执行完成并返回结果。
异步调用
客户方调用后不用等待执行结果返回,但依然可以通过回调通知等方式获取返回结果。 若客户方不关心调用返回结果,则变成单向异步调用,单向调用不用返回结果。
异步和同步的区分在于是否等待服务端执行完成并返回结果。
RPC的调用过程步骤
1、客户端client发起服务调用请求。
2、client stub 可以理解成一个代理,会将调用方法、参数按照一定格式进行封装,通过服务提供的地址,发起网络请求。
3、消息通过网络传输到服务端。
4、server stub接受来自socket的消息
5、server stub将消息进行解包、告诉服务端调用的哪个服务,参数是什么
6、结果返回给server stub。
7、sever stub把结果进行打包交给socket
8、socket通过网络传输消息
9、client slub 从socket拿到消息。
10、client stub解包消息将结果返回给client。
RPC 结构拆解
如下图所示。

RPC 服务方通过 RpcServer 去导出(export)远程接口方法,而客户方通过 RpcClient 去引入(import)远程接口方法。 客户方像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理 RpcProxy 。 代理封装调用信息并将调用转交给 RpcInvoker 去实际执行。 在客户端的 RpcInvoker 通过连接器 RpcConnector 去维持与服务端的通道 RpcChannel, 并使用 RpcProtocol 执行协议编码(encode)并将编码后的请求消息通过通道发送给服务方。
RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用 RpcProtocol 执行协议解码(decode)。 解码后的调用信息传递给 RpcProcessor 去控制处理调用过程,最后再委托调用给 RpcInvoker 去实际执行并返回调用结果。
RPC 组件职责
上面我们进一步拆解了 RPC 实现结构的各个组件组成部分,下面我们详细说明下每个组件的职责划分。
RpcServer
负责导出(export)远程接口
RpcClient
负责导入(import)远程接口的代理实现
RpcProxy
远程接口的代理实现
RpcInvoker
客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回
服务方实现:负责调用服务端接口的具体实现并返回调用结果
RpcProtocol
负责协议编/解码
RpcConnector
负责维持客户方和服务方的连接通道和发送数据到服务方
RpcAcceptor
负责接收客户方请求并返回请求结果
RpcProcessor
负责在服务方控制调用过程,包括管理调用线程池、超时时间等
RpcChannel
数据传输通道
接下来我们写个简单的例子来深入的学习RPC框架
下面就举一个1+1=2 的远程调用的例子。客户端发送两个参数,服务端返回两个数字的相加结果。RpcConsumer类调用CalculateService中的Calculate方法, 首先通过RpcFramework中的call方法,注册自己想要调用那个服务,返回代理,然后就像本地调用一样去调用Calculate方法,计算People,````People```有两个属性都被赋值成1,返回这两个属性相加后的结果。
public class RpcConsumer {
public static void main(String args[]) {
CalculateService service=RpcFramework.call(CalculateService.class,"127.0.0.1",8888);
People people=new People(1,1);
String res=service.Calculate(people);
System.out.println(res);
}
}生成动态代理的代码如下。客户端在调用方法前会先执行invoke方法,建立socket连接,把方法名和参数传递给服务端,然后获取返回结果。
//动态代理机制
public static <T> T call(final Class<?> interfaceClass,String host,int port){
if(interfaceClass==null){
throw new IllegalArgumentException("调用服务为空");
}
if(host==null||host.length()==0){
throw new IllegalArgumentException("主机不能为null");
}
if(port<=0||port>65535){
throw new IllegalArgumentException("端口不合法"+port);
}
return (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),new Class<?>[]{interfaceClass},new CallerHandler(host,port));
}
static class CallerHandler implements InvocationHandler {
private String host;
private int port;
public CallerHandler(String host, int port) {
this.host = host;
this.port = port;
SERVER = new InetSocketAddress(host, port);
}
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
Socket socket = new Socket(host, port);
try {
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try {
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(arguments);
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
Object result = input.readObject();
if (result instanceof Throwable) {
throw (Throwable) result;
}
return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
}RpcProvider 类实现具体的Calculate方法。通过RpcFramework中的publish方法,发布自己的服务。
public class RpcProvider{
public static void main(String[] args) throws Exception {
CalculateService service =new CalculateServiceImpl();
RpcFramework.publish(service,8888);
}
}
interface CalculateService{
String Calculate(People p);
}
class CalculateServiceImpl implements CalculateService{
public String Calculate(People people){
int res=people.getA()+people.getB();
return "计算结果 "+res;
}
}发布服务的代码如下。服务端循环监听某个端口,采用java原生的序列化方法,读取客户端需要调用的方法和参数,执行该方法并将结果返回。
public static void publish(final Object service,int port) throws IOException {
if(service==null)
throw new IllegalArgumentException("发布服务不能是空");
if(port<=0 || port >65535)
throw new IllegalArgumentException("端口不合法"+port);
ServerSocket server=new ServerSocket(port);
while (true) {
try{
final Socket socket=server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
try {
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
String methodName = input.readUTF();
Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
Object[] arguments = (Object[]) input.readObject();
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try {
Method method = service.getClass().getMethod(methodName, parameterTypes);
Object result = method.invoke(service, arguments);
output.writeObject(result);
} catch (Throwable t) {
output.writeObject(t);
} finally {
output.close();
}
} finally {
input.close();
}
} finally {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}catch(Exception e){
e.printStackTrace();
}
}
}可以看到正确返回计算结果2。
以上就是一个简单的RPC例子,下面我们看一下如何优化这个例子。
序列化和I/O模型的优化
数据序列化:
什么是序列化?序列化就是编码的过程,把对象或者数据结构转化成二进制字节码的过程。而反序列化就是把二进制字节码转化成数据结构或者对象。只有经过序列化后的数据才能在网络中传输。
I/O模型:
客户端和服务端的通信依赖Socket I/O。I/O 模型又可以分为:传统的阻塞 I/O(Blocking I/O)、非阻塞 I/O(Non-blocking I/O)、I/O 多路复用(I/O multiplexing)、异步 I/O(Asynchronous I/O)
下面针对上面的两个技术点进行优化。
用Protobuf优化数据序列化
数据序列化方法有很多种方法,常见的有Avro,Thrift,,XML,JSON,Protocol Buffer等工具。本文主要介绍的是Protobuf。Protobuf 全称““Protocol Buffer”” 是google 推出的高性能序列化工具。现已经在Github上开源。Protobuf采用tag来标识字段序号,用varint 和zigzag两种编码方式对整形做特殊的处理,Protobuf序列化后的数据紧凑,而且序列化时间短。
用Netty优化I/O模型
网络通信中I/O模型可以大致分为以下四种(准确说是5种,这里不讨论信号驱动I/O,因为在真正的编程中,我们很少使用这种模型):
1、阻塞I/O
2、非阻塞I/O
3、I/O多路复用
4、异步I/O
我们知道I/O处理是非常耗时的,CPU的处理速度非常快,如何最大化的利用CPU的性能,就是要避免线程阻塞在I/O处理上。业界目前比较多的采用I/O多路复用和异步I/O提高性能。
如何理解这四种I/O模型,大家可以参照此例子。体系化认识RPC Netty 正是采用了第三种 I/O多路复用的方法,I/O多路复用对应Reactor模式。Reactor把耗时的网络操作编码交给专门的线程或者线程池去处理。比如下面这张图是Reactor模式示意图。图中mainReactor线程、subReactor线程、work线程这些不同的线程,分别干不同专业的事情,吞吐量自然上去了。
总结
本文只是通过一个简单例子介绍了RPC中的I/O模型以及序列化,其实RPC本身是一个很大的话题,比如如何保证在不可靠的网络中保证RPC的可靠性?如何实现客户端的重试调用、超时控制?如何优雅的起停服务、发现与注册服务?还有很多问题值得大家研究学习,这里不做过多探讨了。
















