Go语言高并发与微服务实战专题精讲——远程过程调用 RPC——服务端处理RPC请求的原理及源代码分析
远程过程调用 RPC——服务端处理RPC请求的原理及源代码分析
RPC
网络调用会使用到Request
和Response
两个结构体,分别是请求参数和返回参数,通过编解码器(gob/json
)实现二进制和结构体的互相转换,它们的定义如下所示:
在Go
语言的RPC
服务端,一个持续运行的循环不断地监听并接收来自客户端的RPC
请求。每当一个请求到达时,服务端会将其转交给ReadRequestHandler
进行处理。这个处理程序是RPC
框架中的关键组件,它负责解析请求并准备相应的响应。
在处理请求的过程中,ReadRequestHandler
首先会从服务端之前通过Register
方法保存的方法映射表中检索出客户端请求调用的具体方法。这个映射表实际上是一个保存了服务名、方法名与对应实现函数之间关系的字典(map
)。
一旦找到了对应的方法,处理程输中的二进制数据转换为Go语言中可以理解的数据结构。
接下来,利用Go
语言的反射机制,ReadRequestHandler
能够动态地调用解码后的参数所对应的方法。这种动态调用的能力是RPC
框架灵活性的关键,它允许服务端在运行时决定执行哪个具体的函数,而无需在编译时确定。
方法执行完成后,处理程序会将返回的结果编码成RPC
响应消息。这个过程与解码相反,它将Go
语言中的数据结构转换回适合网络传输的格式。
最后,编码后的响应消息会被发送回客户端,完成了整个RPC
调用的处理流程。通过这种方式,Go
语言的RPC
服务端能够高效地处理来自不同客户端的各种请求,提供了强大的服务能力和灵活性。
一、接收请求
下面,我们来看一下具体的代码实现。首先是Accept
函数,它会无限循环的调用net.Listener
的Accept
函数来获取客户端建立连接的请求,获取到连接请求后,会使用协程来处理请求,代码如下:
ServeConn
函数会从建立的连接中读取数据,然后创建一个
gobServerCodec
,并将其交由Server
的ServeCode
函数处理,如下所示:
二、读取并解析请求数据
ServeCodec
函数会循环地调用readRequest
函数读取网络连接上的字节流,解析出请求,然后开启协程执行Server
的call
函数,处理对应的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
名字和方法名字,然后从Server
的map
中获取到服务端注册的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
}
三、执行远程方法并返回响应
Server
的call
函数就是通过mtype.method.Func.Call()
反射调用对应RPC
过程的方法,其源代码如下:
它还会调用sendResponse
将返回值发送给RPC
客户端,源代码如下: