第 7 章 远程过程调用 RPC 7.1 RPC 机制和实现过程 7.1.1 RPC 机制 1.客户端进程以正常的方式调用客户存根 2.客户存根生成一个消息,然后调用本地操作系统的网络模块,存根进入阻塞状态 3.客户端操作系统将网络消息发送给远程操作系统 4.远程操作系统将网络消息交给服务端存根 5.服务端存根将参数调取出来,而后调用服务端程序 6.服务端程序执行相应的操作,操作完成后将结果返回给服务端存根 7.服务端存根将结果打包成一个消息,而后调用本地操作系统 8.服务端操作系统将含有结果的消息发送给客户端操作系统 9.客户端操作系统将消息交给客户存根,存根从阻塞状态恢复,进入运行状态 10.客户端存根将结果从消息中提取,返回给调用它的客户端过程 要实现一个RPC远程过程调用,需要考虑以下几个主要方面:参数传递、通信协议定制、出错和超时处理等。 7.1.2 传递参数 1.传递值参数 比较简单,只需要将参数的值复制到网络消息的数据中即可。 2.传递引用参数 传递应用参数比较困难。单纯传递参数的引用(也包含指针)是完全没有意义的,因为引用地址传递给远程计算机,其指向的内存位置困难跟 远程系统上完全不同。如果你想支持传递引用参数或者传递对象,你就必须发送参数的副本,将它们防止在远程系统内存中,向它们传递指向服务器 内存的指针,然后将对象发送回客户端,复制它的引用,这一过程是很麻烦并且容易出错的。因此,RPC一般不支持直接传递引用对象。 3.数据格式的统一问题 在本地系统上不存在数据不相容的问题,因为数据格式总是相同的;而在分布式系统中则不同,不同远程机器上可能有不同的字节顺序,不同 的大小整数,以及不同的浮点表示。对于RPC,如果想与异构系统通信,我们就需要制定一个"标准"来对所有数据类型进行编码,让其可以作为 参数传递。数据表示格式可以使用隐式或者显式类型。隐式类型,是指只传递值,而不传递变量的名称或者类型。也可以使用显式传递,指需要 指出每个字段的类型以及值。 7.1.3 通信协议制定 rpc调用的通信协议选择是指协议栈的设计和选择。 广义上协议栈可以分为公有协议和私有协议,例如http、smpp、webservice等都是公有协议;如果是某个公司或者组织内部自定义、自己使用 的协议,没有被国际标准化组织接纳和认可的,往往化为私有协议,例如Thrift协议和蚂蚁金服的bolt协议。 分布式架构所需要的企业内部通信模块,往往采用私有协议来设计和研发。私有协议虽然有很多弊端,比如在通用性上、公网传输的能力相比公有 协议会有劣势。然而,高度定制化的私有协议可以最大程度的提升性能,降低成本,提高灵活性与效率。定制私有协议,可以有效的利用协议里的各个 字段,灵活满足各种通信功能需求:比如CRC校验,Server Fail-Fast 机制和自定义序列化器。 整个协议栈的组成部分,包括编码器、解码器、心跳、命令协议和命令处理器。 1.协议设计 协议设计上,我们需要考虑几个关键问题:协议包括的必要字段与主要业务负载字段:协议里设计的每个字段都应该被使用到,避免 无效字段;需要考虑通信功能特性的支持:比如crc校验,安全校验,数据压缩机制等;需要考虑协议的升级机制:毕竟是私有协议,没有 长期的验证,字段新增或者修改,是有可能发生的,因此升级机制是必须考虑的。 2.私有协议的编解码 协议相关的编解码来说,私有协议需要有核心的编码和解码过程,并且针对业务负载能支持不同的编码与解码机制。需要为不同的私有 协议分别设计对应的编解码过程。 3.命令定义和命令处理器 协议的通信过程,会有各种命令定义,一般分为两种:一种是负载命令,另外一种是控制命令。 负载命令一般是指传输业务的具体数据,比如请求参数,响应结果的命令;控制命令一般为功能管理命令,心跳命令等,它们通常完成 复杂的分布式跨节点的协调功能,以此来保证负载命令通信过程的稳定,是必不可少的一部分。 定义了通信命令,我们还需要定义命令处理器,用来编写各个命令对应的业务处理逻辑。同时,我们需要保存命令与命令处理器的映射 关系,以便在处理阶段,走到正确的处理器。 4.命令协议 读者一般比较了解的协议是序列化协议,它也是协议栈的一部分,同一种协议也可以承载多种序列化方式,以http为例,它既可以承载 文本类序列化方式,例如:xml、json等,也可以承载二进制序列化方式,例如ProtoBuffer、Thrift和Avro。 不同协议在编解码效率和传输效率上有所不同。 5.通信模式 通信模式也是通信协议栈的一部分,包括4种模式,分别为 oneway、sync、future和callback。 1.oneway 不关心响应,请求线程不会被阻塞,但使用时需要注意控制调用流量,进行蓄洪,防止压垮下游业务服务。 2.sync 请求会阻塞请求线程,待响应返回后才能进行下一个请求。这是最常见的一种通信模型。 3.future 调用,在调用过程中不会阻塞线程,但获取结果的过程会阻塞线程。 4.callback 真正的异步调用,不会阻塞线程,结果处理是在异步线程里执行。 7.1.4 出错和超时处理 注意幂等调用和非幂等调用。 7.1.5 通用 RPC 接口 rpc的机制和实现过程中需要处理的重要问题,比如说参数传递、通信协议和出错以及超时处理等。rpc框架的实现往往就是解决上述这些 问题的框架,这些框架都提供了一套类似的支持库组件,用来解决相同的问题,比如说协议转码问题和网络通信问题。 1.名称服务操作 2.绑定操作 3.终端操作 4.安全操作 5.国际化操作 6.封装处理/数据转换操作 7.存根内存管理和垃圾回收 8.程序标识操作 7.2 简易的 Go 语言原生 RPC Go语言原生的rpc过程调用实现起来非常简单。服务端只需要对外提供的远程过程方法和结构体,然后将其注册到rpc服务中,然后客户端 就可以通过其服务名称和方法名称进行rpc方法调用。 7.2.1 实践案例:Go 语言 RPC 过程调用实践 1.首先定义远程过程调用相关接口传入参数和返回参数的数据结构。 type StringRequest s