通过几段 Java 代码带你理解 RPC

开发 架构
RPC 远程过程调用可以说是分布式系统的基础,本文将通过 Java 演示一次普通的 rpc 调用到底发生了什么。

RPC 远程过程调用可以说是分布式系统的基础,本文将通过 Java 演示一次普通的 rpc 调用到底发生了什么。

阿粉曾经在网上看到有人提问,为什么 RPC 要叫作远程过程调用,而不叫作 RMC 远程方法调用。但阿粉认为 RPC 的叫法才是合理的,远程调用的是某个过程,不一定是一个具体的方法。(你只要看过后面第一个版本的代码就能懂了)

[[326379]]

这整个过程可以用一句话概括:机器 A 通过网络与机器B建立连接,A 发送一些参数给 B,B 执行某个过程,并把结果返回给 A。

“在写代码之前,先说一个前置背景,假设我们有一个商品类:

  1. public class Product implements Serializable { 
  2.  
  3.     private Integer id; 
  4.     private String name; 
  5.  
  6.     public Product(Integer id, String name) { 
  7.         this.id = id; 
  8.         this.name = name; 
  9.     } 
  10.  
  11.     //toString() 
  12.      
  13.     //get set 方法 

有一个商品服务接口:

  1. public interface IProductService { 
  2.  
  3.     Product getProductById(Integer id); 

服务端有商品服务接口的实现类:

  1. public class ProductServiceImpl implements IProductService { 
  2.     @Override 
  3.     public Product getProductById(Integer id) { 
  4.         //实际上这里应该去查询数据库获得数据,下面简化了 
  5.         return new Product(id, "手机"); 
  6.     } 

下面我们通过客户端发送一个商品 id 到服务端,服务端获得 id 后通过商品服务类获取商品信息,返回给客户端:

  1. public class Client { 
  2.  
  3.     public static void main(String[] args) throws Exception { 
  4.         //建立Socket 
  5.         Socket socket = new Socket("127.0.0.1", 8888); 
  6.         //获取输出流 
  7.         ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
  8.         DataOutputStream dos = new DataOutputStream(baos); 
  9.         //把商品Id通过网络传到服务端 
  10.         dos.writeInt(123); 
  11.  
  12.         socket.getOutputStream().write(baos.toByteArray()); 
  13.         socket.getOutputStream().flush(); 
  14.  
  15.         //读取服务端返回的商品信息 
  16.         DataInputStream dis = new DataInputStream(socket.getInputStream()); 
  17.         Integer id = dis.readInt();     //商品id 
  18.         String name = dis.readUTF();    //商品名称 
  19.         Product product = new Product(id, name);//通过服务端返回的商品信息生成商品 
  20.  
  21.         System.out.println(product); 
  22.          
  23.         //关闭流资源为了方便阅读,没有做try-catch处理 
  24.         dos.close(); 
  25.         baos.close(); 
  26.         socket.close(); 
  27.     } 
  28.  
  29. public class Server { 
  30.     private static boolean running = true
  31.  
  32.     public static void main(String[] args) throws Exception { 
  33.         //建立服务端Socket 
  34.         ServerSocket ss = new ServerSocket(8888); 
  35.         //不断监听,处理客户端请求 
  36.         while (running) { 
  37.             Socket socket = ss.accept(); 
  38.             process(socket); 
  39.             socket.close(); 
  40.         } 
  41.         ss.close(); 
  42.     } 
  43.  
  44.     private static void process(Socket socket) throws Exception { 
  45.         InputStream is = socket.getInputStream(); 
  46.         OutputStream os = socket.getOutputStream(); 
  47.         DataInputStream dis = new DataInputStream(is); 
  48.         DataOutputStream dos = new DataOutputStream(os); 
  49.  
  50.         //读取客户端发过来的id 
  51.         Integer id = dis.readInt(); 
  52.         //调用服务类生成商品 
  53.         IProductService service = new ProductServiceImpl(); 
  54.         Product product = service.getProductById(id); 
  55.         //把商品的信息写回给客户端 
  56.         dos.writeInt(id); 
  57.         dos.writeUTF(product.getName()); 
  58.         dos.flush(); 
  59.  
  60.         dos.close(); 
  61.         dis.close(); 
  62.         os.close(); 
  63.         is.close(); 
  64.     } 

上面的是 RPC 远程调用的原始简易版本,可以看到联网的代码写死在了客户端中,网络部分代码和getProductById()耦合在了一起,如果想把别的方法改为远程调用还要重新写一遍联网的代码,非常麻烦。

[[326380]]

在实际的使用中,我们会编写各种各样的远程调用,打个比方,IProductService接口以后可能会扩展成这样:

  1. public interface IProductService { 
  2.  
  3.     Product getProductById(Integer id); 
  4.      
  5.     Product getProductByName(String name); 
  6.      
  7.     Product getMostExpensiveProduct(); 

我们总不可能为每个方法都编写一段网络连接的代码吧,我们得想到一种办法为所有的方法都嵌入一段共用的网络连接代码。

那具体应该怎样嵌入呢?这里我们可以用到代理模式。

在 Java 中许多优秀的框架都用到了代理模式做代码嵌入,比如说 Mybatis。它把 JDBC 连接部分的代码通过代理模式嵌入到 sql 语句的周围,让我们专注于写 sql。

首先,服务端的代码要进行修改,由于多个方法共用一套联网的代码,我们要在服务端识别调用的是哪一个方法:

  1. public class Server { 
  2.  
  3.     private static boolean running = true
  4.  
  5.     public static void main(String[] args) throws Exception { 
  6.         //...... 
  7.     } 
  8.  
  9.     private static void process(Socket socket) throws Exception { 
  10.         //获取输入流,输出流 
  11.         InputStream is = socket.getInputStream(); 
  12.         OutputStream os = socket.getOutputStream(); 
  13.         ObjectInputStream ois = new ObjectInputStream(is); 
  14.         ObjectOutputStream oos = new ObjectOutputStream(os); 
  15.         //获取本次远程调用的方法名 
  16.         String methodName = ois.readUTF(); 
  17.         //获取本次远程调用方法的参数类型 
  18.         Class[] parameterTypes = (Class[]) ois.readObject(); 
  19.         //获取具体的参数对象 
  20.         Object[] args = (Object[]) ois.readObject(); 
  21.          
  22.         //创建商品服务类实例,(往后这里可以继续优化) 
  23.         IProductService service = new ProductServiceImpl(); 
  24.         //根据远程获取的方法名和参数,调用相应的方法 
  25.         Method method = service.getClass().getMethod(methodName, parameterTypes); 
  26.         Product product = (Product) method.invoke(service, args); 
  27.         //把结果写回给客户端 
  28.         oos.writeObject(product); 
  29.  
  30.         oos.close(); 
  31.         ois.close(); 
  32.         socket.close(); 
  33.     } 

然后在客户端,我们创建一个新的代理类,对外提供一个 getStub 获取代理类的方法。使用 JDK 的动态代理需要三个参数,一个是类加载器,一个是接口的 class 类,最后一个是InvocationHandler实例。

JDK 动态代理背后的逻辑是这样的:JVM 会根据接口的 class 类动态创建一个代理类对象,这个代理对象实现了传入的接口,也就是说它拥有了接口中所有方法的实现。方法具体的实现可以由用户指定,也就是调用InvocationHandler的invoke方法。

在invoke方法中有三个参数,分别是proxy代理类,method调用的方法,args调用方法的参数。我们可以在invoke方法中对具体的实现方法进行增强,在本案例中就是进行网络调用。
  1. public class Stub { 
  2.  
  3.     public static IProductService getStub() { 
  4.  
  5.         InvocationHandler h = new InvocationHandler() { 
  6.             @Override 
  7.             public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
  8.                 //和服务端建立Socket连接 
  9.                 Socket socket = new Socket("127.0.0.1", 8888); 
  10.                 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream()); 
  11.                 //拿到远程调用的方法名 
  12.                 String methodmethodName = method.getName(); 
  13.                 //拿到远程调用方法的参数类型 
  14.                 Class[] parametersTypes = method.getParameterTypes(); 
  15.                 //把方法名传递给服务端 
  16.                 oos.writeUTF(methodName); 
  17.                 //把方法参数类型传递给服务端 
  18.                 oos.writeObject(parametersTypes); 
  19.                 //把方法参数传递给服务端 
  20.                 oos.writeObject(args); 
  21.                 oos.flush(); 
  22.                 //获取远程调用的返回结果 
  23.                 ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); 
  24.                 Product product = (Product) ois.readObject(); 
  25.  
  26.                 ois.close(); 
  27.                 oos.close(); 
  28.                 socket.close(); 
  29.                 return product; 
  30.             } 
  31.         }; 
  32.         Object o = Proxy.newProxyInstance(IProductService.class.getClassLoader(), new Class[]{IProductService.class}, h); 
  33.         return (IProductService) o; 
  34.     } 

这个新版本比第一个版本又美好了一些,但是其实还可以继续优化。现在我们的代理只能够返回IProductService的实现类,得想办法让它返回任意类型的服务实现类。

[[326381]]

思路和远程调用方法相似,在远程调用方法时,我们把方法的名称,参数类型,参数传递给服务端;现在要动态创建服务类,我们可以把服务接口的名字传给服务端。服务端拿到远程接口的名字后,就可以从服务注册表中找到对应服务实现类。

至于服务实现类如何注册到服务注册表,这里提供一个思路:可以考虑使用 Spring 的注解注入。这和我们平时写 spring 代码是相似的,在创建完服务实现类后我们会加上注解 @Service,这样我们就可以在收到远程调用后,遍历使用了 @Service的Bean,找到对应的实现类。

总结

如果想要搭建一个简易的RPC框架,阿粉认为有4个点:

  • 代理问题
  • 服务实例化问题
  • 序列化问题
  • 通信问题

本篇文章的侧重点是前两个,代理问题和服务实例化问题,我们回顾一下:

  • 首先通过代理模式解决了每个远程方法联网的问题
  • 通过传递方法名,方法参数类型,参数,以及后面提及的直接传接口名字的形式解决服务实例化问题,这背后的原理是反射,传这些参数也是给反射用的。

关于另外两个点的延伸

在网络上传输数据避免不了序列化问题。Java 自带的序列化方式效率比较低,如果要造一个 RPC 框架的话,我们可以考虑引入其他第三方的序列化框架。

通信问题也很重要,现有的 RPC 框架使用的通信协议也不相同,各有各的优缺点。但这部分手动实现可能比较困难,可以根据语言自身封装好的 API 进行选择。

 

责任编辑:赵宁宁 来源: Java极客技术
相关推荐

2022-01-26 10:52:21

代码Python数据库

2022-02-14 21:17:21

RPC框架协议

2022-09-14 14:41:21

RPC框架RPC协议

2014-09-02 10:43:45

RedisRPC

2018-12-10 14:59:20

代码卷积模块架构

2021-05-18 06:55:07

Java AQS源码

2018-01-22 17:02:48

Python字符编码ASCII

2022-03-10 08:31:51

REST接口规范设计Restful架构

2022-02-28 11:10:42

ZGCG1收集器

2023-03-06 07:28:57

RPC框架序列化

2022-11-07 18:36:03

组件RPC框架

2020-03-12 10:16:45

代码Java多线程

2021-05-28 05:30:55

HandleV8代码

2023-12-25 08:04:42

2020-11-27 08:02:41

Promise

2009-12-15 10:10:42

Ruby过程对象

2021-02-20 09:45:02

RPC框架Java

2020-05-26 14:18:41

RPC协议NTLM

2019-10-11 08:41:35

JVM虚拟机语言

2017-11-20 11:05:23

数据库MongoDB索引
点赞
收藏

51CTO技术栈公众号