第7章 远程过程调用RPC

思考并回答以下问题:

简易的Go语言原生RPC

Go语言官方的RPC库/net/rpc提供了通过网络访问一个对象方法的能力。服务器需要注册对象,通过对象的类型名暴露这个服务。注册后这个对象的输出方法就能进行远程调用了,这个库封装了底层实现的细节,包括序列化、网络传输和反射调用等。服务器可以注册多个不同类型的对象,但是无法注册相同类型的多个对象。同时,如果对象的方法要能远程访问,它们必须满足一定的条件,否则这个对象的方法会被忽略。这些条件是:

(1)方法的类型是可输出的。

(2)方法本身也是可输出的。

(3)方法必须有两个参数,必须是输出类型或者是内建类型。

(4)方法的第二个参数是指针类型。

(5)方法返回类型为error。

根据上述的条件,能够进行远程方法调用的输出方法的格式如下所示

1
func (t *T) MethodName(argType T1, replyType *T2) error

这里的返回值error和输入参数T1、T2都能够被encoding/gob序列化。即使是使用其他的序列化框架,也要满足这项规则。

这个方法的第一个参数T1代表调用者提供的参数,第二个参数T2代表要返回给调用者的结果。如果返回error,则第二个参数T2不会被修改或赋值。

服务器通过调用ServeConn在单独一个连接上处理请求。但是一般来说,它都是创建一个网络监听器,等待客户端建立请求,然后accept请求,最后再处理。

客户端有两个方法调用服务:Call和Go,可以同步地或者异步地调用服务。当然,调用的时候,需要把服务名、方法名和参数传递给服务器。异步方式调用Go方法使用goroutine发起实际的远程调用,然后通过Done channel通知原goroutine远程调用结束,并获取返回结果。

实践案例:Go语言RPC过程调用实践

Go语言原生的RPC.过程调用实现起来非常简单。服务端只需实现对外提供的远程过程方法和结构体,然后将其注册到RPC服务中,然后客户端就可以通过其服务名称和方法名称进行RPC方法调用。

本实例使用字符串操作的服务来展示如何使用Go语言原生的RPC来进行过程调用。

(1)首先定义远程过程调用相关接口传入参数和返回参数的数据结构,如下代码所示,调用字符串操作的请求包括两个参数:字符串A和字符串B。
Bstring;

(2)定义一个服务对象,这个服务对象可以很简单,比如类型是int或者是interface{},重要的是它输出的方法.这里我们定义一个字符串服务类型的interface,其名称为Service,它有两个函数,分别是字符串拼接函数Concat和字符串差异函数Diff。然后定义一个名为StringService的结构体,并且实现Service接口,并给出Concat和Diff函数的具体实现,代码如下:

(3)实现RPC服务器。这里我们生成了一个StringSevice结构体,并使用rpc.Register注册这个服务,然后通过net.IListen监听对应socket并对外提供服务。客户端可以访问服务StringService以及它的两个方法StringService.Concat和StringService.Diff,代码如下:

(4)客户端就可以进行远程调用了。首先建立HTTP客户端,然后通过Call方法调用
远程StringService的对应方法。比如使用同步的方式,代码如下:

Go语言原生的RPC支持同步和异步两种调用方式,分别是使用Client的Call方法和Go方法。同步调用直接会返回响应值,而异步方法则返回这次调用的Call结构体,然后等待Call结构体的Done管道返回调用结果。

接下来的几个小节将对Go语言的RPC原生实现进行源码分析,细致讲解其具体实现和原理。首先对RPC的Server端代码进行分析,包括注册服务、反射处理和存根保存。接着讲解服务端服务处理RPC请求的流程,最后讲解客户(Client)端的RPC请求处理和相关资源重用。

服务端注册实现原理分析

Server端的RPC代码主要分为两个部分,第一部分是服务方法注册,包括调用注册接口,通过反射处理将方法取出,并存到map中;第二部分是处理网络调用,主要是监听端口、读取数据包、解码请求和调用反射处理后的方法,将返回值编码,返回给客户端。第一部分的处理步骤如图7-6所示,先调用Register方法进行StringService.Concat方法的注册,接着使用反射获取注册方法的相关信息,最后进行信息的存根保存,等待处理客户端RPC调用时再使用。

1.注册服务

Register和RegisterName方法是进行RPC服务注册的入口方法,其参数interface{}类型的rcvr就是要注册的RPC服务类型。这两个方法都直接调用了DefaultServer的相应方法,代码如下所示:

上述代码中的DefaultServer是rpc库自带的默认网络Server的实例,它的定义如下所

2.反射处理

我们来具体看一下Server的Register方法的实现。通过反射获取接口类型和值,并通过suitableMethods函数判断注册的RPC是否符合规范,最后调用serviceMap的LoadOrStore(Sname,8)方法将对应RPC存根存放于map中,供之后查找。具体代码如下所示:

1

3.存根保存

suitableMethods函数判断注册的服务类型是否符合规范,它会生成mapSsting]*methodType。它会遍历类型中所有的方法,依次判断方法能否被输出,取出其参数类型和返回类型,。最后统一存储在返回值的map中,实现代码如下所示:

注册完服务并启动网络服务之后,RPC服务器会监听对应的端口,等待客户端RPC请求的到来。

服务端处理RPC请求原理分析

RPC网络调用会使用到Request和Response两个结构体,分别是请求参数和返回参数,通过编解码器(gob/json)实现二进制和结构体的相互转换,它们的定义如下所示:

图7-7展示了服务端RPC程序的处理请求的过程,它会一直循环处理接收到的客户端RPc请求,将其交由ReadRequestHandler处理,然后从之前Register方法保存的map中获取到要调用的对应方法;接着从请求中解码出对应的参数,使用反射调用其方法,获取到结果后将结果编码成响应消息返回给客户端。

1.接收请求

下面,我们来看一下具体的代码实现。首先是Accept函数,它会无限循环的调用net.Listener的Accept函数来获取客户端建立连接的请求,获取到连接请求后,会使用协程来处理请求,代码如下

图7-7 网络请求处理示意图

ServeConn函数会从建立的连接中读取数据,然后创建一个gobServerCodec,并将其交由Server的ServeCodec函数处理,如下所示:

2.读取并解析请求数据
ServeCodec函数会循环地调用readRequest函数读取网络连接上的字节流,解析出请求,然后开启协程执行Server的call函数,处理对应的RPC调用。

readRequestHeader函数是解析RPC请求的关键函数,它会首先解析请求的头部信息,然后获取信息中包含RPC请求的struct名字和方法名字,然后从Server的map中获取到服务端注册的service及其对应方法,源码如下:

readRequest函数会调用readRequestHeader来获取RPC的一些头部信息,然后再解析
消息体中携带的参数,最后初始化响应的返回值类型,源码如下:

3.执行远程方法并返回响应
Server的call函数就是通过Func.Call反射调用对应RPC过程的方法,它还会调用send
Response将返回值发送给RPC客户端,代码如下:

7.2.4客户端发送RPC请求原理分析
无论是同步调用还是异步调用,每次RPC请求都会生成一个Call对象,并使用seq作为key保存在map中,服务端返回响应值时再根据响应值中的seq从map中取出Call,进行相应处理。客户端发起RPC调用的过程大致如下图7-8所示。我们将依次讲解同步调用和异步调用,请求参数编码和接收服务器响应三个部分的具体实现。

图7-8 客户端请求示意图

1.同步调用和异步调用

本章的7.2.1小节展示了Go原生RPC的客户端支持同步和异步两种调用,下面我们来介绍一下这两种调用的函数以及调用的数据结构。

Call方法直接调用了Go方法,而Go方法则是先创建并初始化了Call对象,记录下此次调用的方法、参数和返回值,并生成DoneChannel;然后调用Client的send方法进行真正的请求发送处理,代码如下:

2.请求参数编码

Client的send函数首先会判断客户端实例的状态,如果处于关闭状态,则直接返回结果;否则会生成唯一的seq值,将Call保存到客户端的哈希表pending中,然后调用客户端编码器的WriteRequest来编码请求并发送,代码如下:

客户端默认的编解码器是gobClientCodec,其具体实现如下面的代码所示,它使用gob的Decoder作为编码器。WriteRequest方法则是先使用编码器依次对请求和请求体进行编码,编码后的数据会写入到gobClientCodec的encBuf中,最后调用Flush函数将数据发送到网络数据流中。客户端发起RPC请求到这里就把请求发送出去了。

3.接收服务器响应

接下来我们来看一下客户端是如何接受并处理服务端返回值的。客户端的input函数接收服务端返回的响应值,它进行无限for循环,不断调用codec也就是gobClientCodecd的ReadResponseHeader函数,然后根据其返回数据中的seq来判断是否是本客户端发出请求的响应值。如果是则获取对应的Call对象,并将其从pending哈希表中删除,继续调用codec的ReadReponseBody方法获取返回值Reply对象,并调用Call对象的done方法,代码如下:

上述代码中,gobClientCodecd的ReadResponseHeader、ReadReponseBody方法和上文中的WriteRequest类似,这里不做赘述。Call对象的done方法则通过Call的DoneChannel,
第7章远程过程调用RPC187
将获得返回值的结果通知到调用层,代码如下:

客户端接收到RPC请求的响应后会进行其他业务逻辑操作,RPC框架则会对执行RPC请求所需要的资源进行回收,下次进行RPC请求时则需要再次建立相应的结构体并获取对应的资源,我们可以使用资源重用避免这种情况的发生。

资源重用

为了减少频繁发送RPC请求时不断创建Request和Response结构体所导致的GC压.力,Server对Request和Response进行了复用,构建了一个对象池,可以从池中获取对应的Request和Response对象,使用完之后再使用free函数将其归还到池中,代码如下:

如上代码所示,getRequest方法可以获得一个Request结构体。它会首先从本地缓存的freeReq队列中获取已经存在的Resquest结构体,如果不存在再进行结构体的初始化。freeRequest方法则和它相反,它将业务逻辑代码归还的Request结构体保存到freeReq队列末尾,供后续重复使用。对于Reponse结构体的操作与Request结构体相同。

总的来说,Go语言原生RPC算是个基础版本的RPC框架,代码精简,可扩展性高,但是只实现了RPC最基本的网络通信,像超时熔断、链接管理(保活与重连)、服务注册发现等功能还是欠缺的。因此还是达不到生产环境开箱即用的水准,不过Github就有一个基于RPC的功能增强版本,叫rpcx,支持了大部分主流RPC的特性。

目前官方已经宣布不再添加新功能,并推荐使用gRPC。但是作为Go标准库中的RPC框架,还是有很多地方值得我们借鉴及学习,本节从源码角度分析了Go语言原生RPC框架,希望能给大家带来对RPC框架的整体认识。

0%