You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

252 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 加餐 | RPC框架代码实例详解
你好我是何小锋好久不见咱们专栏结课有段时间了这期间我和编辑冬青一起对整个课程做了复盘也认真挨个逐字看了结课问卷中的反馈其中呼声最高的是“想看RPC代码实例”今天我就带着你的期待来了。
还记得我在[\[结束语\]](https://time.geekbang.org/column/article/226573)提到过我在写这个专栏之前把公司内部我负责的RPC框架重新写了一遍。口说无凭现在这个RPC框架已经[开源](https://github.com/joyrpc/joyrpc),接受你的检阅。
下面我就针对这套代码做一个详细的解析,希望能帮你串联已学的知识点,实战演练,有所收获。
## RPC框架整体结构
首先说我们RPC框架的整体架构这里请你回想下[\[第 07 讲\]](https://time.geekbang.org/column/article/207137)在这一讲中我讲解了如何设计一个灵活的RPC框架其关键点就是插件化我们可以利用插件体系来提高RPC的扩展性使其成为一个微内核架构如下图所示
![](https://static001.geekbang.org/resource/image/a3/a6/a3688580dccd3053fac8c0178cef4ba6.jpg "插件化RPC")
这里我们可以看到我们将RPC框架大体分为了四层分别是入口层、集群层、协议层和传输层而这四层中分别包含了一系列的插件而在实际的RPC框架中插件会更多。在我所开源的RPC框架中就超过了50个插件其中涉及到的代码量也是相当大的下面我就通过**服务端启动流程、调用端启动流程、RPC调用流程**这三大流程来将RPC框架的核心模块以及核心类串联起来理解了这三大流程会对你阅读代码有非常大的帮助。
## 服务端启动流程
在讲解服务启动流程之前,我们先看下服务端启动的代码示例,如下:
```
public static void main(String[] args) throws Exception {
DemoService demoService = new DemoServiceImpl(); //服务提供者设置
ProviderConfig<DemoService> providerConfig = new ProviderConfig<>();
providerConfig.setServerConfig(new ServerConfig());
providerConfig.setInterfaceClazz(DemoService.class.getName());
providerConfig.setRef(demoService);
providerConfig.setAlias("joyrpc-demo");
providerConfig.setRegistry(new RegistryConfig("broadcast"));
providerConfig.exportAndOpen().whenComplete((v, t) -> {
if (t != null) {
logger.error(t.getMessage(), t);
System.exit(1);
}
});
System.in.read();
}
```
我们可以看出providerConfig是通过调用exportAndOpen()方法来启动服务端的,那么为何这个方法要如此命名呢?
我们可以看下 exportAndOpen 方法的代码实现:
```
public CompletableFuture<Void> exportAndOpen() {
CompletableFuture<Void> future = new CompletableFuture<>();
export().whenComplete((v, t) -> {
if (t != null) {
future.completeExceptionally(t);
} else {
Futures.chain(open(), future);
}
});
return future;
}
```
这里服务的启动流程被分为了两个部分export创建Export对象以及open打开服务。而服务端的启动流程也被分为了两部分服务端的创建流程与服务端的开启流程。
### 服务端创建流程
![](https://static001.geekbang.org/resource/image/f4/5b/f411bc3b3dbfbfaefe08671b25a5f65b.jpg "服务端创建流程图")
这里的ProviderConfig是服务端的配置对象其中接口、分组、注册中心配置等等的相关信息都在这个配置类中配置流程的入口是调用ProviderConfig的export方法整个流程如下
1. 根据ProviderConfig的配置信息生成registryUrl注册中心URL对象与serviceUrl服务URL对象
2. 根据registryUrl调用Registry插件创建Registry对象Registry对象为注册中心对象与注册中心进行交互
3. 调用Registry对象的open方法开启注册中心对象也就是与注册中心建立连接
4. 调用Registry对象的subscribe方法订阅接口的配置信息与全局配置信息
5. 调用InvokerManager创建Exporter对象
6. InvokerManager返回Exporter对象。
服务端的创建流程实际上就是Exporter对象Exporter对象是调用器Invoker接口的子类Invoker接口有两个子类分别是Exporter与ReferExporter用来处理服务端接收的请求而Refer用来向服务端发送请求这两个类可以说是入口层最为核心的两个类。
在InvokerManager创建Exporter对象时实际上会有一系列的操作而初始化Exporter也会有一系列的操作如创建Filter链、创建认证信息等等。这里不再详细叙述你可以阅读下源码。
### 服务端开启流程
![](https://static001.geekbang.org/resource/image/70/cf/706ce659565164d457efc040c93976cf.jpg "服务端开启流程图")
创建完服务端的Exporter对象之后我们就要开启Exporter对象开启Exporter对象最重要的两个操作就是开启传输层中Server的端口用来接收调用端发送过来的请求以及将服务端节点注册到注册中心上让调用端可以发现到这个服务节点整个流程如下
1. 调用Exporter对象的open方法开启服务端
2. Exporter对象调用接口预热插件进行接口预热
3. Exporter对象调用传输层中的EndpointFactroy插件创建一个Server对象一个Server对象就代表一个端口了
4. 调用Server对象的open方法开启端口端口开启之后服务端就可以提供远程服务了
5. Exporter对象调用Registry对象的register方法将这个调用端节点注册到注册中心中。
这里无论是Exporter的open方法、Server的open还是Registry的register方法都是异步方法返回值为CompletableFuture对象这个流程的每个环节也都是异步的。
Server的open操作实际上是一个比较复杂的操作要绑定协议适配器、初始化session管理器、添加eventbus事件监听等等的操作而且整个流程完全异步并且是插件化的。
## 调用端启动流程
在讲解调用端启动流程之前,我们还是先看下代码示例,调用端启动代码示例如下:
```
public static void main(String[] args) {
ConsumerConfig<DemoService> consumerConfig = new ConsumerConfig<>(); //consumer设置
consumerConfig.setInterfaceClazz(DemoService.class.getName());
consumerConfig.setAlias("joyrpc-demo");
consumerConfig.setRegistry(new RegistryConfig("broadcast"));
try {
CompletableFuture<DemoService> future = consumerConfig.refer();
DemoService service = future.get();
String echo = service.sayHello("hello"); //发起服务调用
logger.info("Get msg: {} ", echo);
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
System.in.read();
}
```
调用端流程的启动入口就是ConsumerConfig对象的refer方法ConsumerConfig对象就是调用端的配置对象这里可以看到refer方法的返回值是CompletableFuture与服务端相同调用端的启动流程也完全是异步的下面我们来看下调用端的启动流程。
![](https://static001.geekbang.org/resource/image/01/f3/01ab5a2288cfe1d0aadbe7a38da8c9f3.jpg "调用端启动流程")
调用端具体流程如下:
1. 根据ConsumerConfig的配置信息生成registryUrl注册中心URL对象与serviceUrl服务URL对象
2. 根据registryUrl调用Registry插件创建Registry对象Registry对象为注册中心对象与注册中心进行交互
3. 创建动态代理对象;
4. 调用Registry对象的Open方法开启注册中心对象
5. 调用Registry对象subscribe方法订阅接口的配置信息与全局配置信息
6. 调用InvokeManager的refer方法用来创建Refer对象
7. InvokeManager在创建Refer对象之前会先创建Cluster对象Cluser对象是集群层的核心对象Cluster会维护该调用端与服务端节点的连接状态
8. InvokeManager创建Refer对象
9. Refer对象初始化其中主要包括创建路由策略、消息分发策略、创建负载均衡、调用链、添加eventbus事件监听等等
10. ConsumerConfig调用Refer的open方法开启调用端
11. Refer对象调用Cluster对象的open方法开启集群
12. Cluster对象调用Registry对象的subcribe方法订阅服务端节点变化收到服务端节点变化时Cluster会调用传输层EndpointFactroy插件创建Client对象与这些服务节点建立连接Cluster会维护这些连接
13. ConsumerConfig调用Refer对象封装到ConsumerInvokerHandler中将ConsumerInvokerHandler对象注入给动态代理对象。
在调用端的开启流程中最复杂的操作就是Cluster对象的open操作以及Client对象的open操作。
Cluster对象是集群层的核心对象也是这个RPC框架中处理逻辑最为复杂的对象Cluster对象负责维护该调用端节点集群信息监听注册中心推送的服务节点更新事件调用传输层中的EndpointFactroy插件创建Client对象并且会通过Client与服务端节点建立连接发送协商信息、安全验证信息、心跳信息通过心跳机制维护与服务节点的连接状态。
Client对象的open操作也是有着一系列的操作比如创建Transport对象创建Channel对象生成并记录session信息等等。
Refer对象在构造调用链的时候其最后一个调用链就是Refer对象的distribute方法用来发送远程请求。
动态代理对象内部的核心逻辑就是调用ConsumerInvokerHandler对象的Invoke方法最终就是调用Refer对象我会在下面的RPC调用流程中详细讲下。
## RPC调用流程
讲解完了服务端的启动流程与调用端的启动流程下面我开始讲解RPC的调用流程。RPC的整个调用流程就是调用端发送请求消息以及服务端接收请求消息并处理之后响应给调用端的流程。
下面我就讲解下调用端的发送流程与服务端的接收流程。
### 调用端发送流程
![](https://static001.geekbang.org/resource/image/f8/aa/f875674fd3959193202abb38fdb956aa.jpg "调用端发送流程")
调用端发送流程如下:
1. 动态代理对象调用ConsumerInvokerHandler对象的Invoke方法
2. ConsumerInvokerHandler对象生成请求消息对象
3. ConsumerInvokerHandler对象调用Refer对象的Invoke方法
4. Refer对象对请求消息对象进行处理如设置接口信息、分组信息等等
5. Refer对象调用消息透传插件处理透传信息其中就包括隐式参数信息
6. Refer对象调用FilterChain对象的Invoker方法执行调用链
7. FilterChain对象调用每个Filter
8. Refer对象的distribute方法作为最后一个Filter被调用链最后一个执行。
9. 调用NodeSelecter对象的select方法NodeSelecter是集群层的路由规则节点选择器其select方法用来选择出符合路由规则的服务节点
10. 调用Route对象的route方法Route对象为路由分发器也是集群层中的对象默认为路由分发策略为Failover即请求失败后可以重试请求这里你可以回顾下[\[第 12 讲\]](https://time.geekbang.org/column/article/211261)在这一讲的思考题中我就问过异常重试发送在RPC调用中的哪个环节其实就在此环节
11. Route对象调用LoadBalance对象的select方法通过负载均衡选择一个节点
12. Route对象回调Refer对象的invokeRemote方法
13. Refer对象的invokeRemote方法调用传输层中Client对象向服务端节点发送消息。
在调用端发送流程中最终会通过传输层将消息发送给服务端这里对传输层的操作没有详细的讲解其实传输层内部的流程还是比较复杂的也会有一系列的操作比如创建Future对象、调用FutureManager管理Future对象、请求消息协议转换处理、编解码、超时处理等等的操作。
当调用端发送完请求消息之后,服务端就会接收到请求消息并对请求消息进行处理。接下来我们看服务端的接收流程。
### 服务端接收流程
![](https://static001.geekbang.org/resource/image/65/cb/65b25a2b06f6223e19adaddd992542cb.jpg "服务端接收流程")
服务端的传输层会接收到请求消息并对请求消息进行编解码以及反序列化之后调用Exporter对象的invoke方法具体流程如下
1. 传输层接收到请求触发协议适配器ProtocolAdapter
2. ProtocolAdapter对象遍历Protocol插件的实现类匹配协议
3. 匹配协议之后根据Protocol对象传输层的Server对象绑定该协议的编解码器Codec对象、Channel处理链ChainChannelHandler对象
4. 对接收的消息进行解码与反序列化;
5. 执行Channel处理链
6. 在业务线程池中调用消息处理链MessageHandle插件
7. 调用BizReqHandle对象的handle方法处理请求消息
8. BizReqHandle对象调用restore方法根据连接Session信息处理请求消息数据并根据请求的接口名、分组名与方法名获取Exporter对象
9. 调用Exporter对象的invoke方法Exporter对象返回CompletableFuture对象
10. Exporter对象调用FilterChain的invoke方法
11. FilterChain执行所有Filter对象
12. Exporter对象的invokeMethod方法作为最后一个Filter最后被调用
13. Exporter对象的invokeMethod方法处理请求上下文执行反射
14. Exporter对象将执行反射之后得到的请求结果异步通知给BizReqHandle对象
15. BizReqHandle调用传输层的Channel对象发送响应结果
16. 传输层对响应消息进行协议转换、序列化、编码,最后通过网络传输响应给调用端。
## 总结
今天我们剖析了一款开源的RPC框架的代码主要通过**服务端启动流程、调用端启动流程、RPC调用流程**这三大流程来将RPC框架的核心模块以及核心类串联起来。
在服务端的启动流程中核心工作就是创建和开启Exporter对象。ProviderConfig在创建Exporter对象之前会先创建Registry对象从注册中心中订阅接口配置与全局配置之后才会创建Exporter对象在Exporter开启时会启动一个Server对象来开启一个端口Exporter开启成功之后才会通过Registry对象向注册中心发起注册。
在调用端的启动流程中核心工作就是创建和开启Refer对象开启Refer对象中处理逻辑最为复杂的就是对Cluster的open操作Cluster负责了调用端的集群管理操作其中有注册中心服务节点变更事件的监听、与服务端节点建立连接以及服务端节点连接状态的管理等等。
调用端向服务端发起调用时会先经过动态代理之后会调用Refer对象的invoke方法Refer对象会先对要透传的消息进行处理再执行Filter链调用端最后一个Filter会根据配置的路由规则选择出符合条件的一组服务端节点之后调用Route对象的route方法route方法的内部逻辑会根据配置的负载均衡策略选择一个服务端节点最后向这个服务端节点发送请求消息。
服务端的传输层收到调用端发送过来的请求消息在对请求消息进行一系列处理之后如解码、反序列化、协议转换等等会在业务线程池中处理消息关键的逻辑就是调用Exporter对象的invoke方法Exporter对象的invoke方法会执行服务端配置的Filter链最终通过反射或预编译对象执行业务逻辑再将最终结果封装成响应消息通过传输层响应给调用端。
本讲在调用端向服务端发起调用时没有讲到异步调用实际上Refer对象的invoke方法的实现逻辑完全是异步的同样Exporter对象的invoke方法也是异步的Refer类与Exporter类都是调用端Invoker接口的实现类可以看下Invoker接口中invoke方法的定义:
```
/**
* 调用
*
* @param request 请求
* @return
*/
CompletableFuture<Result> invoke(RequestMessage<Invocation> request);
```
JoyRPC框架是一个纯异步的RPC框架所谓的同步只不过是对异步进行了等待。
入口层的核心对象就是Exporter对象与Refer对象这两个类承担了入口层的大多数核心逻辑。
集群层的核心对象就是Cluster对象与Registry对象Cluser对象的内部逻辑还是非常复杂的核心逻辑就是与Registry交互订阅服务端节点变更事件以及对与服务端节点建立的连接的管理这里我们对Cluser对象没有进行过多介绍你可以去查看代码。
协议层的核心对象就是Protocol接口的各个子类了。
接下来就是传输层了传输层的具体实现我们在本讲也没有过多介绍因为很难通过有限的内容把它讲解完整还是建议你去查看下源码一目了然。传输层是纯异步的并且是完全插件化的其入口就是EndpointFactroy插件通过EndpointFactroy插件获取一个EndpointFactroy对象EndpointFactroy对象是一个工厂类用来创建Client对象与Server对象。
对于一个完善的RPC框架今天我们仅是针对服务端启动流程、调用端启动流程、RPC调用流程这三个主流程做了一个大致的讲解真正实现起来还是要复杂许多因为涉及到了很多细节上的问题但主要脉络出来以后相信也会对你有很大帮助更多的细节就还是要靠你自己去阅读源码啦
今天的加餐分享就到这里,有任何问题,欢迎你在留言区与我交流!