【Golang】源码学习:net/rpc包——基于Http协议的 RPC 服务(二)

第一部分链接: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访问路径和访问方法。

发布了34 篇原创文章 · 获赞 1 · 访问量 1713

猜你喜欢

转载自blog.csdn.net/qq_38093301/article/details/104232361
RPC