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
的一些头部信息,然后再解析消息体中携带的参数,最后初始化响应的返回值类型。这部分代码太长了,直接贴源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | // 此函数是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
及其对应方法,这部分代码也太长了,直接贴源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | // 该函数是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
客户端,源代码如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具