RPC 远程过程调用可以说是分布式系统的基础, 本文将通过 Java 演示一次普通的 rpc 调用到底发生了什么.
我曾经在网上看到有人提问, 为什么 RPC 要叫作远程过程调用, 而不叫作 RMC 远程方法调用. 个人认为 RPC 的叫法才是合理的, 远程调用的是某个过程, 不一定是一个具体的方法, 你只要看过第一个版本的代码就能懂了.
这整个过程可以用一句话概括: 机器 A 通过网络与机器 B 建立连接, A 发送一些参数给 B,B 执行某个过程, 并把结果返回给 A.
先说一个前置背景, 我们有一个商品类
- public class Product implements Serializable {
- private Integer id;
- private String name;
- public Product(Integer id, String name) {
- this.id = id;
- this.name = name;
- }
- //toString()
- //get set 方法
- }
有一个商品服务接口
- public interface IProductService {
- Product getProductById(Integer id);
- }
服务端有商品服务接口的实现类
- public class ProductServiceImpl implements IProductService {
- @Override
- public Product getProductById(Integer id) {
- // 实际上这里应该去查询数据库获得数据, 下面简化了
- return new Product(id, "手机");
- }
- }
下面我们通过客户端发送一个商品 id 到服务端, 服务端获得 id 后通过通过商品服务类获取商品信息, 返回给客户端
- public class Client {
- public static void main(String[] args) throws Exception {
- // 建立 Socket
- Socket socket = new Socket("127.0.0.1", 8888);
- // 获取输出流
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- DataOutputStream dos = new DataOutputStream(baos);
- // 把商品 Id 通过网络传到服务端
- dos.writeInt(123);
- socket.getOutputStream().write(baos.toByteArray());
- socket.getOutputStream().flush();
- // 读取服务端返回的商品信息
- DataInputStream dis = new DataInputStream(socket.getInputStream());
- Integer id = dis.readInt(); // 商品 id
- String name = dis.readUTF(); // 商品名称
- Product product = new Product(id, name);// 通过服务端返回的商品信息生成商品
- System.out.println(product);
- // 关闭流资源为了方便阅读, 没有做 try-catch 处理
- dos.close();
- baos.close();
- socket.close();
- }
- }
- public class Server {
- private static boolean running = true;
- public static void main(String[] args) throws Exception {
- // 建立服务端 Socket
- ServerSocket ss = new ServerSocket(8888);
- // 不断监听, 处理客户端请求
- while (running) {
- Socket socket = ss.accept();
- process(socket);
- socket.close();
- }
- ss.close();
- }
- private static void process(Socket socket) throws Exception {
- InputStream is = socket.getInputStream();
- OutputStream os = socket.getOutputStream();
- DataInputStream dis = new DataInputStream(is);
- DataOutputStream dos = new DataOutputStream(os);
- // 读取客户端发过来的 id
- Integer id = dis.readInt();
- // 调用服务类生成商品
- IProductService service = new ProductServiceImpl();
- Product product = service.getProductById(id);
- // 把商品的信息写回给客户端
- dos.writeInt(id);
- dos.writeUTF(product.getName());
- dos.flush();
- dos.close();
- dis.close();
- os.close();
- is.close();
- }
- }
上面的是 RPC 远程调用 1.0 版本, 可以看到联网的代码写死在了客户端中, 网络的代码和 getProductById() 耦合在了一起, 实际的 rpc 框架是绝对不可能这么做的.
在实际的使用中, 我们会编写各种各样的远程调用, 打个比方, IProductService 接口以后可能会扩展成这样:
- public interface IProductService {
- Product getProductById(Integer id);
- Product getProductByName(String name);
- Product getMostExpensiveProduct();
- }
我们总不可能为每个方法都编写一段网络连接的代码吧, 我们得想到一种办法为所有的方法都嵌入一段共用的网络连接代码.
那具体应该怎样嵌入呢? 这里我们可以用到代理模式.
在 Java 中许多优秀的框架都用到了代理模式做代码嵌入, 比如说 Mybatis. 它把 JDBC 连接部分的代码通过代理模式嵌入到 sql 语句的周围, 让我们专注于写 sql.
首先, 服务端的代码要进行修改:
- public class Server {
- private static boolean running = true;
- public static void main(String[] args) throws Exception {
- //......
- }
- private static void process(Socket socket) throws Exception {
- // 获取输入流, 输出流
- InputStream is = socket.getInputStream();
- OutputStream os = socket.getOutputStream();
- ObjectInputStream ois = new ObjectInputStream(is);
- ObjectOutputStream oos = new ObjectOutputStream(os);
- // 获取本次远程调用的方法名
- String methodName = ois.readUTF();
- // 获取本次远程调用方法的参数类型
- Class[] parameterTypes = (Class[]) ois.readObject();
- // 获取具体的参数对象
- Object[] args = (Object[]) ois.readObject();
- // 创建商品服务类实例
- IProductService service = new ProductServiceImpl();
- // 根据远程获取的方法名和参数, 调用相应的方法
- Method method = service.getClass().getMethod(methodName, parameterTypes);
- Product product = (Product) method.invoke(service, args);
- // 把结果写回给客户端
- oos.writeObject(product);
- oos.close();
- ois.close();
- socket.close();
- }
- }
然后在客户端, 我们创建一个新的代理类, 对外提供一个 getStub 获取代理类的方法. 使用 JDK 的动态代理需要三个参数, 一个是类加载器, 一个是接口的 class 类, 最后一个是 InvocationHandler 实例.
JDK 动态代理背后的逻辑是这样的: JVM 会根据接口的 class 类动态创建一个代理类对象, 这个代理对象实现了传入的接口, 也就是说它拥有了接口中所有方法的实现. 方法具体的实现可以由用户指定, 也就是调用 InvocationHandler 的 invoke 方法.
在 invoke 方法中有三个参数, 分别是 proxy 代理类, method 调用的方法, args 调用方法的参数. 我们可以在 invoke 方法中对具体的实现方法进行增强, 在本案例中就是进行网络调用.
- public class Stub {
- public static IProductService getStub() {
- InvocationHandler h = new InvocationHandler() {
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- // 和服务端建立 Socket 连接
- Socket socket = new Socket("127.0.0.1", 8888);
- ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
- // 拿到远程调用的方法名
- String methodName = method.getName();
- // 拿到远程调用方法的参数类型
- Class[] parametersTypes = method.getParameterTypes();
- // 把方法名传递给服务端
- oos.writeUTF(methodName);
- // 把方法参数类型传递给服务端
- oos.writeObject(parametersTypes);
- // 把方法参数传递给服务端
- oos.writeObject(args);
- oos.flush();
- // 获取远程调用的返回结果
- ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
- Product product = (Product) ois.readObject();
- ois.close();
- oos.close();
- socket.close();
- return product;
- }
- };
- Object o = Proxy.newProxyInstance(IProductService.class.getClassLoader(), new Class[]{IProductService.class}, h);
- return (IProductService) o;
- }
- }
这个新版本比第一个版本又美好了一些, 但是其实还可以继续优化. 现在我们的代理只能够返回 IProductService 的实现类, 得想办法让它返回任意类型的服务实现类.
思路和远程调用方法相似, 在远程调用方法时, 我们把方法的名称, 参数类型, 参数传递给服务端; 现在要动态创建服务类, 我们可以把服务接口的名字传给服务端. 服务端拿到远程接口的名字后, 就可以从服务注册表中找到对应服务实现类.
至于服务实现类如何注册到服务注册表, 这里提供一个思路: 可以考虑使用 Spring 的注解注入. 这和我们平时写 spring 代码是相似的, 在创建完服务实现类后我们会加上注解 @Service, 这样我们就可以在收到远程调用后, 遍历使用了 @Service 的 Bean, 找到对应的实现类.
参考: 马士兵 rpc 的演化过程公开课
https://www.bilibili.com/video/BV1Ug4y1875i?p=2
来源: https://www.cnblogs.com/tanshaoshenghao/p/12767414.html