定义
RPC(Remote Procedure Call)即远程过程调用,是一种计算机通信协议,它允许程序在不同的计算机之间进行通信和交互,就像本地调用一样。
为什么需要 RPC?
回到 RPC 的概念,RPC 允许一个程序(称为服务消费者)像调用自己程序的方法一样,调用另一个程序(称为服务提供者)的接口,而不需要了解数据的传输处理过程、底层网络通信的细节等。这些都会由 RPC 框架帮你完成,使得开发者可以轻松调用远程服务,快速开发分布式系统。
举个例子,现在有个项目 A 提供了点餐服务,项目 B 需要调用点餐服务完成下单。
点餐服务和接口的示例伪代码如下:
interface OrderService {
// 点餐,返回 orderId
long order(参数1, 参数2, 参数3);
}
如果没有 RPC 框架,项目 B 怎么调用项目 A 的服务呢?
首先,由于项目 A 和项目 B 都是独立的系统,不能像 SDK 一样作为依赖包引入。那么就需要项目 A 提供 web 服务,并且编写一个点餐接口暴露服务,比如访问 http://wys.com
就能调用点餐服务;然后项目 B 作为服务消费者,需要自己构造请求,并通过 HttpClient 请求上述地址。如果项目 B 需要调用更多第三方服务,每个服务和方法的调用都编写一个 HTTP 请求,那么会非常麻烦!
示例伪代码如下:
url = "http://wys.com"
req = new Req(参数1, 参数2, 参数3)
res = httpClient.post(url).body(req).execute()
orderId = res.data.orderId
而有了 RPC 框架,项目 B 可以通过一行代码完成调用!
示例伪代码如下:
orderId = orderService.order(参数1, 参数2, 参数3)
看起来就跟调用自己项目的方法没有任何区别!是不是很丝滑?
基本设计
RPC 框架为什么能帮我们简化调用?如何实现一个 RPC 框架呢?
其实很简单,开局一张图,有服务消费者和服务提供者两个角色:
消费者想要调用提供者,就需要提供者启动一个 web 服务
,然后通过 请求客户端
发送 HTTP 或者其他协议的请求来调用。
比如请求 yupi.icu/order
地址后,提供者会调用 orderService 的 order 方法:
但如果提供者提供了多个服务和方法,每个接口和方法都要单独写一个接口?消费者要针对每个接口写一段 HTTP 调用的逻辑么?
其实可以提供一个统一的服务调用接口,通过 请求处理器
根据客户端的请求参数来进行不同的处理、调用不同的服务和方法。
可以在服务提供者程序维护一个 本地服务注册器
,记录服务和对应实现类的映射。
举个例子,消费者要调用 orderService 服务的 order 方法,可以发送请求,参数为 service=orderService,method=order
,然后请求处理器会根据 service 从服务注册器中找到对应的服务实现类,并且通过 Java 的反射机制调用 method 指定的方法。
需要注意的是,由于 Java 对象无法直接在网络中传输,所以要对传输的参数进行 序列化
和 反序列化
。
为了简化消费者发请求的代码,实现类似本地调用的体验。可以基于代理模式,为消费者要调用的接口生成一个代理对象,由代理对象完成请求和响应的过程。
所谓代理,就是有人帮你做一些事情,不用自己操心。
至此,一个最简易的 RPC 框架架构图诞生了:
上图中的虚线框部分,就是 RPC 框架需要提供的模块和能力。
扩展
1、服务注册发现
问题 1:消费者如何知道提供者的调用地址呢?
类比生活场景,我们点外卖时,外卖小哥如何知道我们的地址和店铺的地址?肯定是买家和卖家分别填写地址,由平台来保存的。因此,我们需要一个 注册中心
,来保存服务提供者的地址。消费者要调用服务时,只需从注册中心获取对应服务的提供者地址即可。
架构图如下:
一般用现成的第三方注册中心,比如 Redis、Zookeeper 即可。
2、负载均衡
问题 2:如果有多个服务提供者,消费者应该调用哪个服务提供者呢?
我们可以给服务调用方增加负载均衡能力,通过指定不同的算法来决定调用哪一个服务提供者,比如轮询、随机、根据性能动态调用等。
架构图如下:
3、容错机制
问题 3:如果服务调用失败,应该如何处理呢?
为了保证分布式系统的高可用,我们通常会给服务的调用增加一定的容错机制,比如失败重试、降级调用其他接口等等。
架构图如下: