本文转载自微信公众号「爱笑的架构师」,作者雷小帅。转载本文请联系爱笑的架构师公众号。
2022 年认真干点事!
动手实现一个简易的 RPC 轮子真的很难吗?no no no,很简单的,不信你把文章看完(doge)。
动动手
RPC 框架典型的架构
典型的 RPC 架构大致可以分为三个部分:
(1)服务提供者(RPC Server):运行在服务器端,提供服务接口定义与服务实现类。
(2)注册中心(Registry):运行在服务器端,负责将本地服务发布成远程服务,管理远程服务,提供给服务消费者使用。
(3)服务消费者(RPC Client):运行在客户端,通过远程代理对象调用远程服务。
通过上面的图可以看出,一次简单的 RPC 调用可以分为以下几个步骤:
(1)服务提供者启动后主动向服务注册中心注册机器ip、端口以及提供的服务列表;
(2)服务消费者启动时向服务注册中心获取服务提供方地址列表,在本地缓存一份;
(3)服务消费者通过本地调用的方式调用服务,调用模块收到请求后通过负载均衡策略选取合适的远程服务地址;
(4)协议模块负责将方法、入参等信息序列化(编码)成能够进行网络传输的消息体,并将消息通过网络发送给服务端;
(5)服务端收到消息后进行解码(反序列化操作)。
(6)根据解码结果调用本地的服务进行相关处理;
(7)服务端将处理返回的结果进行序列化(编码),并将结果通过网络发送至服务消费者;
(8)服务消费者收到消息后进行解码最终得到结果;
敲黑板:在不同的 RPC 框架实现中步骤 1、2、3的顺序可能有些不同。
RPC 核心功能
一个完整的商用 RPC 框架有很多功能,最最核心的基本就是三个:服务寻址、数据编解码、网络传输。
服务寻址
如果是本地调用,被调用的方法在同一个进程内,操作系统或虚拟机可以地址空间找到;但是在远程调用中,这是行不通的,因为两个进程的地址空间是完全不一样的,并且也无法知道远端的进程在何处。
要想实现远程调用,我们需要对服务消费者和服务提供者进行约束:
- 在远程过程调用中所有的函数都必须有一个ID,这个 ID 在整套系统中是唯一确定的。
- 服务消费者在做远程过程调用时,发送的消息体中必须携带这个 ID。
- 服务消费者和服务提供者分别维护一个函数和 ID 的对应表。
当服务消费者需要进行远程调用时,它就查一下这个表,找出对应的 ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
上面说的可能比较抽象,通俗一点就是服务消费者如何寻找服务提供者,这就是服务寻址。
服务寻址的实现方式有很多种,比较常见的是:服务注册中心。要调用服务,首先你需要一个服务注册中心去查询对方服务都有哪些实例,然后根据负载均衡策略择优选一。
像 Dubbo 框架的服务注册中心是可以配置的,官方推荐使用 Zookeeper。
数据编解码(序列化和反序列化)
对计算机网络稍微有一点了解的同学都知道,数据在网络中传输是二进制的:01010101010101010,类似这种,只有二进制数据才能在网络中传输。
那一个客户端调用远程服务的一个方法,像方法入参这些必然需要转换成二进制才能进行传输,这种将对象转换成二进制流的过程就叫做序列化编码。
服务端接收到二进制流不能识别,势必要将二进制流转换成对象,这个逆过程就叫做反序列化解码。
一般场景下是可以将序列化编码简称为序列化。
敲黑板:
如果非要较真,严格来说序列化和编码是两个不同的概念,我画一张图大家都明白了。
序列化和编码的对比
序列化+编码的逆过程就是:解码+反序列化。
网络传输
提起网络传输大家脑海里肯定马上就能想到 TCP/IP四层模型、OSI 七层模型,那通常 RPC 会选择那一层作为传输协议呢?
在回答这个问题前我们先看下 RPC 需要网络传输实现什么功能。
客户端的数据经过序列化+编码后,就需要通过网络传输到服务端。网络传输层需要把前面说的函数 ID 和序列化后的参数字节流传给服务端,服务端处理完然后再把序列化后的调用结果传回客户端。
原则上只要能实现上面这个功能的都可以作为传输层来使用,具体协议没有限制。
我们先来看下 TCP 协议,TCP 连接可以是按需连接,需要调用的时候就先建立连接,调用结束后就立马断掉,也可以是长连接,客户端和服务器建立起连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期检测建立的连接是否存活有效。
由此可见 TCP 的性能确实很好,因此市面上大部分 RPC 框架都使用 TCP 协议,但也有少部分框架使用其他协议,比如 gRPC 就基于 HTTP2 来实现的。
敲黑板:
数据编解码和网络传输可以有多种组合方式,比如常见的有:HTTP+JSON, Dubbo 协议+TCP 等。
常见的 RPC 框架
说了这么多 RPC 相关的技术,我们盘点一下市面上常用的 RPC 框架。
- RMI(Sun/Oracle)
- Thrift(Facebook/Apache)
- gRPC(Google)
- Finagle(Twitter)
- Dubbo(阿里巴巴/Apache)
- Motan(新浪微博)
- brpc(百度/Apache)
- ……欢迎大家补充其他的。
总结
(1)服务提供者需要以某种形式提供服务调用相关的信息,包括但不限于服务接口定义、数据结构、或者中间态的服务定义文件。例如Facebook的 Thrift 框架的IDL文件,Web service的 WSDL 文件;服务的消费者需要通过一定的场景获取远程服务调用相关的信息。
(2)远程代理对象:服务消费者用的服务实际是远程服务的本地代理,说白了就是通过动态代理来实现的。
(3)序列化:毕竟是远程通信,需要将对象转化成二进制流进行传输。不同的RPC框架应用的场景不同,在序列化上也会采取不同的技术。
(4)通信:RPC框架与具体的协议无关。Netty 是一个高性能的网络通信框架。
因此要实现一个 RPC 框架,只需要把上面四点实现了就基本完成了。大家学会了吗?