1 需求分析

RPC 全称 Remote Procedure Call ,简单地来说,它能让使用者像调用本地方法一样,调用远程的接口,而不需要关注底层的具体细节。

例如车辆违章代办功能,如果车辆因为某种原因违章,只需要通过这个违章代办功能(它也许是个APP),我们就能动动手指,而省去了一些跑腿的工作。

不像微服务背景下大家所说的 RPC 框架,如 Dubbo 之类。这个 RPC 框架不提供过多的关于服务注册、服务发现、服务管理等功能。它针对的是这样的一些场景:在内部网络,或者局域网内,两个属于同个业务的系统之间需要通信,而我们又觉得去设计多一种二进制网络协议过于繁琐并且没有必要,这时候如果给客户端开发者一些明确的接口,让他知道实现什么功能该调用什么接口,那么省去的工作量以及开发效率上的提升不言而喻。

这个 RPC 系统基于 Java 语言实现,需求如下:

  • RPC 服务端可以通过一条长连接发布多个接口(Interface),客户端按需生成对应接口的代理。
  • RPC 客户端也可以发布接口,以便在必要的时候,服务端可以主动调用客户端的接口实现
  • 客户端与服务端之间保持长连接并且维持心跳
  • 服务端针对不同的接口实现,可以指定不同的线程池去处理
  • 序列化协议支持扩展
  • 通信协议与具体编程语言无关
  • 支持并发调用,一个RPC客户端实例要求是线程安全的

2. 通信协议设计

高效的通信协议一般是二进制格式的,比较常见的还有文本协议比如说HTTP,为了追求效率,这个 RPC 框架就采用二进制格式。

协议的基本要素

魔数

要了解到,报文是在网络上传输的,安全性比较低,因此有必要采取一些措施使得并不是任何人都可以随随便便往我们的端口上发东西,因此我们对报文要有一个初步的识别功能,这时候“魔数(magic number)”就派上用场了。魔数并不受任何规范约束,没有人可以要求你的魔数应该遵循什么规范,实际上魔数只是我们通信双方都约定的一个“暗号”,不知道这个暗号的人就无法参与进通信中。例如 Java 源文件编译后的 class 文件开头就有一个魔数:0xCAFEBABE,随随便便打开一个class文件用十六进制编辑器查看,就能看到。

Java 虚拟机加载 class 的时候会先验证魔数。如果不是 CAFEBABE 就认为是不合法的 class 文件,并拒绝加载。

不过魔数起到的安全防范作用是非常有限的,“有心人”可以通过抓取网络包就识别出魔数了。因此魔数这个东西其实是“防君子不防小人”。

协议版本

一个协议可能也会有多个版本,例如说 HTTP1.0 和 HTTP1.1,不同版本的协议元素可能发生了改变,解析方式也会发生改变,因此协议设计这一块,需要预留出地方声明协议的版本,通信双方在解析协议或者拼装协议的时候才有迹可循。

报文类型

对于RPC框架来说,报文可能有多种类型:心跳类型报文、认证类型报文、请求类型报文、响应类型报文等。

上下文 ID

RPC 调用其实是一个“请求-响应”的过程,并且跨物理机器,因此每次请求和响应,都必须带上上下文 ID,通信双方才能把请求和响应对应起来。

状态

状态用来标识一次调用时正常结束还是异常结束,通常由被调用方置状态。

请求数据

即发送到服务端的调用请求,通常是序列化后的二进制流,长度不定。

长度编码字段

收报文的一方怎么知道发报文的那一方发了多少字节呢?因此发送方必须在协议里告诉接收方需要接受多少字节才算一个完整的报文。

保留字段

协议一旦被设计,并非一成不变的,日后可能有变动的可能,因此还需要考虑保留一些字节空间作为保留字段,以备日后协议的扩展。

协议设计

结合以上的一些设计原则,具体协议设计如下:

 ------------------------------------------------------------------------
 | magic (2bytes) | version (1byte) |  type (1byte)  | reserved (7bits) |
 ------------------------------------------------------------------------
 | status (1byte) |    id (8bytes)    |        body length (4bytes)     |
 ------------------------------------------------------------------------
 |                                                                      |
 |                   body ($body_length bytes)                          |
 |                                                                      |
 ------------------------------------------------------------------------

3. 链路可靠性

客户端与服务端之间的连接采用 TCP 长连接,一个客户端与服务端之间保持至少一条长连接。接口调用请求的发送,在多条连接之间进行负载均衡。

每条连接在空闲的时候,由客户端主动向服务端发送心跳报文,并且客户端在发现连接失效或断开的时候,自动进行重连。

每个客户端向服务端建立连接后,在正式发起接口调用请求之前,都需要进行check in 操作, check in 操作主要是将客户端的身份标识(identifier)和客户端的心跳间隔告诉服务端。利用 netty 的 handler 责任链机制和自带的 IdleStateHandler,自动检测出连接是否空闲,并在空闲时触发心跳报文的发送。而服务端在客户端 checkin 后,根据客户端的心跳频率,在自己的 handler pipeline 上动态加入一个 IdleStateHandler,来检测出客户端是否已经失联,如果是,则主动关闭连接。

同时,客户端本地将会起一个定时执行任务的线程,定期检查连接是否失效,如果失效,则关闭旧连接,并进行连接的重建。

03-05 23:17