前言
本文是分享一个 RPC 的设计与实现,当然只是简易版的轮子,但非常适合用来学习 RPC 的原理。没有复杂的线程模型,只是简简单单的实现。
首先大家可能会觉得写一个框架很麻烦,但实际上只是和写业务代码没什么区别。只是用到的代码稍微比业务代码基础一些,当然了要实现一个可扩展、高性能、易用的框架肯定需要扎实的基础知识支撑。但是仅仅作为熟悉原理的框架,就不需要了解那么深厚的基础。
本文适用的读者为使用过 RPC 框架,dubbo 尤佳,并对其实现原理感兴趣的同学。首先我们来梳理核心的一些抽象概念。
首先RPC是为了解决跨进程调用出现的一种解决方案。调用远程方法不需要考虑有多少服务提供者,对消费者(调用方)而言只有一个副本。简单来说,需要实现以下一种特性:调用远程方法像调用本地方法一样。
至于IPC我们使用TCP,对应到实现使用Netty网络通信框架,同时网络间传输需要使用序列化,可能还要设计自己的通信协议。由于太过麻烦,我们直接使用sofa-bolt来做为通信层实现。看一下简介:
SOFABolt 是蚂蚁金融服务集团开发的一套基于 Netty 实现的网络通信框架。 为了让 Java 程序员能将更多的精力放在基于网络通信的业务逻辑实现上,而不是过多的纠结于网络底层 NIO 的实现以> 及处理难以调试的网络问题,Netty 应运而生。 为了让中间件开发者能将更多的精力放在产品功能特性实现上,而不是重复地一遍遍制造通信框架的轮子,SOFABolt 应> 运而生。
也就是说从请求参数的封装,序列化、编码、网络传输、解析请求、处理请求、封装返回消息数据、在进行返回数据的序列化、编码、在通过网络返回给客户端这些复杂的过程。 我们选择直接使用 sofa-bolt从而极大的减轻我们我们的编码量(感谢开源社区)
客户端子调用时我们需要屏蔽掉复杂的网络请求处理代码,我们选择使用动态代理。可选的有jdkProxy、javassist、cglib。为了代码的基础性,我们选择使用原生jdkProxy。
千里之行,始于足下。接下来正式开始我们的编码之旅。
正文
模型设计
首先我们需要进行模型设计。为了方便理解,我画了个图,算了不画了我找了张图(图片来自网络)。
- 客户端发起远程调用的流程
RpcClient其实就是消费者的进程,引入服务提供者的Jar包,调用了某个接口的函数。- 进行到代理类
RpcProxy中(就是动态代理生成的类,封装后续的网络请求细节等) - 代理类中出现
RpcInvoker这是真正的调用者,里面会请求网络通信。 RpcConnector、RpcProtocal这些都认为是网络通信的模块,我们框架中直接使用sofa-bolt也就看不到协议。
- 服务端发布服务的流程
RpcServer即服务提供者的进程,调用export方法对外发布服务,同时开放TCP服务端口。- 接着
RpcAcceptor接收到上面来自客户端的网络请求,进行解码后被RpcProcessor网络请求处理器接收到。 - 找到服务提供者的
RpcInvoker进行反射调用,最后将程序执行结果返回给客户端。
整个流程简单来说就是这样,其中缺少了服务注册与发现,常见的可以使用Zookeeper。我们这里选择使用本地文件的形式,因为暂时不考虑集群。服务提供者在启动时启动TCP服务端口,消费者在进行网络请求时访问该端口(短连接)。
整体结构
其实花了一天时间代码已经写完了,整体结构如下:
下面依次介绍下包的结构:
| 包名 | 介绍 |
|---|---|
| api | 消费者和提供者通过这个包进行服务引用和发布 |
| discover | 服务注册与发现 |
| exception | 你懂得 |
| invoker | 提供者和消费者的真实调用者 |
| proxy | 实现消费者和提供者的动态代理 |
| remoting | 通信层 |
| utils | 你懂得 |
其中还有一些模型,如URL、request、response、context都是请求时一些胶水对象。
使用效果
提供者:
public class ProviderTest {
@Test
public void testExport() throws InterruptedException {
final Provider provider = new Provider();
provider.setServiceClass(IEcho.class);
provider.setRef(EchoImpl.class);
provider.setUrl(new URL("127.0.0.1", 4399));
provider.export();
Thread.currentThread().join();
}
}
消费者:
@Slf4j
public class ConsumerTest {
@Test
public void test() throws InterruptedException {
Consumer consumer = new Consumer();
consumer.setServiceClass(IEcho.class);
final IEcho ref = consumer.ref();
log.info("获取代理对象:" + ref);
for (int i = 0; i < 100; i++) {
final String result = ref.echo("1111");
System.out.println(result);
TimeUnit.SECONDS.sleep(5);
}
}
}
收到请求后的提供者:
消费者响应:
本地文件实现的服务注册与发现:
整体代码非常简单,其中重要的两个代理是:
提供者代理类:
消费代理类:
Invoker的具体实现:
服务提供者:
服务消费者:
后语
本文是对一个简单 RPC 的实验,由于时间的缘故没有复杂的功能。希望对大家有所帮助,代码在这里。