拓展|如何设计一个短小精悍、可拓展的RPC框架?(含实现代码)

本文来自公众号读者王码农的投稿
感谢王码农同学的技术分享
简介
如果大家对RPC有一些了解的话,或多或者都会听到过一些大名鼎鼎的RPC框架,比如Dobbo、gRPC。但是大部分人对于他们底层的实现原理其实不甚了解。
有一种比较好的学习方式:就是如果你想要了解一个框架的原理,你可以尝试去写一个简易版的框架出来,就比如如果你想理解Spring IOC的思想,最好的方式就是自己实现一个小型的IOC容器,自己慢慢体会。
所以本文尝试带领大家去设计一个小型的RPC框架,同时对于框架会保持一些拓展点。
通过阅读本文,你可以收获:
理解RPC框架最核心的理念
学习在设计框架的时候,如何保持拓展性
本文会依赖一些组件,他们是实现RPC框架必要的一些知识,文中会尽量降低这些知识带来的障碍。但是,最好期望读者有以下知识基础:
Zookeeper基本入门
Netty基本入门
RPC框架应该长什么样子
我们首先来看一下:一个RPC框架是什么东西?
我们最直观的感觉就是:
集成了RPC框架之后,通过配置一个注册中心的地址。一个应用(称为服务提供者)将某个接口(interface)“暴露”出去,另外一个应用(称为服务消费者)通过“引用”这个接口(interface),然后调用了一下,就很神奇的可以调用到另外一个应用的方法了
给我们的感觉就好像调用了一个本地方法一样。即便两个应用不是在同一个JVM中甚至两个应用都不在同一台机器中。
那他们是如何做到的呢?
其实啊,当我们的服务消费者调用某个RPC接口的方法之后,它的底层会通过动态代理,然后经过网络调用,去到服务提供者的机器上,然后执行对应的方法。
接着方法的结果通过网络传输返回到服务消费者那里,然后就可以拿到结果了。
整个过程如下图:
那么这个时候,可能有人会问了:服务消费者怎么知道服务提供者在哪台机器的哪个端口呢?
这个时候,就需要“注册中心”登场了,具体来说是这样子的:
服务提供者在启动的时候,将自己应用所在机器的信息提交到注册中心上面。
服务消费者在启动的时候,将需要消费的接口所在机器的信息抓回来
这样一来,服务消费者就有了一份服务提供者所在的机器列表了
 拓展|如何设计一个短小精悍、可拓展的RPC框架?(含实现代码)
文章图片
“服务消费者”拿到了“服务提供者”的机器列表就可以通过网络请求来发起请求了。
网络客户端,我们应该采用什么呢?有几种选择:
使用JDK原生BIO(也就是ServerSocket那一套)。阻塞式IO方法,无法支撑高并发。
使用JDK原生NIO(Selector、SelectionKey那一套)。非阻塞式IO,可以支持高并发,但是自己实现复杂,需要处理各种网络问题。
使用大名鼎鼎的NIO框架Netty,天然支持高并发,封装好,API易用。
作为一个有追求的程序员,我们要求开发出来的框架要求支持高并发、又要求简单、还要快。当然是选择Netty来实现了,使用Netty的一些很基本的API就能满足我们的需求。
网络协议定义
当然了,既然我们要使用网络传输数据。我们首先要定义一套网络协议出来。
你可能又要问了,啥叫网络协议?
网络协议,通俗理解,意思就是说我们的客户端发送的数据应该长什么样子,服务端可以去解析出来知道要做什么事情。话不多说,上代码:
假设我们现在服务提供者有两个类:
现在我要调用HelloService.sayHello(TestBean testBean)这个方法
作为“服务消费者”,应该怎么定义我们的请求,从而让服务端知道我是要调用这个方法呢?
这需要我们将这个接口信息产生一个唯一的标识: 这个标识会记录了接口名、具体是那个方法、然后具体参数是什么!
然后将这些信息组织起来发送给服务端,我这里的方式是将信息保存为一个JSON格式的字符串来传输。
比如上面的接口我们传输的数据大概是这样的:
嗯,我这里用一个JSON来标识这次调用是调用哪个接口的哪个方法,其中interface标识了唯一的类,parameter标识了里面具体有哪些参数, 其中key就是参数的类全限定名,value就是这个类的JSON信息。
可能看到这里,大家可能有意见了: 数据不一定用JSON格式传输啊,而且使用JSON也不一定性能最高啊。
你使用JDK的Serializable配合Netty的ObjectDecoder来实现,这当然也可以,其实这里是一个拓展点,我们应该要提供多种序列化方式来供用户选择
但是这里选择了JSON的原因是因为它比较直观,对于写文章来说比较合理。
开发服务提供者
嗯,搞定了网络协议之后,我们开始开发“服务提供者”了。对于服务提供者,因为我们这里是写一个简单版本的RPC框架,为了保持简洁。
我们不会引入类似Spring之类的容器框架,所以我们需要定义一个服务提供者的配置类,它用于定义这个服务提供者是什么接口,然后它具体的实例对象是什么:
有了这个东西之后,我们就知道需要暴露哪些接口了。
为了框架有一个统一的入口,我定义了一个类叫做,可以认为这是一个应用程序上下文,他的构造函数,接收2个参数,代码如下:


推荐阅读