第一部分链接:https://blog.csdn.net/qq_38093301/article/details/104210592
3、客户端请求发起
在与RPC服务端建立http连接并验证后,客户端程序将使用本次的底层TCP连接Conn对象建立一个Client实例,并开启一个协程接受服务端返回消息。Client实例建立过程:
func NewClient(conn io.ReadWriteCloser) *Client {
encBuf := bufio.NewWriter(conn)
client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
return NewClientWithCodec(client)
}
可以看到首先使用TCP连接conn建立了Client中一个重要的成员:gobClientCodec,可以翻译为gob客户端编码译码器,其包含了conn连接对象以及使用它创建的gob编码和解码器对象、缓冲写对象。随后,将该实例作为一个成员,调用NewClientWithCodec()函数建立Client实例。
func NewClientWithCodec(codec ClientCodec) *Client {
client := &Client{
codec: codec, //gob编码解码器
pending: make(map[uint64]*Call), //rpc请求等待区
}
go client.input() //启动input()协程处理接收内容
return client
}
在这个函数中,只对Client对象的编码解码器和rpc等待字典两个成员进行了复制,然后启动了input()协程监听服务器的回复内容,就完成了Client客户端实例的创建,一个客户端实例的完整内容包括:
type Client struct {
codec ClientCodec //gob编码解码器
reqMutex sync.Mutex // 请求队列互斥锁
request Request //请求队列
mutex sync.Mutex // 后续属性的互斥锁
seq uint64 //等待请求序号
pending map[uint64]*Call //等待请求字典
closing bool // 连接关闭标记
shutdown bool // 被动关闭标记
}
可以通过研究一个RPC请求的发起和回复流程去学习上述属性的具体功能,以一次rpc远程过程调用的发起为切入点:
err=client.Call("Args.Multiply",args,&reply)
client.Call()方法对服务端发起了远程调用请求,发送了一个“Service.Method”的字符串来唯一的标识想要调用的方法,按照服务端对服务方法的约束,调用方法有且只有两个参数:非指针型的第一参数做为输入,指针型的第二参数用于接收计算结果。
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
return call.Error
}
可以看到,该函数调用Go()函数将参数传入,并附带了一个chan *Call的1缓冲区管道用于通信,该函数运行结束将通过返回值拿回该管道(Done),用管道的方式拿到call请求实例,该结构包含了本次请求的基本信息,包括RPC调用结束后的错误消息。可以推测大致流程为,执行Client.go()方法后,客户端主线程将在这行程序中等待管道的输出值因此阻塞,直到调用结束,调用程序将调用结果反映到Call结构中,通过管道返还给主协程,结束主协程的阻塞。
type Call struct {
ServiceMethod string // 要调用的服务和方法名
Args interface{} // 传入方法的参数
Reply interface{} // 方法的返回值
Error error // 调用结束后的错误状态
Done chan *Call // 标记调用结束的管道值
}
go函数将接受到的参数打包后建立Call对象实例,传入Clent.send()函数向服务器发起调用。
func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
call := new(Call)
call.ServiceMethod = serviceMethod
call.Args = args
call.Reply = reply
......限制协程Done被分配值且是带缓冲协程
call.Done = done
client.send(call) //将call实例传递到send函数以发送请求
return call
}
client.go(*call)完成了的发送工作,主要有:
- 互斥锁操作保证互斥访问
- 判断client是否关闭或被断开
- 确定发送序号
- 将本次call实例加入Client的pending队列中
- 将本次请求包装为一个request对象,连同参数调用client.codec.WriteRequest,编码并发送到服务端
func (client *Client) send(call *Call) {
client.reqMutex.Lock() //全程保证req对象的互斥访问
defer client.reqMutex.Unlock()
// Register this call.
client.mutex.Lock() //保证整个Client对象的互斥访问,修改属性
......省略对Client状态的判断部分
seq := client.seq
client.seq++ //得到当前请求序号并将call对象加入请求队列
client.pending[seq] = call
client.mutex.Unlock()
//编码并发送调用请求
client.request.Seq = seq
client.request.ServiceMethod = call.ServiceMethod
err := client.codec.WriteRequest(&client.request, call.Args)
......省略err检查部分
}
编码和发送请求势必是使用包含conn连接和编码、解码器的gob编码解码器codec来完成,主要工作如下:
func (c *gobClientCodec) WriteRequest(r *Request, body interface{}) (err error) {
if err = c.enc.Encode(r); err != nil { //编码请求体
return
}
if err = c.enc.Encode(body); err != nil { //编码请求内容(即参数)
return
}
return c.encBuf.Flush() //写入conn
}
发送过程就此结束,可以注意到的一些细节:
- 客户端和服务端之间的requset队列将使用统一的seq序号来处理,便于多次请求发起后,对回复结果的对号入座。
- 在最终处理过后,发送给服务器的是编码过的目标服务方法字符串和输入参数两部分。
4、服务端处理请求
既然客户端通过gob编码传输了调用请求,在服务端势必也需要一个gob编码解码器进行通信,在获取TCP连接对象conn后,服务点就执行了Server实例建立的工作,其中就包含了服务端gob编码解码器。
func (server *Server) ServeConn(conn io.ReadWriteCloser) {
buf := bufio.NewWriter(conn)
srv := &gobServerCodec{
rwc: conn,
dec: gob.NewDecoder(conn),
enc: gob.NewEncoder(buf),
encBuf: buf,
}
server.ServeCodec(srv)
}
gobServerCodec与客户端的gobClientCodec结构相同,执行了相同的功能(编码+发送),但不同的是,客户端的编码译码器被放在了Client实例中将被多次使用,而在服务端建立的编码译码器直接被ServeCode()函数使用来处理监听到的调用请求。
func (server *Server) ServeCodec(codec ServerCodec) {
sending := new(sync.Mutex)
wg := new(sync.WaitGroup)
for {
service, mtype, req, argv, replyv, keepReading, err := server.readRequest(codec)
if err != nil {
if debugLog && err != io.EOF {
log.Println("rpc:", err)
}
if !keepReading {
break
}
// send a response if we actually managed to read a header.
if req != nil {
server.sendResponse(sending, req, invalidRequest, codec, err.Error())
server.freeRequest(req)
}
continue
}
wg.Add(1)
go service.call(server, sending, wg, mtype, req, argv, replyv, codec)
}
// We've seen that there are no more requests.
// Wait for responses to be sent before closing codec.
wg.Wait()
codec.Close()
}
可以看到,该函数首先开启一个互斥锁来保证一次只有一个发送线程操作conn,还有一个等待组量来限制在所有队列发送完毕后才关闭连接,随后便进入一个无线循环之中重复接收请求=>处理Error状况=>开启协程处理的循环中,具体为:
1)通过server.ReadRequest(codec)调用解析出请求内容
func (server *Server) readRequest(codec ServerCodec) (service *service, mtype *methodType, req *Request, argv, replyv reflect.Value, keepReading bool, err error) {
//1、解析出request类型的请求头,并找到对应的服务和方法
service, mtype, req, keepReading, err = server.readRequestHeader(codec)
......省略错误处理部分
//2、按类型情况为服务方法创建reflect.Value类型的参数用于调用
argIsValue := false // if true, need to indirect before calling.
if mtype.ArgType.Kind() == reflect.Ptr {
argv = reflect.New(mtype.ArgType.Elem())
} else {
argv = reflect.New(mtype.ArgType)
argIsValue = true
}
// argv guaranteed to be a pointer now.
4、读取request中的调用请求值,填充到argv中
if err = codec.ReadRequestBody(argv.Interface()); err != nil {
return
}
if argIsValue {
argv = argv.Elem()
}
//5、初始化调用返回参数,并对map和Slice的情况做额外处理
replyv = reflect.New(mtype.ReplyType.Elem())
switch mtype.ReplyType.Elem().Kind() {
case reflect.Map:
replyv.Elem().Set(reflect.MakeMap(mtype.ReplyType.Elem()))
case reflect.Slice:
replyv.Elem().Set(reflect.MakeSlice(mtype.ReplyType.Elem(), 0, 0))
}
return
}
其中,readRequestHeader函数利用codec和统一的request类型,可以很容易解析出具体的request内容,再前往Server中存储的Service字典找到具体的Service,在Service的Method字典中找到对应的Method:
func (server *Server) readRequestHeader(codec ServerCodec) (svc *service, mtype *methodType, req *Request, keepReading bool, err error) {
// 通过互斥锁从server的request链最末端拿到一个新的request量
req = server.getRequest()
//codec.Decode(req)即可
err = codec.ReadRequestHeader(req)
...省略错误处理
//成功读到了请求头,说明连接正常,即使后续操作失败,仍可以读取下一个请求
keepReading = true
//分离服务名和方法名
dot := strings.LastIndex(req.ServiceMethod, ".")
...省略错误处理
serviceName := req.ServiceMethod[:dot]
methodName := req.ServiceMethod[dot+1:]
// 查找到请求的服务和方法
svci, ok := server.serviceMap.Load(serviceName)
......省略错误处理
svc = svci.(*service)
mtype = svc.method[methodName]
......省略错误处理
return
}
这一过程完成后,程序得到了相应远程调用的所有准备:
- service:服务提供者实例
- mtype:被调用函数(methodType)
- req:本次请求头的值(request)
- argv、replyv:传入调用函数的参数和返回值(reflect.Value)
- keepReading, err:是否正常读取下一条请求标记和错误
2) 排除错误信息,开启协程执行调用
go service.call(server, sending, wg, mtype, req, argv, replyv, codec)
实际上,依靠上述解码出的调用信息和reflect包提供的反射技术,这个调用已经很容易完成:
func (s *service) call(server *Server, sending *sync.Mutex, wg *sync.WaitGroup, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {
//调用结束后释放一个等待量
if wg != nil {
defer wg.Done()
}
//方法被调用次数+1
mtype.Lock()
mtype.numCalls++
mtype.Unlock()
function := mtype.method.Func
// 调用服务提供的函数
returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})
// 处理调用结果为错误的情况
errInter := returnValues[0].Interface()
errmsg := ""
if errInter != nil {
errmsg = errInter.(error).Error()
}
//向客户端发送调用结果
server.sendResponse(sending, req, replyv.Interface(), codec, errmsg)
//释放request结构,调用结束!
server.freeRequest(req)
}
最后可以看到,调用结果将被通过server.sendResponse方法返回
func (server *Server) sendResponse(sending *sync.Mutex, req *Request, reply interface{}, codec ServerCodec, errmsg string) {
//1、从server链中获得一个response实例
resp := server.getResponse()
//2、准备response内容
resp.ServiceMethod = req.ServiceMethod
if errmsg != "" {
resp.Error = errmsg
reply = invalidRequest
}
//sql序列号是客户端辨认response对应call的关键
resp.Seq = req.Seq
sending.Lock()
//3、将response对象和reply对象发送到客户端
err := codec.WriteResponse(resp, reply)
if debugLog && err != nil {
log.Println("rpc: writing response:", err)
}
sending.Unlock()
server.freeResponse(resp)
}
以上,是服务器处理一次RPC请求的全部过程。
5、客户端接收调用结果
go client.input() //启动input()协程处理接收内容
当调用结果到达客户端后,就由在连接建立阶段启动的client.Input()协程负责处理。
func (client *Client) input() {
var err error
var response Response
// 循环读取TCP相应
for err == nil {
//建立空的Response对象用于接收
response = Response{}
//解码出request机构(c.dec.Decode(response))
err = client.codec.ReadResponseHeader(&response)
if err != nil {
break
}
//根据response体中包含的序号确定这次响应对应的正在等待的请求call
seq := response.Seq
client.mutex.Lock()
call := client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
switch {
case call == nil://没有对应的call,不予处理
err = client.codec.ReadResponseBody(nil)
if err != nil {
err = errors.New("reading error body: " + err.Error())
}
case response.Error != ""://有对应的call,但调用存在Error
//给call.Error赋予响应的值并结束这次call
call.Error = ServerError(response.Error)
err = client.codec.ReadResponseBody(nil)
if err != nil {
err = errors.New("reading error body: " + err.Error())
}
call.done()
default://默认情况,尝试用call中存储的reply结构读取出响应中的返回值
//c.dec.Decode(body)
//如果解码成功,call.Reply中就成功的保存了这次调用的结果
//因为reflect.Value类型的reply是以指针赋值,所以主线程中投入的参数
//指针已经指向了调用结果
err = client.codec.ReadResponseBody(call.Reply)
//如果读取失败,说明服务器端出现问题,报错并结束本次连接
if err != nil {
call.Error = errors.New("reading body " + err.Error())
}
call.done()
}
}
//循环体结束即连接中断,需要做一些善后工作
client.reqMutex.Lock()
client.mutex.Lock()
client.shutdown = true
closing := client.closing
if err == io.EOF {
if closing {
err = ErrShutdown
} else {
err = io.ErrUnexpectedEOF
}
}
for _, call := range client.pending {
call.Error = err
call.done()
}
client.mutex.Unlock()
client.reqMutex.Unlock()
if debugLog && err != io.EOF && !closing {
log.Println("rpc: client protocol error:", err)
}
}
当得到成功或失败的调用结果后,call.Reply和call.Error中已经保存了这次调用结果,即可通知主线程停止阻塞,得到最终结果,这项工作由call.done()完成:
func (call *Call) done() {
select {
case call.Done <- call:
// ok
......debug情况
}
只要将本次call通过call.Done(chan *call)管道传会主线程,就可以结束本次RPC过程。
综合上述三个阶段,可以得到一个大致的流程图:
最终整个RPC包逻辑流程图大致如下:
四、学习后记
全程学习一遍net/rpc包,发现它即是一个实现远程调用的Go语言优秀内置工具,也是一个很值得学习的Go语言编程范例。
Go团队利用了Go语言中的反射技术、协程、管道,gob编解码码,在HTTP协议的基础上,实现了一个轻量的rpc协议,提供了快速注册服务和多线程远程调用的功能,同时拥有完善的错误处理和内存管理机制。
作为一个GoLang菜鸟,主要学到的有:
1、Go虽然没有对象概念,但完全可以以结构和方法的组合实现面向对象编程,上述Clint、Server和Codec等结构都是采用面向对象的方法进行程序的组织。
2、reflect包给编程人员提供了深入了解、存储和使用结构和方法的能力,在复杂的服务调用场合非常实用。
3、无论是在本机多线程还是网络多连接的场合,都需要考虑使用sync包实现公共变量的保护,比如net/rpc包中体现出的:使用sync.Mutex实现互斥访问,保护共有变量、使用sync.WaitGroup实现对队列的保护。
4、对于多系统通信的场合,有一个统一的协议事半功倍,比如在net/rpc包的设计中,统一了输入服务函数的格式,约定了参数格式、结果返回方式,与此同时,在连接建立阶段也规定了特定的HTTP访问路径和访问方法。