RPC 是一种方便的网络通信编程模型,由于和编程语言的高度结合,大大减少了处理网络数据的复杂度,让代码可读性也有可观的提高。但是 RPC 本身的构成却比较复杂,由于受到编程语言、网络模型、使用习惯的约束,有大量的妥协和取舍之处。
RPC 框架的讨论一直是各个技术交流群中的热点话题,例如阿里的 dubbo、新浪微博的 motan、谷歌的 grpc 以及不久前蚂蚁金服开源的 sofa 都是比较出名的 RPC 框架。
我们在各种操作系统、编程语言生态圈中,多少都会接触过“远程调用”的概念。一般来说,它们指的是用一行简单的代码,通过网络调用另外一个计算机上的某段程序。比如:
远程调用本身是网络通信的一种概念,它的特点是把网络通信封装成一个类似函数的调用。网络通信在远程调用外,一般还有其他的几种概念:数据包处理、消息队列、流过滤、资源拉取等待,它们的差异如下表所示:
方案 | 编程方式 | 信息封装 | 传输模型 | 典型应用 |
---|---|---|---|---|
远程调用 | 调用函数、输入参数、获得返回值 | 使用编程语言的变量、类型、函数 | 发出请求、获得响应 | Java RMI |
数据包处理 | 调用 Send()/Recv(),使用字节码数据、编解码、处理内容 | 把通信内容构造成二进制的协议包 | 发送/接收 | UDP 编程 |
消息队列 | 调用 Put()/Get(),使用“包”对象,处理其包含的内容 | 消息被封装成语言可用的对象或结构 | 对某队列存入一个消息或取出一个消息 | ActiveMQ |
流过滤 | 读取一个流或写出一个流,对流中的单元包即刻处理 | 单元长度很小的统一数据结构 | 连接、发送/接收、处理 | 网络视频 |
资源拉取 | 输入一个资源 ID,获得资源内容 | 请求或响应都包含:头部和正文 | 请求后等待响应 | WWW |
因此在传输协议和编码协议上,我们可以选择不同的方案。比如 WebService 方案就是用的 HTTP 传输协议 +SOAP 编码协议,而 REST 的方案往往使用 HTTP+JSON 协议。
Facebook 的 Thrift 可以定制任何不同的传输协议和编码协议,可以用 TCP+Google Protocol Buffer 也可以用 UDP+JSON 等。
由于屏蔽了网络层,可以根据实际需要来独立的优化网络部分,而无需涉及业务逻辑的处理代码,这对于需要在各种网络环境下运行的程序来说,非常有价值。
可以直接用编程语言来书写数据结构和函数定义,取代编写大量的编码协议格式和分包处理逻辑。对于那些业务逻辑非常复杂的系统,比如网络游戏,可以节省大量定义消息格式的时间。
函数调用模型非常容易学习,不需要学习通信协议和流程,让经验较浅的程序员也能很容易的开始使用网络编程。
由于把网络通信包装成“函数”,需要大量额外的处理,比如需要预生产代码,或者使用反射机制。这些都是额外消耗 CPU 和内存的操作。而且为了表达复杂的数据类型,比如变长的类型 string/map/list,这些都要数据包中增加更多的描述性信息,则会占用更多的网络包长度。
如果是为了某些特定的业务需求,比如传送一个固定的文件,那么应该用 HTTP/FTP 协议模型;如果为了做监控或者 IM 软件,用简单的消息编码收发会更快速高效;如果是为了做代理服务器,用流式的处理会很简单。另外,如果要做数据广播,那么消息队列会很容易做到,而远程调用这几乎无法完成。
因此,远程调用最适合是业务需求多变或者网络环境多变的场景。
RPC 的结构如下图所示:
RPC 服务端通过 RpcServer 去导出(export)远程接口方法,而客户端通过 RpcClient 去引入(import)远程接口方法。客户端像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理 RpcProxy,代理封装调用信息并将调用转交给 RpcInvoker 去实际执行。
在客户端的 RpcInvoker 通过连接器 RpcConnector 去维持与服务端的通道 RpcChannel,并使用 RpcProtocol 执行协议编码(encode)并将编码后的请求消息通过通道发送给服务端。
RPC 服务端接收器 RpcAcceptor 接收客户端的调用请求,同样使用 RpcProtocol 执行协议解码(decode),解码后的调用信息传递给 RpcProcessor 去控制处理调用过程,最后再委托调用给 RpcInvoker 去实际执行并返回调用结果。
RPC 各个组件的职责如下所示:
Go语言的 net/rpc 很灵活,它在数据传输前后实现了编码解码器的接口定义。这意味着,开发者可以自定义数据的传输方式以及 RPC 服务端和客户端之间的交互行为。
RPC 提供的编码解码器接口如下:
type ClientCodec interface {
WriteRequest(*Request, interface{}) error
ReadResponseHeader(*Response) error
ReadResponseBody(interface{}) error
Close() error
}
type ServerCodec interface {
ReadRequestHeader(*Request) error
ReadRequestBody(interface{}) error
WriteResponse(*Response, interface{}) error
Close() error
}
接口 ClientCodec 定义了 RPC 客户端如何在一个 RPC 会话中发送请求和读取响应。客户端程序通过 WriteRequest() 方法将一个请求写入到 RPC 连接中,并通过 ReadResponseHeader() 和 ReadResponseBody() 读取服务端的响应信息。当整个过程执行完毕后,再通过 Close() 方法来关闭该连接。
接口 ServerCodec 定义了 RPC 服务端如何在一个 RPC 会话中接收请求并发送响应。服务端程序通过 ReadRequestHeader() 和 ReadRequestBody() 方法从一个 RPC 连接中读取请求信息,然后再通过 WriteResponse() 方法向该连接中的 RPC 客户端发送响应。当完成该过程后,通过 Close() 方法来关闭连接。
通过实现上述接口,我们可以自定义数据传输前后的编码解码方式,而不仅仅局限于 Gob。
同样,可以自定义 RPC 服务端和客户端的交互行为。实际上,Go标准库提供的 net/rpc/json 包,就是一套实现了 rpc.ClientCodec 和 rpc.ServerCodec 接口的 JSON-RPC 模块。