Go语言高并发与微服务实战专题精讲——远程过程调用 RPC——客户端处理RPC请求的原理及源代码分析
远程过程调用 RPC——客户端处理RPC请求的原理及源代码分析
客户端无论是同步调用还是异步调用, 每次RPC
请求都会生成一个Call
对象, 并使用seq
作为key
保存在map
中, 服务端返回响应值时再根据响应值中的seq
从map
中取出Call
, 进行相应处理。
客户端发起RPC
调用的过程大致如下所示, 我们依次讲解同步调用和异步调用, 请求参数编码和接收服务器响应三个部分的具体实现:
一、同步调用和异步调用实现
首先我们看看src/net/rpc/client.go
的源代码位置:
我们来分析下, src/net/rpc/client.go
, 首先调用Call
方法, 然后Call
方法调用了Go
方法, 代码如下:
// Call invokes the named function, waits for it to complete, and returns its error status. /* 函数签名定义了一个名为 Call 的方法, 该方法属于 Client 类型。 它接受三个参数:一个字符串 serviceMethod(表示要调用的远程方法名), 和两个空接口类型的参数 args(表示调用方法的参数)和 reply(用于接收方法的返回值)。 该方法返回一个 error 类型, 表示远程调用的错误状态。 */ func (client *Client) Call(serviceMethod string, args any, reply any) error { /* 异步调用: 在函数体内, 首先通过 client.Go(...) 方法启动了一个异步的远程过程调用。 Go 方法会立即返回一个 Call 结构体, 其中包含了一个 Done 通道。 当远程调用完成或出错时, 该通道会接收到一个 Call 结构体。 */ /* 同步调用: <-client.Go(...).Done 这一行代码会阻塞, 直到从 Done 通道中接收到 Call 结构体。 这实际上是将异步调用转换为同步调用的过程, 即 Call 方法会等待远程调用完成或出错后才继续执行。 */ /*代码详细解释: 方法调用: client.Go(serviceMethod, args, reply, make(chan *Call, 1)) 1、client 是一个指向 Client 类型的指针。 2、Go 是 Client 类型的一个方法, 这个方法是异步启动一个远程过程调用(RPC)。 3、serviceMethod 是一个字符串, 指定了要调用的远程服务的方法名。 4、args 是传递给远程方法的参数。 5、reply 是一个用于存储远程方法返回结果的变量。 6、make(chan *Call, 1) 创建一个缓冲区大小为 1 的通道, 用于接收 *Call 类型的值。这个通道是用于接收异步调用的状态或结果。 通道创建: make(chan *Call, 1) 1、使用 make 函数创建了一个通道, 这个通道可以存储一个 *Call 类型的值(即指向 Call 结构体的指针)。 2、通道的缓冲区大小为 1, 意味着这个通道可以存储一个元素, 在元素被取出之前, 如果有新的元素发送进来, 发送操作将会被阻塞, 直到通道中的元素被取出。 接收通道中的数据:<-client.Go(...).Done 1、client.Go(...) 的返回值是一个包含 Done 通道的结构体或对象。 2、Done 是一个通道, 当 Go 方法中的异步操作完成时, 会将结果(一个 *Call 类型的值)发送到这个通道。 3、
而Go
方法则是先创建并初始化了Call
对象, 记录下此次调用的方法、参数和返回值, 并生成DoneChannel
。首先, 我们有两个重要的结构体定义, 源代码如下:
// Call represents an active RPC. // Call结构体代表了一个活跃的RPC调用。 type Call struct { ServiceMethod string // 调用的服务和方法的名称。 Args any // 函数的参数(通常是一个结构体指针)。 Reply any // 函数的返回值(通常是一个结构体指针)。 Error error // 完成后, 记录错误状态。 Done chan *Call // 当Go调用完成时, 会向此通道发送*Call。 } // Client represents an RPC Client. // Client结构体代表了一个RPC客户端。 type Client struct { codec ClientCodec reqMutex sync.Mutex // protects following request Request mutex sync.Mutex // protects following seq uint64 pending map[uint64]*Call closing bool // user has called Close shutdown bool // server has told us to stop }
接下来是Client结构体的Go方法, 源代码如下:
/* 这个方法的主要目的是允许用户异步地启动一个RPC调用, 而不必等待它完成。 用户可以通过检查Done通道来知道何时调用完成, 并通过Call结构体的其他字段获取调用的结果或错误信息。 这种方法对于需要并发执行多个RPC调用, 而不希望阻塞主执行线程的场景非常有用。 */ // Go方法异步地调用一个远程过程。 // 它返回一个代表这次调用的Call结构体。 // done通道会在调用完成时, 通过返回相同的Call对象来发出信号。 // 如果done为nil, 该方法会分配一个新的通道。 // 如果done非nil, 它必须是有缓冲的, 否则程序会故意崩溃以避免潜在的死锁。 func (client *Client) Go(serviceMethod string, args any, reply any, done chan *Call) *Call { // 创建一个新的Call实例。 call := new(Call) // 设置Call的各个字段。 call.ServiceMethod = serviceMethod // 调用的服务和方法名。 call.Args = args // 调用参数。 call.Reply = reply // 用于接收返回值的变量。 // 检查done通道是否为nil, 或者是否有足够的缓冲区。 if done == nil { // 如果done为nil, 创建一个新的有缓冲通道, 缓冲区大小为10。 done = make(chan *Call, 10) // buffered channel } else if cap(done) == 0 { // 如果done通道没有缓冲区, 记录一个恐慌日志并停止程序执行, // 因为无缓冲通道可能导致死锁。 log.Panic("rpc: done channel is unbuffered") } // 设置Call的Done字段为处理过的done通道。 call.Done = done // 调用client的send方法发送RPC请求。 // 这个方法通常会将调用请求发送到服务端。 // (注意:这里的send方法没有在代码段中给出, 但它负责将RPC请求发送到服务器) client.send(call) // 返回处理过的Call实例, 以便调用者可以后续处理。 return call }
二、请求参数编码
当RPC客户端想要发送一个请求时,它会创建一个Call
对象,并填充该对象的各种字段,如服务方法名、参数、回复对象等。然后,它会调用send
方法来发送这个请求。
在send
方法中,客户端首先锁定请求互斥锁以确保线程安全。然后,它检查客户端的状态,如果客户端已关闭或正在关闭,则立即返回错误。否则,它会增加序列号并将此调用添加到挂起的调用映射中。
接下来,客户端设置请求的序列号和服务方法名,并通过调用编解码器的WriteRequest
方法来发送请求。在这个方法中,编解码器首先使用其内部的编码器来编码请求对象(包含序列号和服务方法名),然后编码请求的参数(即RPC调用的参数)。最后,编解码器刷新其内部缓冲区的编码器以确保所有数据都已发送到网络。这就是RPC客户端请求参数的编码和发送过程,具体源码实现,位置如下:
我们学习下这段源码片段:
// Call represents an active RPC. type Call struct { ServiceMethod string // The name of the service and method to call. Args any // The argument to the function (*struct). Reply any // The reply from the function (*struct). Error error // After completion, the error status. Done chan *Call // Receives *Call when Go is complete. } // Client represents an RPC Client. // There may be multiple outstanding Calls associated // with a single Client, and a Client may be used by // multiple goroutines simultaneously. type Client struct { codec ClientCodec reqMutex sync.Mutex // protects following request Request mutex sync.Mutex // protects following seq uint64 pending map[uint64]*Call closing bool // user has called Close shutdown bool // server has told us to stop } // A ClientCodec implements writing of RPC requests and // reading of RPC responses for the client side of an RPC session. // The client calls WriteRequest to write a request to the connection // and calls ReadResponseHeader and ReadResponseBody in pairs // to read responses. The client calls Close when finished with the // connection. ReadResponseBody may be called with a nil // argument to force the body of the response to be read and then // discarded. // See NewClient's comment for information about concurrent access. type ClientCodec interface { WriteRequest(*Request, any) error ReadResponseHeader(*Response) error ReadResponseBody(any) error Close() error } // 定义了一个名为send的方法,该方法属于Client结构体,并接收一个指向Call结构体的指针作为参数。Call结构体通常包含RPC调用的信息,如服务方法名、参数、回复和错误等。 func (client *Client) send(call *Call) { // 锁定reqMutex互斥锁。这是为了确保在发送RPC请求时,对Client结构体的相关字段(如request、seq等)的访问是线程安全的。这防止了并发发送请求时可能出现的数据竞争。 client.reqMutex.Lock() // 使用defer关键字来确保在send函数返回之前解锁reqMutex。这是一种常见的Go语言模式,用于确保互斥锁总会被正确解锁,即使在函数发生错误或异常时也是如此。 defer client.reqMutex.Unlock() // Register this call. 注册调用主要是将调用信息添加到客户端的挂起调用映射中,以便稍后能够匹配响应。 // 锁定另一个互斥锁mutex。这个锁用于保护对pending映射(存储挂起的RPC调用)的并发访问。 client.mutex.Lock() // 检查客户端是否处于关闭或正在关闭的状态。如果是,那么不应该继续发送RPC请求。 if client.shutdown || client.closing { // 如果客户端正在关闭或已关闭,则立即解锁mutex并退出后续操作。 client.mutex.Unlock() // 设置调用的错误为ErrShutdown call.Error = ErrShutdown // 通知调用已完成(通过调用done方法) call.done() // 立即返回,不再继续执行后续代码 return } // 生成一个新的序列号(seq)用于标识这个RPC调用。 seq := client.seq // 每次发送请求时,序列号都会递增,以确保每个请求都有一个唯一的标识符。 client.seq++ // 在pending映射中,以新生成的序列号为键,将当前的RPC调用存储为值。这样,当响应返回时,客户端可以根据序列号找到对应的调用,并处理响应。 client.pending[seq] = call // 解锁mutex,因为对pending映射的修改已经完成。 client.mutex.Unlock() // Encode and send the request. 编码并发送RPC请求 // 设置请求对象的序列号和服务方法字段。这些字段将被发送到服务器,以便服务器知道如何处理请求。 client.request.Seq = seq client.request.ServiceMethod = call.ServiceMethod // 调用编解码器的WriteRequest方法来编码并发送RPC请求。这通常涉及将请求对象(包含序列号和服务方法)和调用参数序列化为字节流,并通过网络连接发送到服务器。如果编码或发送过程中发生错误,err变量将被设置为非空值。 err := client.codec.WriteRequest(&client.request, call.Args) // 如果编码或发送过程中发生错误,则从pending中删除该调用,并返回错误。 if err != nil { client.mutex.Lock() call = client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() if call != nil { call.Error = err call.done() } } }
三、接受服务器响应
接下来我们来看一下客户端是如何接受并处理服务端返回值的。客户端的input
函数接收服务端返回的响应值,它进行无限for
循环, 不断调用codec
, 也就是gobClientCodecd
的ReadResponseHeader
函数, 然后根据其返回数据中的seq
来判断是否是本客户端发出请求的响应值。如果是则获取对应的Call
对象, 并将其从pending
哈希表中删除, 继续调用codec
的ReadResponseBody
方法获取返回值Reply
对象, 并调用Call
对象的done
方法,代码如下:
// Client represents an RPC Client. // There may be multiple outstanding Calls associated // with a single Client, and a Client may be used by // multiple goroutines simultaneously. type Client struct { codec ClientCodec reqMutex sync.Mutex // protects following request Request mutex sync.Mutex // protects following seq uint64 pending map[uint64]*Call closing bool // user has called Close shutdown bool // server has told us to stop } // 定义一个名为input的方法,它属于Client结构体。 // input函数负责从RPC服务器接收响应,并根据响应的类型(错误响应、正常响应或空响应)执行相应的操作。它还负责处理客户端关闭和错误情况,并确保所有挂起的调用都被正确地通知和终止。 func (client *Client) input() { var err error var response Response for err == nil { // 每次循环时,都会重置response变量,并尝试从编解码器读取响应头。如果读取失败,err将被设置为非nil,并跳出循环。 response = Response{} err = client.codec.ReadResponseHeader(&response) // 如果读取响应头时发生错误,跳出循环。 if err != nil { break } // 从响应头中获取序列号seq,然后锁定mutex互斥锁,从pending映射中获取与序列号对应的调用call,并从映射中删除它。然后解锁mutex。 seq := response.Seq client.mutex.Lock() call := client.pending[seq] delete(client.pending, seq) client.mutex.Unlock() switch { // 如果call为nil,说明没有对应的挂起调用,可能是因为发送请求时发生了错误,并且调用已经被删除。在这种情况下,我们需要读取错误响应体并忽略它。 case call == nil: // We've got no pending call. That usually means that // WriteRequest partially failed, and call was already // removed; response is a server telling us about an // error reading request body. We should still attempt // to read error body, but there's no one to give it to. // 尝试读取错误响应体,并将其丢弃(通过传递nil作为参数)。如果读取失败,将err设置为一个新的错误消息。 err = client.codec.ReadResponseBody(nil) if err != nil { err = errors.New("reading error body: " + err.Error()) } // 如果响应包含错误消息,说明服务器返回了一个错误响应。 case response.Error != "": // We've got an error response. Give this to the request; // any subsequent requests will get the ReadResponseBody // error if there is one. // 将错误消息设置为调用的错误字段,并尝试读取错误响应体并丢弃它。然后调用done方法通知调用已完成,并传递错误。 call.Error = ServerError(response.Error) err = client.codec.ReadResponseBody(nil) if err != nil { err = errors.New("reading error body: " + err.Error()) } call.done() //如果响应既没有错误消息也不是错误响应,那么它是一个正常的响应。 default: // 尝试读取响应体并将其解码到调用的回复字段中。如果读取失败,将错误设置为一个新的错误消息。然后调用done方法通知调用已完成。 err = client.codec.ReadResponseBody(call.Reply) if err != nil { call.Error = errors.New("reading body " + err.Error()) } call.done() } } // Terminate pending calls. // 锁定reqMutex和mutex互斥锁,将shutdown设置为true,并检查是否因为正常关闭或异常关闭导致err为io.EOF。 client.reqMutex.Lock() client.mutex.Lock() client.shutdown = true closing := client.closing if err == io.EOF { if closing { err = ErrShutdown } else { err = io.ErrUnexpectedEOF } } // 遍历所有挂起的调用,并将它们的错误字段设置为err,然后调用done方法通知它们已完成。 for _, call := range client.pending { call.Error = err call.done() } client.mutex.Unlock() client.reqMutex.Unlock() // 如果启用了调试日志记录,并且err不是io.EOF且客户端没有关闭,则记录一个错误消息。 if debugLog && err != io.EOF && !closing { log.Println("rpc: client protocol error:", err) } }