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.class, new 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());
总结
| 特点 | RPC | RMI |
|---|---|---|
| 语言 | 多语言支持 | 仅限 Java 语言 |
| 调用模型 | 函数调用(过程调用) | 面向对象,调用对象方法 |
| 序列化机制 | 通常需要手动指定序列化 | 自动进行对象序列化 |
| 复杂性 | 通常更简单,函数级别调用 | 较复杂,涉及对象、方法 |
| 应用范围 | 跨语言、跨平台 | 限制在 Java 生态系统中 |
| 典型应用 | gRPC、Thrift | Java 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)。