摘要:

  本文阐述了RPC框架与远程调用的产生背景,介绍了RPC的基本概念和使用背景,之后手动实现了简易的RPC框架并佐以实例进行演示,以便让各位看官对RPC有一个感性、清晰和完整的认识,最后讨论了RPC框架几个较为重要问题。总之,RPC框架的精髓在于动态代理和反射,通过它们使得远程调用“本地化”,对用户透明且友好。

一. 引子

  上学时我们写得应用大都比较简单,基本上都属于单体应用,服务调用也局限于本地,如下所示:

// 服务接口
public interface HelloService { String hello(String name); String hi(String msg);
} // 服务本地实现
public class HelloServiceImpl implements HelloService{
@Override
public String hello(String name) {
return "Hello " + name;
} @Override
public String hi(String msg) {
return "Hi, " + msg;
}
} // 服务本地调用
public class Main {
public static void main(String[] args) {
HelloService helloService = new HelloServiceImpl();
helloServiceProxy.hello("Panda");
helloServiceProxy.hi("Panda");
}/** Output
hello : Hello rico
hi : Hi, panda
**/
}

  我们写这样的单体应用来学习、做实验正常且合理,但是在生产环境中,单体应用在各方面的性能上和可维护性方面就远远不能满足需求了。应用内各项业务互相纠缠、耦合性太大,不利于后期的维护和升级,主要表现在以下两点上:

  • 可用性低。所有鸡蛋都放在同一个篮子里,一旦有问题导致单体应用挂掉,所有业务都不能访问,稳定性要求难以满足;
  • 不利于各业务团队进行合作,开发效率低。单体应用各业务耦合度太高,不同业务团队开发进度和实现细节不尽相同,难以高效协作。

  将不同的业务拆分到多个应用中,让不同的应用分别承担不同的功能是解决这些问题的必杀技。将不同业务分拆到不同的应用后,不但可以大幅度提升系统的稳定性还有助于丰富技术选型,进一步保证系统的性能。总的来说,从单体应用到分布式多体应用是系统升级必经之路。

  当一个单体应用演化成多体应用后,远程调用就粉墨登场了。在一个应用时,相互通信直接通过本地调用就可完成,而变为多体应用时,相互通信就得依赖远程调用了,这时一个高效稳定的RPC框架就显得非常必要了。可能有的同学会觉得,没必要非得用RPC框架啊,简单的HTTP调用不是也可以实现远程通信吗?确实,简单的HTTP调用确实也可以实现远程通信,但是它不是那么的合适,原因有二:

  • RPC远程调用像本地调用一样干净简洁,但其他方式对代码的侵入性就比较强;
  • 一般使用RPC框架实现远程通信效率比其他方式效率要高一些。

  当我们踏入公司尤其是大型互联网公司就会发现,公司的系统都由成千上万大大小小的服务组成,各服务部署在不同的机器上,由不同的团队负责。这时就会有两个很关键的问题:

  • 要搭建一个新服务,免不了需要依赖已有的服务,而现在已有的服务都在远端,怎么调用?
  • 其它团队想使用我们的新服务,我们的服务该怎么发布以便他人调用?

下文将对RPC框架的基本原理进行介绍,并对这两个问题展开探讨,同时参考前辈的博文《RPC框架几行代码就够了》手写一个简易RPC框架以加深对PRC原理的理解。


二. RPC 框架介绍
  对于多体应用,由于各服务部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都要写一坨网络通信相关的代码,不仅复杂而且极易出错。如果有一种方式能让我们像调用本地服务一样调用远程服务,而让调用者对网络通信这些细节透明,那么将大大解放程序员的双手,大幅度提高生产力。比如,服务消费方在执行helloService.hi(“Panda”)时,实质上调用的是远端的服务。这种方式其实就是RPC(Remote Procedure Call Protocol),在各大互联网公司中被广泛使用,如阿里巴巴的HSF、Dubbo(开源)、Facebook的Thrift(开源)、Google GRPC(开源)、Twitter的Finagle(开源)等。

  RPC的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。要让网络通信细节对使用者透明,我们需要对通信细节进行封装,下面是一个RPC的经典调用的流程,并且反映了所涉及到的一些通信细节:

  • (1). 服务消费方(client)以本地调用方式调用服务;
  •  (2). client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
  •  (3). client stub找到服务地址,并将消息发送到服务端;
  •  (4). server stub收到消息后进行解码;
  •  (5). server stub根据解码结果 反射调用 本地的服务;
  •  (6). 本地服务执行并将结果返回给server stub;
  •  (7). server stub将返回结果打包成消息并发送至消费方;
  •  (8). client stub接收到消息,并进行解码;
  •  (9). 服务消费方得到最终结果。

  RPC框架就是要将2~8这些步骤封装起来,让用户对这些细节透明,使得远程方法调用看起来像调用本地方法一样。


三. RPC框架简易实现及其实例分析

(1).服务端

  服务端提供客户端所期待的服务,一般包括三个部分:服务接口,服务实现以及服务的注册暴露三部分,如下:

  • 服务接口
public interface HelloService {

    String hello(String name);

    String hi(String msg);
}
  • 服务实现
public class HelloServiceImpl implements HelloService{
@Override
public String hello(String name) {
return "Hello " + name;
} @Override
public String hi(String msg) {
return "Hi, " + msg;
}
}
  • 服务暴露:只有把服务暴露出来,才能让客户端进行调用,这是RPC框架功能之一。、
public class RpcProvider {
public static void main(String[] args) throws Exception {
HelloService service = new HelloServiceImpl();
// RPC框架将服务暴露出来,供客户端消费
RpcFramework.export(service, 1234);
}
}

(2).客户端

  客户端消费服务端所提供的服务,一般包括两个部分:服务接口和服务引用两个部分,如下:

  • 服务接口:与服务端共享同一个服务接口
public interface HelloService {

    String hello(String name);

    String hi(String msg);
}
  • 服务引用:消费端通过RPC框架进行远程调用,这也是RPC框架功能之一
public class RpcConsumer {
public static void main(String[] args) throws Exception {
// 由RpcFramework生成的HelloService的代理
HelloService service = RpcFramework.refer(HelloService.class, "127.0.0.1", 1234);
String hello = service.hello("World");
System.out.println("客户端收到远程调用的结果 : " + hello);
}
}

(3).RPC框架原型实现

  RPC框架主要包括两大功能:一个用于服务端暴露服务,一个用于客户端引用服务。

  • 服务端暴露服务
 /**
* 暴露服务
*
* @param service 服务实现
* @param port 服务端口
* @throws Exception
*/
public static void export(final Object service, int port) throws Exception {
if (service == null) {
throw new IllegalArgumentException("service instance == null");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Export service " + service.getClass().getName() + " on port " + port);
// 建立Socket服务端
ServerSocket server = new ServerSocket(port);
for (; ; ) {
try {
// 监听Socket请求
final Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
try {
/* 获取请求流,Server解析并获取请求*/
// 构建对象输入流,从源中读取对象到程序中
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream());
try { System.out.println("\nServer解析请求 : ");
String methodName = input.readUTF();
System.out.println("methodName : " + methodName);
// 泛型与数组是不兼容的,除了通配符作泛型参数以外
Class<?>[] parameterTypes = (Class<?>[])input.readObject();
System.out.println(
"parameterTypes : " + Arrays.toString(parameterTypes));
Object[] arguments = (Object[])input.readObject();
System.out.println("arguments : " + Arrays.toString(arguments)); /* Server 处理请求,进行响应*/
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream());
try {
// service类型为Object的(可以发布任何服务),故只能通过反射调用处理请求
// 反射调用,处理请求
Method method = service.getClass().getMethod(methodName,
parameterTypes);
Object result = method.invoke(service, arguments);
System.out.println("\nServer 处理并生成响应 :");
System.out.println("result : " + result);
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();
}
}
}

  从该RPC框架的简易实现来看,RPC服务端逻辑是:首先创建ServerSocket负责监听特定端口并接收客户连接请求,然后使用Java原生的序列化/反序列化机制来解析得到请求,包括所调用方法的名称、参数列表和实参,最后反射调用服务端对服务接口的具体实现并将得到的结果回传至客户端。至此,一次简单PRC调用的服务端流程执行完毕。


  • 客户端引用服务
 /**
* 引用服务
*
* @param <T> 接口泛型
* @param interfaceClass 接口类型
* @param host 服务器主机名
* @param port 服务器端口
* @return 远程服务,返回代理对象
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static <T> T refer(final Class<T> interfaceClass, final String host, final int port)
throws Exception {
if (interfaceClass == null) {
throw new IllegalArgumentException("Interface class == null");
}
// JDK 动态代理的约束,只能实现对接口的代理
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
"The " + interfaceClass.getName() + " must be interface class!");
}
if (host == null || host.length() == 0) {
throw new IllegalArgumentException("Host == null!");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println(
"Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port); // JDK 动态代理
T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class<?>[] {interfaceClass}, new InvocationHandler() {
// invoke方法本意是对目标方法的增强,在这里用于发送RPC请求和接收响应
@Override
public Object invoke(Object proxy, Method method, Object[] arguments)
throws Throwable {
// 创建Socket客户端,并与服务端建立链接
Socket socket = new Socket(host, port);
try {
/* 客户端像服务端进行请求,并将请求参数写入流中*/
// 将对象写入到对象输出流,并将其发送到Socket流中去
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream());
try {
// 发送请求
System.out.println("\nClient发送请求 : ");
output.writeUTF(method.getName());
System.out.println("methodName : " + method.getName());
output.writeObject(method.getParameterTypes());
System.out.println("parameterTypes : " + Arrays.toString(method
.getParameterTypes()));
output.writeObject(arguments);
System.out.println("arguments : " + Arrays.toString(arguments)); /* 客户端读取并返回服务端的响应*/
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream());
try {
Object result = input.readObject();
if (result instanceof Throwable) {
throw (Throwable)result;
}
System.out.println("\nClient收到响应 : ");
System.out.println("result : " + result);
return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
});
return proxy;
}

  从该RPC框架的简易实现来看,RPC客户端逻辑是:首先创建Socket客户端并与服务端建立链接,然后使用Java原生的序列化/反序列化机制将调用请求发送给客户端,包括所调用方法的名称、参数列表将服务端的响应返回给用户即可。至此,一次简单PRC调用的客户端流程执行完毕。特别地,从代码实现来看,实现透明的PRC调用的关键就是  动态代理,这是RPC框架实现的灵魂所在。


  • RPC原型实现
public class RpcFramework {
/**
* 暴露服务
*
* @param service 服务实现
* @param port 服务端口
* @throws Exception
*/
public static void export(final Object service, int port) throws Exception {
if (service == null) {
throw new IllegalArgumentException("service instance == null");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Export service " + service.getClass().getName() + " on port " + port);
// 建立Socket服务端
ServerSocket server = new ServerSocket(port);
for (; ; ) {
try {
// 监听Socket请求
final Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
try {
/* 获取请求流,Server解析并获取请求*/
// 构建对象输入流,从源中读取对象到程序中
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream());
try { System.out.println("\nServer解析请求 : ");
String methodName = input.readUTF();
System.out.println("methodName : " + methodName);
// 泛型与数组是不兼容的,除了通配符作泛型参数以外
Class<?>[] parameterTypes = (Class<?>[])input.readObject();
System.out.println(
"parameterTypes : " + Arrays.toString(parameterTypes));
Object[] arguments = (Object[])input.readObject();
System.out.println("arguments : " + Arrays.toString(arguments)); /* Server 处理请求,进行响应*/
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream());
try {
// service类型为Object的(可以发布任何服务),故只能通过反射调用处理请求
// 反射调用,处理请求
Method method = service.getClass().getMethod(methodName,
parameterTypes);
Object result = method.invoke(service, arguments);
System.out.println("\nServer 处理并生成响应 :");
System.out.println("result : " + result);
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();
}
}
} /**
* 引用服务
*
* @param <T> 接口泛型
* @param interfaceClass 接口类型
* @param host 服务器主机名
* @param port 服务器端口
* @return 远程服务,返回代理对象
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static <T> T refer(final Class<T> interfaceClass, final String host, final int port)
throws Exception {
if (interfaceClass == null) {
throw new IllegalArgumentException("Interface class == null");
}
// JDK 动态代理的约束,只能实现对接口的代理
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
"The " + interfaceClass.getName() + " must be interface class!");
}
if (host == null || host.length() == 0) {
throw new IllegalArgumentException("Host == null!");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println(
"Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port); // JDK 动态代理
T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class<?>[] {interfaceClass}, new InvocationHandler() {
// invoke方法本意是对目标方法的增强,在这里用于发送RPC请求和接收响应
@Override
public Object invoke(Object proxy, Method method, Object[] arguments)
throws Throwable {
// 创建Socket客户端,并与服务端建立链接
Socket socket = new Socket(host, port);
try {
/* 客户端像服务端进行请求,并将请求参数写入流中*/
// 将对象写入到对象输出流,并将其发送到Socket流中去
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream());
try {
// 发送请求
System.out.println("\nClient发送请求 : ");
output.writeUTF(method.getName());
System.out.println("methodName : " + method.getName());
output.writeObject(method.getParameterTypes());
System.out.println("parameterTypes : " + Arrays.toString(method
.getParameterTypes()));
output.writeObject(arguments);
System.out.println("arguments : " + Arrays.toString(arguments)); /* 客户端读取并返回服务端的响应*/
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream());
try {
Object result = input.readObject();
if (result instanceof Throwable) {
throw (Throwable)result;
}
System.out.println("\nClient收到响应 : ");
System.out.println("result : " + result);
return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
});
return proxy;
}
}

四. 关于RPC框架的若干问题说明
(1).RPC框架如何做到透明化远程服务调用?

  如何封装通信细节才能让用户像以本地调用方式调用远程服务呢?就Java而言,动态代理恰是解决之道。Java动态代理有JDK动态代理和CGLIB动态代理两种方式。尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,因此RPC框架的大部分实现还是选择JDK动态代理的方式。在上面的例子中,RPCFramework实现中的invoke方法封装了与远端服务通信的细节,消费方首先从RPCFramework获得服务提供方的接口,当执行helloService.hi(“Panda”)方法时就会调用invoke方法。

(2).如何发布自己的服务?

  如何让别人使用我们的服务呢?难道就像我们上面的代码一样直接写死服务的IP以及端口就可以了吗?事实上,在实际生产实现中,使用人肉告知的方式是不现实的,因为实际生产中服务机器上/下线太频繁了。如果你发现一台机器提供服务不够,要再添加一台,这个时候就要告诉调用者我现在有两个IP了,你们要轮询调用来实现负载均衡;调用者咬咬牙改了,结果某天一台机器挂了,调用者发现服务有一半不可用,他又只能手动修改代码来删除挂掉那台机器的ip。这必然是相当痛苦的!

  有没有一种方法能实现自动告知,即机器的上线/下线对调用方透明,调用者不再需要写死服务提供方地址?当然可以,生产中的RPC框架都采用的是自动告知的方式,比如,阿里内部使用的RPC框架HSF是通过ConfigServer来完成这项任务的。此外,Zookeeper也被广泛用于实现服务自动注册与发现功能。不管具体采用何种技术,他们大都采用的都是 发布/订阅模式。

(3).序列化与反序列化

  我们知道,Java对象是无法直接在网络中进行传输的。那么,我们的RPC请求如何发给服务端,客户端又如何接收来自服务端的响应呢?答案是,在传输Java对象时,首先对其进行序列化,然后在相应的终端进行反序列化还原对象以便进行处理。事实上,序列化/反序列化技术也有很多种,比如Java的原生序列化方式、JSON、阿里的Hessian和ProtoBuff序列化等,它们在效率上存在差异,但又有各自的特点。

  除上面提到的三个问题外,生产中使用的RPC框架要考虑的东西还有很多,在此就不作探讨了。本文的目的就是为了让各位看官对RPC框架有一个感性的、较为深入的了解,如果达到了这一目的,笔者的目的基本就算达到了。


五. 总结
  本文阐述了远程调用的产生背景,然后介绍了RPC的基本概念和要解决的问题,之后手动实现了简易得RPC框架并佐以实例进行演示,使看官们对RPC有一个感性完整的认识,最后讨论了RPC框架的几个重要问题。总之,RPC框架的精髓在于动态代理和反射,通过它们使得远程调用“本地化”,对用户透明且友好。

出处:https://blog.csdn.net/justloveyou_/article/details/79441306

RPC框架原理简述:从实现一个简易RPCFramework说起(转)的更多相关文章

  1. 自行实现一个简易RPC框架

    10分钟写一个RPC框架 1.RpcFramework package com.alibaba.study.rpc.framework; import java.io.ObjectInputStrea ...

  2. 实现一个简易的Unity网络同步引擎——netgo

    实现一个简易的Unity网络同步引擎Netgo 目前GOLANG有大行其道的趋势,尤其是在网络编程方面.因为和c/c++比较起来,虽然GC占用了一部分机器性能,但是出错概率小了,开发效率大大提升,而且 ...

  3. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

  4. 自己来实现一个简易的OCR

    来做个简易的字符识别 ,既然是简易的 那么我们就不能用任何的第三方库 .啥谷歌的 tesseract-ocr, opencv 之类的 那些玩意是叼 至少图像处理 机器视觉这类课题对我这种高中没毕业的人 ...

  5. 基于 getter 和 setter 撸一个简易的MVVM

    Angular 和 Vue 在对Angular的学习中,了解到AngularJS 的两个主要缺点: 对于每一次界面时间,Ajax 或者 timeout,都会进行一个脏检查,而每一次脏检查又会在内部循环 ...

  6. 探秘Tomcat——一个简易的Servlet容器

    即便再简陋的服务器也是服务器,今天就来循着书本的第二章来看看如何实现一个servlet容器. 背景知识 既然说到servlet容器这个名词,我们首先要了解它到底是什么. servlet 相比你或多或少 ...

  7. 使用Windows Form 制作一个简易资源管理器

    自制一个简易资源管理器----TreeView控件 第一步.新建project,进行基本设置:(Set as StartUp Project:View/Toolbox/TreeView) 第二步.开始 ...

  8. [后端人员耍前端系列]AngularJs篇:使用AngularJs打造一个简易权限系统

    一.引言 上一篇博文已经向大家介绍了AngularJS核心的一些知识点,在这篇博文将介绍如何把AngularJs应用到实际项目中.本篇博文将使用AngularJS来打造一个简易的权限管理系统.下面不多 ...

  9. ENode 2.0 - 第一个真实案例剖析-一个简易论坛(Forum)

    前言 经过不断的坚持和努力,ENode 2.0的第一个真实案例终于出来了.这个案例是一个简易的论坛,开发这个论坛的初衷是为了验证用ENode框架来开发一个真实项目的可行性.目前这个论坛在UI上是使用了 ...

随机推荐

  1. 如何去掉C#字符串中的所有空格(转载)

    如何去掉C#字符串中的所有空格 来源:https://www.cnblogs.com/donchen/p/8966059.html 字符串行数Trim()可以去掉字符串前后的空格,如:  C# Cod ...

  2. SQL使用总结

    本文为转载:对于SQL的学习与使用,推荐大家去这儿,讲的很系统: http://www.w3school.com.cn/sql/index.asp 练习SQL的使用,推荐大家去这里: https:// ...

  3. 程序员50题(JS版本)(一)

    程序1:有1.2.3.4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少? for(var i=1,sum=0;i<=4;i++){ for(var j=1;j<=4;j++){ ...

  4. js知识点总结

    组成 ECMAscript 基础语法     变量 数据类型 运算符 数组 函数 对象BOM 浏览器对象模型     window对象(获取浏览器宽高)     history对象     locat ...

  5. 如何解决升级到Dynamics 365后有很多的Sandbox的WorkerProcess并导致异常?

    关注本人微信和易信公众号: 微软动态CRM专家罗勇 ,回复254或者20170505可方便获取本文,同时可以在第一间得到我发布的最新的博文信息,follow me!我的网站是 www.luoyong. ...

  6. mysql innodb存储引擎和一些参数优化

    mysql 的innodb存储引擎是事务性引擎,支持acid.innodb支持版本控制和高并发的技术是svcc:需要重点注意:myisam只缓存索引,innodb缓存索引和数据:

  7. C#判断远程计算机的指定端口是否打开的代码

    如下的内容段是关于C#判断远程计算机的指定端口是否打开的内容,应该能对小伙伴有一些用. using System.Net;if(!string.IsNullOrEmpty(txtPort.Text)) ...

  8. C#自定义FTP访问类的代码

    如下资料是关于C#自定义FTP访问类的代码,应该对各朋友有帮助. using System; using System.Collections.Generic; using System.Text; ...

  9. Numpy库的下载与安装总结

    今天在做Plotly的散点图时,需要Numpy 这个库的使用 没有安装Numpy这个库的时候,报错一般是下图这样:ModuleNotFoundError: No module named 'numpy ...

  10. c/c++ 多线程 一个线程等待某种事件发生

    多线程 一个线程等待某种事件发生 背景:某个线程在能够完成其任务之前可能需要等待另一个线程完成其任务. 例如:坐夜间列车,为了能够不坐过站, 1,整夜保持清醒,但是这样你就会非常累,不能够睡觉. 2, ...