【golang 源码分析】第二章 RPC client源码分析

本文详细解析了Go语言中的RPC客户端实现原理,包括其核心结构体、关键函数及如何利用连接复用来支持并发请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >



第二章 RPC client源码分析

 

rpc客户端的逻辑很简单,将一个个的调用请求序列化后原子的发送给服务器,有一个专门的gorutine等待服务器应答,这goroutine会将收到的每个应答分发给对应的请求,完成了一次rpc调用。

client是基于单个socket连接来,channel来实现复用连接以及并发的。而临时的调用对象Call都是保存在Client的map中的,每个call怎么查找,根据seq序列号在请求时候发过去,之后response的时候,client根据返回的seq再反查pending的map结果的。

rpc客户端,最重要的事情就是连接复用,也就是说一个连接上需要并发的同时跑多个请求

1.1 结构体

type Call struct {
       ServiceMethod string      // The name of the service and method to call.
       Args          interface{} // The argument to the function (*struct).
       Reply         interface{} // The reply from the function (*struct).
       Error         error       // After completion, the error status.
       Done          chan *Call  // Strobes when call is complete.
}

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
}

 

type ClientCodec interface {
       // WriteRequest must be safe for concurrent use by multiple goroutines.
       WriteRequest(*Requestinterface{}) error
       ReadResponseHeader(*Response) error
       ReadResponseBody(interface{}) error

       Close() error
}

定义了编码解码器的接口,要自定义一个编码解码器,只要实现这个接口就OK了。WriteRequest就是将rpc请求的远程方法名、参数等信息进行序列化打包,写入socket可以按照自己的意愿去打包请求进行发送。 两个Read接口就是从socket读取响应数据包,然后采用相应的算法进行反序列化解包。

如不能自定义编码解码器,就只能用在客户端和服务器都是Go。这降低了其灵活性。自定义编码解码器后,就可以实现其他rpc的协议,比如:thrift。github的go-thrift项目

1.2 调用入口NewClient

 

func NewClient(conn io.ReadWriteCloser) *Client

func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error 

func NewClient(conn io.ReadWriteCloser) *Client {
       encBuf := bufio.NewWriter(conn)
       client := &gobClientCodec{conngob.NewDecoder(conn)gob.NewEncoder(encBuf)encBuf}
       return NewClientWithCodec(client)
}
func NewClientWithCodec(codec ClientCodec) *Client {
       client := &Client{
              codec:   codec,
              pending: make(map[uint64]*Call),
       }
       go client.input()
       return client
}

new一个客户端对象,通过这个对象的Call方法去执行远程服务端方法。同时建了一个input goroutine. input goroutine阻塞在连接上,wait for服务端的响应NewClient内部使用的默认的gob编码,gobClientCodes实现了Codec的接口。 

Call方法的主要是将一个rpc请求发送到服务端,同时放入一个等待队列,等待服务器的响应。

1.3 input函数

func (client *Clientinput() {
       var err error
       var response Response

for循环就是永久负责这个连接,只在连接上发生错误才退出。


       for err == nil {

读响应头, 响应头有一个重要的信息是正文数据长度,知道这个长度信息知道读多少正文才是一个完整应答


              response = Response{}
              err = client.codec.ReadResponseHeader(&response)
              if err != nil {
                     break
              
}

从响应头里取seq,这个seq是客户端生成的,send中发送给服务器,服务器应答的时候,将这个seq响应给客户端。只有依靠seq客户端才知道这个应答是对应pending队列中的个请求。


              seq := response.Seq
              client.mutex.Lock()
              call := client.pending[seq]
              delete(client.pendingseq)
              client.mutex.Unlock()

              switch {
              case call == nil:
                     取出body里的内容并丢弃
                     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.
                     call.Error = ServerError(response.Error)
                     err = client.codec.ReadResponseBody(nil)
                     if err != nil {
                            err = errors.New("reading error body: " + err.Error())
                     }
                     call.done()
              default:
                     err = client.codec.ReadResponseBody(call.Reply)
                     if err != nil {
                            call.Error = errors.New("reading body " + err.Error())
                     }
                     call.done()
              }
       }

下面代码在处理连接上出错,以及服务端关闭连接等情况的清理工作. 如果不清可能导致一些调用rpc的goroutine永久阻塞等待。


       // Terminate pending calls.
       client.reqMutex.Lock()
       client.mutex.Lock()
       client.shutdown = true
       closing := client.closing
       if err == io.EOF {
              if closing {
                     err = ErrShutdown
              } else {
                     err = io.ErrUnexpectedEOF
              }
       }
       for _call := range client.pending {
              call.Error = err
              call.done()
       }
       client.mutex.Unlock()
       client.reqMutex.Unlock()
       if debugLog && err != io.EOF && !closing {
              log.Println("rpc: client protocol error:"err)
       }
}

等待一个读取应答的goroutine只有一个,这个goroutine负责读取有响应,将响应分发给对应的请求,就完成了一次rpc请求。

1.4 send函数

 

func (client *Clientsend(call *Call) {
       client.reqMutex.Lock()
       defer client.reqMutex.Unlock()

       // Register this call.
       client.mutex.Lock()
       if client.shutdown || client.closing {
              call.Error = ErrShutdown
              client.mutex.Unlock()
              call.done()
              return
       
}

pending使用map实现的,rpc请求都会生存一个唯一递增的seq, seq就是用来标记请求的,这个很像tcp包的seq


       seq := client.seq
       client.seq++
       client.pending[seq] = call
       client.mutex.Unlock()

       // Encode and send the request.
       client.request.Seq = seq
       client.request.ServiceMethod = call.ServiceMethod
       err := client.codec.WriteRequest(&client.requestcall.Args)
       if err != nil {
              client.mutex.Lock()
              call = client.pending[seq]
              delete(client.pendingseq)
              client.mutex.Unlock()
              if call != nil {
                     call.Error = err
                     call.done()
              }
       }
}

一个rpc请求发出去主要过程,第一是将请求放入等待队列,第二是序列化,最后是写入socket

1.5 接口函数Call

同步死等:发送请求的时候,调用就是Call,传入方法名,参数,获取返回等

func (client *ClientCall(serviceMethod stringargs interface{}reply interface{}) error {
       call := <-client.Go(serviceMethodargsreplymake(chan *Call1)).Done
       return call.Error
}

 

1.6 接口函数Go

异步

 

func (client *ClientGo(serviceMethod stringargs interface{}reply interface{}done chan *Call) *Call {
       call := new(Call)
       call.ServiceMethod = serviceMethod
       call.Args = args
       call.Reply = reply
       if done == nil {
              done = make(chan *Call10// buffered.
       else {
              // If caller passes done != nil, it must arrange that
              // done has enough buffer for the number of simultaneous
              // RPCs that will be using that channel. If the channel
              // is totally unbuffered, it's best not to run at all.
              if cap(done) == {
                     log.Panic("rpc: done channel is unbuffered")
              }
       }
       call.Done = done
       client.send(call)
       return call
}

 

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值