第一部分链接:https://blog.youkuaiyun.com/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()函数向服务器发起调用。