Go语言高并发与微服务实战专题精讲——远程过程调用 RPC——服务端处理RPC请求的原理及源代码分析

远程过程调用 RPC——服务端处理RPC请求的原理及源代码分析

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

  在Go语言的RPC服务端,一个持续运行的循环不断地监听并接收来自客户端的RPC请求。每当一个请求到达时,服务端会将其转交给ReadRequestHandler进行处理。这个处理程序是RPC框架中的关键组件,它负责解析请求并准备相应的响应。

  在处理请求的过程中,ReadRequestHandler首先会从服务端之前通过Register方法保存的方法映射表中检索出客户端请求调用的具体方法。这个映射表实际上是一个保存了服务名、方法名与对应实现函数之间关系的字典(map)。

  一旦找到了对应的方法,处理程输中的二进制数据转换为Go语言中可以理解的数据结构。

  接下来,利用Go语言的反射机制,ReadRequestHandler能够动态地调用解码后的参数所对应的方法。这种动态调用的能力是RPC框架灵活性的关键,它允许服务端在运行时决定执行哪个具体的函数,而无需在编译时确定。

 方法执行完成后,处理程序会将返回的结果编码成RPC响应消息。这个过程与解码相反,它将Go语言中的数据结构转换回适合网络传输的格式。

  最后,编码后的响应消息会被发送回客户端,完成了整个RPC调用的处理流程。通过这种方式,Go语言的RPC服务端能够高效地处理来自不同客户端的各种请求,提供了强大的服务能力和灵活性。

一、接收请求

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

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

二、读取并解析请求数据

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

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

// 此函数是Server结构体的一个方法,用于从编解码器(codec)中读取RPC请求,  
// 并解析出服务、方法类型、请求对象、参数和回复的反射值等信息。  
func (server *Server) readRequest(codec ServerCodec) (service *service, mtype *methodType, req *Request, argv, replyv reflect.Value, keepReading bool, err error) {  
  
 // 首先调用readRequestHeader方法读取请求头,获取服务、方法类型、请求对象和是否继续读取的标志等信息。  
 service, mtype, req, keepReading, err = server.readRequestHeader(codec)  
  
 // 如果在读取请求头时发生错误,进入错误处理流程。  
 if err != nil {  
 // 如果keepReading为false,表示不需要继续读取后续数据,直接返回。  
 if !keepReading {  
 return  
 }  
  
 // 如果需要继续读取但发生了错误,则丢弃请求体,并返回。  
 // 这样做可能是为了保持与客户端的通信协议同步,即使发生错误也继续读取完整个请求。  
 codec.ReadRequestBody(nil)  
 return  
 }  
  
 // 解码参数值。根据方法类型的参数类型,创建一个新的反射值来存储参数。  
 argIsValue := false // 标记参数是否是一个值类型,如果是,则在调用前需要进行间接引用。  
  
 // 检查方法类型的参数是否是指针类型。  
 if mtype.ArgType.Kind() == reflect.Pointer {  
 // 如果是指针类型,则创建一个该指针类型指向的新实例。  
 argv = reflect.New(mtype.ArgType.Elem())  
 } else {  
 // 如果不是指针类型,则直接创建一个该类型的新实例,并标记参数是一个值类型。  
 argv = reflect.New(mtype.ArgType)  
 argIsValue = true  
 }  
  
 // 此时argv一定是一个指针类型的反射值。  
 // 调用编解码器的ReadRequestBody方法读取请求体,并将数据填充到之前创建的参数反射值中。  
 if err = codec.ReadRequestBody(argv.Interface()); err != nil {  
 // 如果在读取请求体时发生错误,直接返回。  
 return  
 }  
  
 // 如果参数是一个值类型,则通过Elem()方法获取其指向的实际值,以便后续使用。  
 if argIsValue {  
 argv = argv.Elem()  
 }  
  
 // 创建一个新的反射值来存储回复数据。回复数据的类型总是指针类型,指向方法类型的回复类型。  
 replyv = reflect.New(mtype.ReplyType.Elem())  
  
 // 根据回复类型的元素类型,进行初始化。如果是Map或Slice类型,则创建一个空的Map或Slice。  
 switch mtype.ReplyType.Elem().Kind() {  
 case reflect.Map:  
 // 如果是Map类型,则创建一个空的Map。  
 replyv.Elem().Set(reflect.MakeMap(mtype.ReplyType.Elem()))  
 case reflect.Slice:  
 // 如果是Slice类型,则创建一个空的Slice。  
 replyv.Elem().Set(reflect.MakeSlice(mtype.ReplyType.Elem(), 0, 0))  
 }  
  
 // 返回解析得到的服务、方法类型、请求对象、参数和回复的反射值、是否继续读取的标志以及可能的错误信息。  
 return  
}

readRequestHeader函数是解析RPC请求的关键函数,它会首先解析请求的头部信息,然后获取信息中包含RPC请求的struct名字和方法名字,然后从Servermap中获取到服务端注册的service及其对应方法,这部分代码也太长了,直接贴源码如下:

// 该函数是Server结构体的一个方法,用于从编解码器(codec)中读取RPC请求的头信息。 
func (server *Server) readRequestHeader(codec ServerCodec) (svc *service, mtype *methodType, req *Request, keepReading bool, err error) { 
   
 // 调用server的getRequest方法来获取一个新的请求对象。 
 // 这通常是为了复用请求对象,减少内存分配和垃圾回收的开销。 
 req = server.getRequest() 
   
 // 调用codec的ReadRequestHeader方法来从编解码器中读取请求头, 
 // 并将读取到的信息填充到之前获取的req对象中。 
 err = codec.ReadRequestHeader(req) 
   
 // 如果在读取请求头时发生错误,进入错误处理流程。 
 if err != nil { 
 // 防御性编程:如果读取失败,将请求对象设置为nil, 
 // 防止后续代码错误地使用可能包含不完整或错误数据的请求对象。 
 req = nil 
   
 // 检查错误是否为io.EOF或io.ErrUnexpectedEOF,这两种错误表示输入流结束, 
 // 可能是因为客户端关闭了连接或数据不完整。 
 if err == io.EOF || err == io.ErrUnexpectedEOF { 
 // 如果是这两种错误之一,则直接返回,不再处理。 
 return 
 } 
   
 // 如果不是上述两种特定错误,则创建一个新的错误消息, 
 // 该消息包含原始错误信息,但前缀有额外的描述信息, 
 // 以便于调用者更容易理解错误的来源和性质。 
 err = errors.New("rpc: server cannot decode request: " + err.Error()) 
 return 
 } 
   
 // 如果成功读取了请求头,则设置keepReading标志为true, 
 // 表示可以继续读取后续的请求数据(如果有的话)。 
 keepReading = true 
   
 // 使用strings.LastIndex查找服务名和方法名之间的分隔符(这里是点号'.'), 
 // 以便将服务名和方法名分割开。 
 dot := strings.LastIndex(req.ServiceMethod, ".") 
   
 // 如果找不到分隔符,说明服务名和方法名的格式不正确,设置错误信息并返回。 
 if dot < 0 { 
 err = errors.New("rpc: service/method request ill-formed: " + req.ServiceMethod) 
 return 
 } 
   
 // 分割出服务名和方法名。 
 serviceName := req.ServiceMethod[:dot] 
 methodName := req.ServiceMethod[dot+1:] 
   
 // 使用服务名在server的服务映射表(serviceMap)中查找对应的服务。 
 svci, ok := server.serviceMap.Load(serviceName) 
   
 // 如果在服务映射表中找不到对应的服务,设置错误信息并返回。 
 if !ok { 
 err = errors.New("rpc: can't find service " + req.ServiceMethod) 
 return 
 } 
   
 // 将接口类型的服务转换为具体的服务结构体指针。 
 svc = svci.(*service) 
   
 // 在找到的服务中查找指定的方法。 
 mtype = svc.method[methodName] 
   
 // 如果在服务中找不到指定的方法,设置错误信息并返回。 
 if mtype == nil { 
 err = errors.New("rpc: can't find method " + req.ServiceMethod) 
 } 
   
 // 返回解析得到的服务、方法类型、请求对象、是否继续读取的标志以及可能的错误信息。 
 return 
}

三、执行远程方法并返回响应

Servercall函数就是通过mtype.method.Func.Call()反射调用对应RPC过程的方法,其源代码如下:

它还会调用sendResponse将返回值发送给RPC客户端,源代码如下:

posted @ 2024-04-25 13:08  左扬  阅读(26)  评论(0编辑  收藏  举报
levels of contents