RPC 和 RMI 还没搞懂?

356 阅读12分钟

RPC(Remote Procedure Call,远程过程调用)

RPC 的诞生主要为了解决分布式系统中进程之间的通信问题。传统的网络通信如套接字编程较为复杂,RPC 提供了一种抽象,将底层的网络通信隐藏起来,使得程序员只需关注函数调用本身,而无需处理底层的通信细节,就像调用本地函数一样。RPC 的核心思想是隐藏底层的网络通信细节,给开发者提供一种透明的方式进行远程调用。早期的 RPC 框架主要是基于底层的协议(如 TCP/IP)实现的,后来出现了基于 HTTP、gRPC 等的现代 RPC 实现,极大地简化了跨语言、跨平台的调用。

# 作用

 • 简化远程调用:程序员不需要关心底层网络通信,只需要像调用本地函数一样调用远程服务。
 • 跨网络、进程边界调用:支持不同机器、不同进程之间的函数调用,适用于分布式系统。

# 工作流程

 1. 客户端发起调用请求(类似本地函数调用)。
 2. 客户端通过 RPC 框架将请求序列化并发送给远程服务器。
 3. 服务器接收到请求,进行反序列化,并调用相应的函数。
 4. 服务器执行函数后,返回结果给客户端,客户端通过 RPC 框架反序列化结果并接收返回值。

 比如说,一个方法可能是这样定义的:
  Result funA(String name){…}
  那么:
  首先,要解决通讯的问题,主要是通过在客户端和服务器之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
  第二,要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架,如何连接到B服务器(如主机或IP地址)以及特定的端口,方法的名称名称是什么,这样才能完成调用。比如基于Web服务协议栈的RPC,就要提供一个endpoint URI,或者是从UDDI服务上查找。如果是RMI调用的话,还需要一个RMI Registry来注册服务的地址。
  第三,当A服务器上的应用发起远程过程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数的值要序列化成二进制的形式,也就是序列化(Serialize)或编组(marshal),通过寻址和传输将序列化的二进制发送给B服务器。
  第四,B服务器收到请求后,需要对参数进行反序列化(序列化的逆操作),恢复为内存中的表达方式,然后找到对应的方法(寻址的一部分)进行本地调用,然后得到返回值。

# 例子

 • gRPC:Google 提供的一种现代化、高性能的 RPC 框架,支持多种语言。
 • Thrift:Apache 的跨语言 RPC 框架。

# 应用场景

 • 分布式系统中的服务调用。
 • 微服务架构下的服务间通信。

这里有个概念大家得分清楚,RPC 其实是指集合了通信框架,远程通信协议,应用级的服务框架的一套技术的解决方案,RPC 更多的是封装了“服务发现”,"负载均衡",“熔断降级”等一类面向服务的高级特性。

在一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。

典型 RPC 的使用场景中涉及的技术框架从上层到下层:

# 1.应用层(服务框架)

 这里是服务的具体实现,RPC 封装了业务逻辑和服务相关的高级特性,比如:

  服务发现:自动定位需要调用的服务端实例。
  负载均衡:将请求分发到合适的服务实例,常见策略包括轮询、随机、权重等。
  熔断降级:在服务异常时触发容错机制,防止 cascading failures(连锁故障)。

# 2.RPC 协议层

 负责定义远程调用的行为和消息格式。RPC 协议指明了客户端与服务端如何进行通信,包括如何序列化和反序列化调用参数和返回结果。

 常见的 RPC协议包括:
  gRPC:   基于 HTTP/2 和 Protocol Buffers。
  Thrift: 支持多语言的序列化和通信框架。
  Dubbo:  阿里巴巴的分布式服务框架。
  JSON-RPC、XML-RPC:基于 JSON 或 XML 格式的远程调用协议。

# 3.传输层(网络通信)

 在底层,RPC 需要通过网络协议进行通信。常见的传输协议包括:

  HTTP/HTTP2:大多数现代 RPC 框架(如 gRPC)使用 HTTP/2 进行通信,支持更高效的双向通信和流控。
  TCP/UDP:传统的 RPC 实现,如 Dubbo,可能使用 TCP/IP 直接进行通信。
  QUIC:新兴的传输协议,支持快速的连接建立和流控,正在逐渐被引入。

# 4.序列化/反序列化层

 RPC 需要将函数调用的参数和结果序列化为字节流以进行网络传输,并在接收端反序列化回原始对象。

 常见的序列化协议:

  Protocol Buffers(Protobuf):高效、紧凑的二进制序列化格式,gRPC 默认使用。
  Thrift:支持跨语言的序列化和反序列化。
  JSON、XML:文本格式,便于调试,但效率较低。

# RPC 的核心要素

 服务发现:客户端在调用服务时,必须找到当前可用的服务实例,常用的方式包括注册中心(如 Zookeeper、Eureka)和客户端缓存。
 负载均衡:RPC 调用中,服务框架往往会根据配置的策略自动选择某个服务实例来处理请求,确保服务的高可用性和效率。
 熔断降级:当调用的服务不可用或者延迟过高时,熔断机制会触发,防止故障蔓延,并提供降级方案以确保系统的稳定性。

 补充说明:

  RPC 协议 主要负责程序间的远程调用,包括如何传递数据和结果。协议通常涵盖网络传输、消息格式、调用参数的序列化和反序列化等。
  RPC 协议是封装了底层网络通信的抽象层,使开发者不需要手动编写复杂的套接字通信代码。

 举例说明:

  gRPC:它是 Google 推出的一个高性能、跨语言的 RPC 框架,支持 HTTP/2 作为传输协议,使用 Protocol Buffers 作为序列化格式,并且内置了服务发现、负载均衡、熔断等功能。
  Dubbo:这是阿里巴巴推出的一个服务框架,支持 TCP 传输和多种序列化协议,同时内置了服务发现和负载均衡等高级特性。

既然有 HTTP 请求,为什么还要用 RPC 调用?(为什么要使用自定义 tcp 协议的 rpc 做后端进程通信?)

HTTP是应用层协议,而TCP是传输层协议。相比之下TCP更底层一下,所以效率相对会比HTTP高,因为HTTP相当于对TCP还进行了一次包装。简单来说成熟的rpc库相对http容器,更多的是封装了“服务发现”,"负载均衡",“熔断降级”一类面向服务的高级特性。可以这么理解,rpc框架是面向服务的更高级的封装。如果把一个http servlet容器上封装一层服务发现和函数代理调用,那它就已经可以做一个rpc框架了。

代码事例:客户端(如何调用RPC)

public class RPCClient<T> {
  // 
    public static <T> T getRemoteProxyObj(final Class<?> serviceInterface, final InetSocketAddress addr) {
        // 1.将本地的接口调用转换成JDK的动态代理,在动态代理中实现接口的远程调用
        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;
                        ObjectOutputStream output = null;
                        ObjectInputStream input = null;
                        try{
                            // 2.创建Socket客户端,根据指定地址连接远程服务提供者
                            socket = new Socket();
                            socket.connect(addr);

                            // 3.将远程服务调用所需的接口类、方法名、参数列表等编码后发送给服务提供者
                            output = new ObjectOutputStream(socket.getOutputStream());
                            output.writeUTF(serviceInterface.getName());
                            output.writeUTF(method.getName());
                            output.writeObject(method.getParameterTypes());
                            output.writeObject(args);

                            // 4.同步阻塞等待服务器返回应答,获取应答后返回
                            input = new ObjectInputStream(socket.getInputStream());
                            return input.readObject();
                        } finally {
                            if (socket != null){
                                socket.close();
                            }
                            if (output != null){
                                output.close();
                            }
                            if (input != null){
                                input.close();
                            }
                        }
                    }
                });
    }
}

服务端:(如何绑定调用的方法)

public class ServiceCenter implements Server {
  // 线程池
    private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
  // 服务注册表(被调用的方法接口信息)(类似spring容器)
    private static final HashMap<String, Class> serviceRegistry = new HashMap<String, Class>();
  // 该方法是主要方法
  private static class ServiceTask implements Runnable {
        Socket client = null;

        public ServiceTask(Socket client) {
            this.client = client;
        }

        @Override
        public void run() {
            ObjectInputStream input = null;
            ObjectOutputStream output = null;
            try{
        // 获取运程传递过来的 接口,方法名,参数等信息
                input = new ObjectInputStream(client.getInputStream());
                String serviceName = input.readUTF();
                String methodName = input.readUTF();
                Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
                Object[] arguments = (Object[]) input.readObject();
        // 根据名称从服务中获取注册的接口实例,并调用方法
                Class serviceClass = serviceRegistry.get(serviceName);
                if(serviceClass == null){
                    throw new ClassNotFoundException(serviceName + "not found!");
                }
        // 执行方法,并返回结果
                Method method = serviceClass.getMethod(methodName, parameterTypes);
                Object result = method.invoke(serviceClass.newInstance(), arguments);

                output = new ObjectOutputStream(client.getOutputStream());
                output.writeObject(result);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                if(output!=null){
                    try{
                        output.close();
                    }catch (IOException e){
                        e.printStackTrace();
                    }
                }
                if (input != null) {
                    try {
                        input.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (client != null) {
                    try {
                        client.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

  // 一下方法不是很主要
    private static boolean isRunning = false;
    private static int port;

    public ServiceCenter(int port){
        ServiceCenter.port = port;
    }

    @Override
    public void start() throws IOException {
        ServerSocket server = new ServerSocket();
        server.bind(new InetSocketAddress(port));
        System.out.println("Server Start .....");
        try{
            while(true){
                executor.execute(new ServiceTask(server.accept()));
            }
        }finally {
            server.close();
        }
    }

    @Override
    public void register(Class serviceInterface, Class impl) {
        serviceRegistry.put(serviceInterface.getName(), impl);
    }

    @Override
    public boolean isRunning() {
        return isRunning;
    }

    @Override
    public int getPort() {
        return port;
    }

    @Override
    public void stop() {
        isRunning = false;
        executor.shutdown();
    }

   
}

测试

public class RPCTest {
    public static void main(String[] args) throws IOException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Server serviceServer = new ServiceCenter(8088);
                    serviceServer.register(ServiceProducer.class, ServiceProducerImpl.class);
                    serviceServer.start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        ServiceProducer service = RPCClient.getRemoteProxyObj(ServiceProducer.classnew InetSocketAddress("localhost"8088));
        System.out.println(service.sendData("test"));
    }
}

RMI(Remote Method Invocation,远程方法调用)

随着面向对象编程的兴起,简单的函数调用已经无法满足需求,尤其是在 Java 等面向对象语言中,远程调用需要支持对象的方法调用。Java 提出了 RMI,它基于 RPC 的思想,但增加了对远程对象的支持,使得远程调用不仅限于过程调用,还可以调用对象的属性和方法。所以可以说 RMI 是 Java 特有的一种远程调用技术,允许 Java 程序调用远程 JVM 上的对象方法。与 RPC 不同,RMI 是基于面向对象的模型设计的,能够在网络上调用远程对象的方法。

# 作用

 • 远程对象调用:允许 Java 程序在不同的 JVM 中互相调用对象的方法。
 • 自动处理对象的序列化:RMI 自动处理远程对象的序列化和传递。

# 工作流程

 1. 客户端调用远程对象的方法。
 2. 远程对象的代理对象将方法调用请求发送到服务器。
 3. 服务器反序列化请求,调用远程对象的实际方法。
 4. 执行完毕后,服务器将结果返回客户端。

# 例子

 • Java RMI:Java 的标准库中自带的 RMI 实现,允许 Java 程序间进行远程方法调用。

# 应用场景

 • 基于 Java 的分布式应用开发。
 • Java 程序之间的远程调用,尤其是分布式系统中的对象方法调用。

RMI事例代码

// 服务器端
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
    public String getMessage() throws RemoteException {
        return "Hello from server!";
    }
}

// 客户端
MyRemote service = (MyRemote) Naming.lookup("rmi://localhost/MyRemoteService");
System.out.println(service.getMessage());

总结

特点RPCRMI
语言多语言支持仅限 Java 语言
调用模型函数调用(过程调用)面向对象,调用对象方法
序列化机制通常需要手动指定序列化自动进行对象序列化
复杂性通常更简单,函数级别调用较复杂,涉及对象、方法
应用范围跨语言、跨平台限制在 Java 生态系统中
典型应用gRPC、ThriftJava RMI

RPC(Remote Procedure Call,远程过程调用) 和 RMI(Remote Method Invocation,远程方法调用) 都是实现分布式计算的技术,允许程序在不同的计算机上运行时进行通信与调用。它们本质上都是为了跨进程、跨机器进行通信的解决方案,但有一些区别。可以说 RMI(Remote Method Invocation) 是基于 RPC(Remote Procedure Call) 实现的,但 RMI 进一步扩展了 RPC 的功能,适应了面向对象编程的特点。

关系与区别:

1.基于 RPC:

  • RMI 实现了 RPC 的核心思想,即允许跨网络或跨进程调用远程服务的方法。在这个基础上,RMI 针对 Java 的面向对象特性进行了增强。RPC 关注的是函数调用,而 RMI 则允许在远程系统上调用对象的方法。

2.面向过程 vs. 面向对象:

  • RPC:面向过程编程范式,主要实现远程函数调用(远程过程调用),它的核心在于函数级别的调用。
  • RMI:面向对象编程范式,专注于调用远程对象的方法,RMI 允许将对象作为调用的一部分在网络上传递,而不仅仅是函数或数据。这与 Java 的对象模型更匹配。

3.序列化和对象传递:

  • RMI 提供了对象序列化的功能,能够自动将远程对象传递给其他 JVM 执行。这是 RPC 中没有的,因为 RPC 通常只处理原始数据类型或简单的数据结构,而不会处理复杂的对象。

致谢

更多内容欢迎关注 [ 小巫编程室 ] 公众号,喜欢文章的话,也希望能给小编点个赞或者转发,你们的喜欢与支持是小编最大的鼓励,小巫编程室感谢您的关注与支持。好好学习,天天向上(good good study day day up)。