学习笔记—RPC框架

什么是RPC

  RPC,全程是Remote Procedure Call,换言之就是远程过程调用。是分布式系统的一种常见的通信方式,已经有许多年的历史了,也算是一种比较古早的技术。

  RPC是一种进程间通信技术,允许程序调用另一个地址空间(通常是远程服务器上)的函数,就像调用本地函数一样。它隐藏了底层的网络通信细节,使得分布式系统中的各个模块能够像在本地系统中一样进行协作。

RPC的基本工作原理

  RPC的核心思想是让一个程序可以通过网络调用另一个程序中的函数或方法,类似于本地调用。它的工作过程通常包括以下几个步骤:

  • 客户端请求: 客户端调用一个本地的代理函数(通常称为“stub”或“代理”),该函数负责将调用的参数打包成一个请求消息。
  • 请求序列化: 该请求消息通过序列化的方式(通常是JSON、XML或二进制格式)转化为一个可传输的数据流。
  • 发送请求: 客户端将序列化后的数据发送到远程服务器。
  • 服务器接收请求: 服务器解包并解析请求,调用相应的服务或方法。
  • 响应: 服务器执行方法后将结果返回,通过网络传输给客户端。
  • 结果返回: 客户端解包响应结果,供调用者使用,整个过程就像在本地调用一样透明。

  如图所示:

RPC工作原理
  可以看到的是一个经典的RPC调用流程,客户端调用本地代理函数后,经过网络传输到服务端,服务端执行响应的函数,再通过网络返回结果。

RPC的主要组成部分

  RPC主要由以下四个部分组成:

  • 客户端代理(Client Stub): 它是客户端代码中的一个本地函数,负责将本地调用转换为网络消息并发送到服务器。
  • 服务器代理(Server Stub): 服务器上的代理程序,它接收客户端的消息,调用本地的服务函数,并将结果返回给客户端。
  • 网络传输协议: 通常使用TCP/IP进行网络通信,但也可以使用HTTP、WebSocket等协议。
  • 序列化和反序列化: 由于客户端和服务器在不同的进程甚至是不同的机器上运行,需要将调用的参数和返回值转化为二进制或文本格式,以便在网络上传输(比如使用Protocol Buffers、Thrift、JSON、XML等格式)。

RPC的类型

  • 同步RPC: 客户端发出请求后,必须等待服务器处理完毕并返回结果,客户端才能继续后续操作。典型的RPC大多是同步调用。
  • 异步RPC: 客户端发出请求后,不必等待服务器返回,客户端可以继续执行其他操作,服务器的响应在稍后时刻以回调或其他方式处理。

RPC的优势之处

  简化分布式系统开发: RPC将网络通信的复杂性隐藏在接口之下,开发人员不必关心底层的网络细节,只需要关注调用接口和业务逻辑。

  灵活的部署架构: 通过RPC,应用可以轻松地在多个服务器之间进行分布式部署,适应大规模应用的需求。

  跨语言支持: 许多RPC框架支持多种编程语言,这使得不同语言之间的服务互操作性得以实现。

RPC的缺点

  调试困难: 由于RPC跨越了网络边界,排查问题时可能会受到网络延迟、超时、服务不可用等因素的影响,调试比本地调用复杂。

  网络开销: 每次RPC调用都需要在网络上传输请求和响应,因此相较于本地调用会有网络延迟和带宽开销。

  耦合度较高: 客户端和服务端通常需要共享相同的接口定义,某些实现上需要双方同时更新以保持兼容性。

RPC比较于REST和消息队列

特性 RPC 消息队列 REST
通信模式 同步/异步 异步为主 同步为主
传输协议 TCP、HTTP/2、HTTP 任意传输协议(常用TCP) HTTP
数据格式 二进制(如Protocol Buffers、Thrift)或文本 通常为消息序列化(JSON、XML、二进制) JSON、XML等文本格式
设计风格 面向过程或函数调用 基于消息的发布-订阅或队列模型 面向资源,操作资源的表现层
典型应用场景 微服务、分布式系统、跨语言服务 日志、事件处理、任务调度 Web API、面向浏览器的服务
调用方式 远程过程调用(函数或方法调用) 发布-订阅或生产-消费模型 基于HTTP动词(GET、POST、PUT等)
依赖 客户端和服务器共享接口定义 消息代理(如RabbitMQ、Kafka等) 无需特别依赖,只需遵循HTTP协议
延迟 较低(但有网络延迟) 较低到中等(取决于队列和网络情况) 较高(HTTP本身较慢,特别是HTTPS)
调试难度 较难(涉及网络、序列化等) 较难(涉及消息丢失、重试、队列溢出等) 较简单(HTTP可视化工具丰富)
扩展性 中等(接口耦合度高) 高(可进行水平扩展) 高(无状态设计,易于扩展)
常见框架/工具 gRPC、Thrift、XML-RPC、JSON-RPC RabbitMQ、Kafka、ActiveMQ Spring Boot(Java)、Django(Python)

  相比之下,在REST和RPC中,RPC的接口设计可以更灵活、封装更强,但REST更简单、标准化,容易与浏览器等客户端交互。

  消息队列(如RabbitMQ、Kafka)常用于异步的消息通信,而RPC更多用于同步的远程调用。消息队列能够缓冲消息,适合高吞吐量的场景,而RPC则倾向于实时、低延迟的交互。

RPC实战

  对于一个简单的RPC实现来说,代码参见:https://github.com/gagaducko/RPC-Framework-Simple

  分为四个主要的模块和一些辅助的模块,即client模块、序列化模块、网络模块和server模块。

  其具体的目录结构如下:

1
2
3
4
5
6
7
8
--RPC-Framework-Simple
----client
----codec
----common
----protocol
----server
----test
----transport

  common是通用工具模块,protocol是网络协议模块,transport是网络传输模块,codec是编解码模块(序列化),server是服务端模块,client是客户端模块,而test则是一个集成测试的模块,用于验证正确性。

  通用工具模块主要是一个反射工具类,用于创建对象、获得公共方法,调用方法等。

  codec编解码会进行序列化和反序列化,采用fastjson.JSON实现。

  protocol和transport作为网络模块,主要负责处理网络请求的handle并定义server服务和client消费者。

1
2
3
4
// handle用于处理网络请求
public interface RequestHandler {
void onRequest(InputStream recive, OutputStream toRespon);
}
1
2
3
4
5
6
7
8
9
// TransportServer
public interface TransportServer {
// 初始化server服务
void init(int port, RequestHandler handler);
// start启动
void start();
// close
void stop();
}
1
2
3
4
5
6
7
8
9
// client消费服务
public interface TransportClient {
// 连接Server服务
void connect(Peer peer);
// 订阅server服务并返回response
InputStream write(InputStream data);
// close
void close();
}

  server服务端主要调用transport中的HttpTransportServer,将请求在Handle中实现,并封装在Response中,完成服务注册、管理、发现的实现。

  client客户端则选择一个server端点连接,代理反射调用方法。

  test模块的测试代码如下:

1
2
3
4
5
6
7
public class Server {
public static void main(String[] args) {
RpcServer server = new RpcServer(new RpcServerConfig());
server.register(CalcService.class, new CalcServiceImpl());
server.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Client {
public static void main(String[] args) {
RpcClient client = new RpcClient();
CalcService service = client.getProxy(CalcService.class);
int number1 = (int)(Math.random() * 100) + 1;
int number2 = (int)(Math.random() * 100) + 1;
int number3 = (int)(Math.random() * 100) + 1;
int number4 = (int)(Math.random() * 100) + 1;
int add = service.add(number1, number2);
int minus = service.minus(number3, number4);

System.out.println("number1 is: " + number1 + " and number2 is: " + number2);
System.out.println("add number1 and number2 get: " + add);
System.out.println("number3 is: " + number3 + " and number4 is: " + number4);
System.out.println("minus number3 by number4 get: " + minus);
}
}

  运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Server
22:20:17.061 [main] INFO org.eclipse.jetty.util.log - Logging initialized @489ms to org.eclipse.jetty.util.log.Slf4jLog
22:20:17.194 [main] INFO rpc.gagaduck.server.ServiceManager - register service: rpc.gagaduck.test.CalcService minus
22:20:17.194 [main] INFO rpc.gagaduck.server.ServiceManager - register service: rpc.gagaduck.test.CalcService add
22:20:17.197 [main] INFO org.eclipse.jetty.server.Server - jetty-9.4.19.v20190610; built: 2019-06-10T16:30:51.723Z; git: afcf563148970e98786327af5e07c261fda175d3; jvm 1.8.0_381-b09
22:20:17.241 [main] INFO o.e.j.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@4f063c0a{/,null,AVAILABLE}
22:20:17.754 [main] INFO o.e.jetty.server.AbstractConnector - Started ServerConnector@457e2f02{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
22:20:17.754 [main] INFO org.eclipse.jetty.server.Server - Started @1187ms
22:20:26.151 [qtp1798286609-32] INFO r.g.transport.HTTPTransportServer - client connect
client connect
22:20:26.389 [qtp1798286609-32] INFO rpc.gagaduck.server.RpcServer - get request: Request(serviceDescriptor=ServiceDescriptor{clazz='rpc.gagaduck.test.CalcService', method='add', returnType='int', parameterTypes=[int, int]}, params=[1, 2])
22:20:26.413 [qtp1798286609-32] INFO rpc.gagaduck.server.RpcServer - response client
22:20:26.452 [qtp1798286609-22] INFO r.g.transport.HTTPTransportServer - client connect
client connect
22:20:26.452 [qtp1798286609-22] INFO rpc.gagaduck.server.RpcServer - get request: Request(serviceDescriptor=ServiceDescriptor{clazz='rpc.gagaduck.test.CalcService', method='minus', returnType='int', parameterTypes=[int, int]}, params=[10, 8])
22:20:26.453 [qtp1798286609-22] INFO rpc.gagaduck.server.RpcServer - response client
1
2
3
4
5
6
7
# Client

22:26:07.439 [main] INFO r.g.client.RandomTransportSelector - connect server: Peer(host=127.0.0.1, port=3000)
number1 is: 70 and number2 is: 28
add number1 and number2 get: 98
number3 is: 33 and number4 is: 50
minus number3 by number4 get: -17

参考资料

慕课-自己动手实现RPC框架
【微服务】RPC的实现原理


学习笔记—RPC框架
https://gagaducko.github.io/2024/10/08/学习笔记—RPC框架/
作者
gagaduck
发布于
2024年10月8日
许可协议