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

每天早上七点三十,准时推送干货

Photo by Matthew Sichkaruk on Unsplash

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

在实际的使用中,我们会编写各种各样的远程调用,打个比方,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 类动态创建一个代理类对象,这个代理对象实现了传入的接口,也就是说它拥有了接口中所有方法的实现。方法具体的实现可以由用户指定,也就是调用InvocationHandlerinvoke方法。

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框架,阿粉认为有4个点:

  1. 代理问题

  2. 服务实例化问题

  3. 序列化问题

  4. 通信问题

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

  1. 首先通过代理模式解决了每个远程方法联网的问题

  2. 通过传递方法名,方法参数类型,参数,以及后面提及的直接传接口名字的形式解决服务实例化问题,这背后的原理是反射,传这些参数也是给反射用的。

关于另外两个点的延伸

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

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


参考:马士兵 rpc 的演化过程公开课 https://www.bilibili.com/video/BV1Ug4y1875i?p=2

< END >

如果大家喜欢我们的文章,欢迎大家转发,点击在看让更多的人看到。也欢迎大家热爱技术和学习的朋友加入的我们的知识星球当中,我们共同成长,进步。

往期精彩回顾

手把手教你,使用JWT实现单点登录

面试常被问到的 JDK 命令,你知道几个?

Java 生态圈中的嵌入式数据库,哪家强?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,以下是Java生成一个简单rpc框架的代码: 1.定义接口: public interface HelloWorldService { String sayHello(String name); } 2.服务端实现接口方法: public class HelloWorldServiceImpl implements HelloWorldService { @Override public String sayHello(String name) { return "Hello " + name + "!"; } } 3.服务端暴露接口: public class Server { public static void main(String[] args) throws Exception { InetSocketAddress address = new InetSocketAddress("localhost", 8888); RpcExporter.exporter(address); //将服务暴露出去 } } 4.服务端将接口暴露出去: public class RpcExporter { public static void exporter(InetSocketAddress address) throws Exception { ServerSocket serverSocket = new ServerSocket(); //新建服务器Socket serverSocket.bind(address); //根据地址绑定 try { while (true) { Socket socket = serverSocket.accept(); //等待连接 new Thread(new ExporterTask(socket)).start(); //新建线程进行处理 } } finally { serverSocket.close(); } } private static class ExporterTask implements Runnable { Socket client = null; public ExporterTask(Socket client) { this.client = client; } @Override public void run() { ObjectInputStream input = null; ObjectOutputStream output = null; try { //输入输出流获取客户端请求和响应 input = new ObjectInputStream(client.getInputStream()); String interfaceName = input.readUTF(); //接口名 Class<?> service = Class.forName(interfaceName); //获取接口 String methodName = input.readUTF(); //方法名 Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); //参数类型 Object[] arguments = (Object[]) input.readObject(); //实参 Method method = service.getMethod(methodName, parameterTypes); //反射调用服务端的实现方法 Object result = method.invoke(service.newInstance(), arguments); output = new ObjectOutputStream(client.getOutputStream()); //向客户端输出响应 output.writeObject(result); } catch (Exception e) { e.printStackTrace(); } finally { try { if (output != null) output.close(); if (input != null) input.close(); if (client != null) client.close(); } catch (IOException e) { e.printStackTrace(); } } } } } 5.客户端调用: public class Client<T> { public T getRemoteProxy(final Class<?> serviceInterface, final InetSocketAddress addr) { return (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(), new Class<?>[]{serviceInterface}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Socket socket = null; ObjectInputStream input = null; ObjectOutputStream output = null; try { socket = new Socket(); socket.connect(addr); output = new ObjectOutputStream(socket.getOutputStream()); output.writeUTF(serviceInterface.getName()); output.writeUTF(method.getName()); output.writeObject(method.getParameterTypes()); output.writeObject(args); input = new ObjectInputStream(socket.getInputStream()); return input.readObject(); } catch (Exception e) { e.printStackTrace(); } finally { if (output != null) output.close(); if (input != null) input.close(); if (socket != null) socket.close(); } return null; } }); } } 以上是一个简单的Java RPC框架的代码,希望能解决你的问题。 ### 回答2: Java生成一个简单的RPC框架的代码涉及到以下几个主要步骤: 1.定义接口:首先需要定义远程服务接口。这个接口描述了可以从远程调用的方法。 ``` public interface HelloService { String sayHello(String name); } ``` 2.实现接口:编写接口的具体实现类。 ``` public class HelloServiceImpl implements HelloService { @Override public String sayHello(String name) { return "Hello, " + name + "!"; } } ``` 3.发布服务:创建一个RPC服务器实例,并将实现类注册到服务器上。 ``` public class RpcServer { public static void main(String[] args) { HelloService helloService = new HelloServiceImpl(); RpcServerHandler handler = new RpcServerHandler(); handler.registerService(HelloService.class, helloService); RpcServerTransport serverTransport = new RpcServerTransport(); serverTransport.start(handler); } } ``` 4.远程调用:创建客户端代码来调用远程服务。 ``` public class RpcClient { public static void main(String[] args) { RpcClientTransport clientTransport = new RpcClientTransport(); clientTransport.init(); HelloService helloService = clientTransport.create(HelloService.class); String result = helloService.sayHello("world"); System.out.println(result); clientTransport.close(); } } ``` 以上所示的代码是一个简单的RPC(远程过程调用)框架的基本实现。它通过网络传输数据实现了远程方法调用的功能。实现RPC框架需要考虑到网络传输、序列化、线程池等方面的细节。以上代码中的`RpcServerHandler`负责处理远程请求,`RpcServerTransport`用于监听客户端请求并转发给`RpcServerHandler`进行处理,`RpcClientTransport`负责发送请求给服务器并接收响应。而真正的业务逻辑则在`HelloServiceImpl`中实现。这个简单的RPC框架可以作为一个起点,可以根据需求逐步扩展和完善。 ### 回答3: RPC(Remote Procedure Call)是一种用于实现远程调用的通信协议。在Java中,我们可以使用Java的网络编程和反射机制来实现一个简单的RPC框架。 首先,我们需要定义一个通信协议,用于客户端和服务端之间的通信。可以使用Socket实现网络通信。客户端向服务端发送请求,服务端接收请求并执行相应的方法,然后将结果返回给客户端。 接下来,我们需要定义一个接口,用于客户端和服务端之间的方法调用。这样,客户端就可以通过调用这些方法来远程调用服务端的方法。同时,服务端也需要实现这些方法,用于处理客户端的请求。 接着,我们需要实现一个代理类,用于将客户端的方法调用转发给服务端。在这个代理类中,可以使用Java的反射机制,根据方法名和参数类型来调用服务端的对应方法。然后将结果返回给客户端。 在客户端中,可以通过创建代理对象来远程调用服务端的方法。客户端需要指定服务端的IP地址和端口号,以及需要调用的方法名和参数。 在服务端中,可以创建一个线程池来处理客户端的请求。当有新的请求到达时,服务端会根据请求的方法名和参数类型,调用相应的方法,并将结果返回给客户端。 最后,在客户端和服务端的主程序中,分别创建客户端和服务端的实例,然后启动它们。客户端可以调用代理对象的方法来实现远程调用,而服务端则会监听客户端的请求并执行相应的方法。 以上就是一个简单的Java RPC框架的实现代码。当然,真实的RPC框架会更加复杂,涉及更多的细节。但是通过上面的实现,我们可以理解RPC的基本原理和实现方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值