GO微服务实战第十六节 Go RPC 如何实现服务间通信?

144 篇文章 29 订阅
84 篇文章 38 订阅

在上一课时,我们讲解了 RPC 的相关概念和常见的 RPC 框架。其中, Go RPC 是指 Go 语言原生支持的 RPC 框架,它虽然简单但却十分经典,非常适合作为你后续深入了解 RPC 框架时的研究对象。

在本课时,我们将先通过一个字符串服务为案例简单讲解 Go RPC 是如何进行通信的,然后再具体剖析 Go RPC 的底层原理和实现,对以服务端注册服务、接收并处理客户端请求和客户端发起请求等步骤分别进行详细介绍,相信你学习后,一定会对 Go RPC 有更加全面的了解和认识。

Go 语言 RPC 过程调用实践

Go 语言原生的 RPC 过程调用实现起来非常简单。服务端只需实现对外提供的远程过程方法和结构体,然后将其注册到 RPC 服务中,客户端就可以通过其服务名称和方法名称进行 RPC 方法调用。

本课时我们就使用字符串操作的服务来展示如何使用 Go 语言原生的 RPC 来进行过程调用。

第一步,定义远程过程调用相关接口传入参数和返回参数的数据结构。如下代码所示,调用字符串操作的请求包括两个参数:字符串 A 和字符串 B。

type StringRequest struct {
  A string
  B string
}

第二步,定义一个服务对象。这个服务对象可以很简单,比如类型是 int 或者是 interface{},重要的是它输出的方法。这里我们定义一个字符串服务类型的 interface,其名称为 Service,它有一个字符串拼接函数 Concat;然后定义一个名为 StringService 的结构体,实现 Service 接口,并给出 Concat 具体实现。代码如下:

type Service interface {
 // Concat a and b
 Concat(req StringRequest, ret *string) error
}
type StringService struct {
}
func (s StringService) Concat(req StringRequest, ret *string) error {
 // test for length overflow
 if len(req.A)+len(req.B) > StrMaxSize {
   *ret = ""
   return ErrMaxSize
 }
 *ret = req.A + req.B
 return nil
}

第三步,实现 RPC 服务器。这里我们生成了一个 StringSevice 结构体,并使用 rpc.Register 注册这个服务,然后通过 net.Listen 监听对应 socket 并对外提供服务。客户端可以访问服务 StringService 以及它的方法 Concat,代码如下:

func main() {
 stringService := new(service.StringService)
 rpc.Register(stringService)
 rpc.HandleHTTP()
 l, e := net.Listen("tcp", "127.0.0.1:1234")
 if e != nil {
   log.Fatal("listen error:", e)
 }
 http.Serve(l, nil)
}

第四步,建立 HTTP 客户端,然后通过 Call 方法调用远程 StringService 的对应方法,比如使用同步的方式,代码如下所示。这时客户端就可以进行远程调用了。

func main() {
 client, err := rpc.DialHTTP("tcp", "127.0.0.1:1234")
 if err != nil {
   log.Fatal("dialing:", err)
 }
 stringReq := &service.StringRequest{"A", "B"}
 var reply string
 err = client.Call("StringService.Concat", stringReq, &reply)
 if err != nil {
   log.Fatal("Concat error:", err)
 }
 // 异步的调用方式 
 call := client.Call("StringService.Concat", stringReq, &reply)
 _ := <-call.Done
}

通过上述代码的编写就可以实现两个 Go 服务之间的 RPC 调用了。那 Go RPC 又是如何实现的呢?

Go RPC 原理解析

接下来我们将对 Go 语言的 RPC 原生实现进行源码分析,细致讲解其具体实现和原理。首先我们会对 RPC 的服务(Server)端代码进行分析,包括注册服务、反射处理和存根保存,然后讲解服务端处理 RPC 请求的流程,最后讲解客户(Client)端的 RPC 请求处理。

1. Go RPC 服务端原理

服务端的 RPC 代码主要分为两个部分:①服务方法注册,包括调用注册接口,通过反射处理将方法取出,并存到 map 中;②处理网络调用,主要是监听端口、读取数据包、解码请求和调用反射处理后的方法,将返回值编码,返回给客户端。

在上面的示例代码中,我们使用 rpc.Register 对 StringService 进行了注册,Register 是进行 RPC 服务注册的入口方法,其参数 interface{} 类型的 rcvr 就是要注册的 RPC 服务类型,该注册过程的流程如下图所示。

Drawing 0.png

服务端注册 RPC 服务示意图

Register 方法中通过反射获取接口类型和值,并通过 suitableMethods 函数判断注册的 RPC 是否符合规范,最后调用 serviceMap 的 LoadOrStore(sname, s) 方法将对应 RPC 存根存放于 map 中,供之后查找。

func (server *Server) register(rcvr interface{}, name string, useName bool) error {
  // 如果服务为空,默认注册一个 
  if server.serviceMap == nil { 
    server.serviceMap = make(map[string]*service) 
  }
  // 获取注册服务的反射信息 
  s := new(service) 
  s.typ = reflect.TypeOf(rcvr) 
  s.rcvr = reflect.ValueOf(rcvr) 
  // 可以使用自定义名称 
  sname := reflect.Indirect(s.rcvr).Type().Name() 
  if useName { 
    sname = name 
  } 
  // 方法必须是暴露的,既服务名首字符大写;不允许重复注册。代码有省略
  if !isExported(sname) && !useName { 
  }   
  if _, present := server.serviceMap[sname]; present { 
  } 

s.name = sname
// 开始注册 rpc struct 内部的方法存根
s.method = suitableMethods(s.typ, true)
if len(s.method) == 0 {
// 如果struct内部一个方法也没,那么直接报错,打印详细的错误信息
}
// 保存在server的serviceMap中
server.serviceMap[s.name] = s
return nil
}

接下来,我们来看一下服务端处理 RPC 请求的实现。如下图就展示了服务端 RPC 程序处理请求的过程,它会一直循环处理接收到的客户端 RPC 请求,将其交由 ReadRequestHandler 处理,然后从之前 Register 方法保存的 map 中获取到要调用的对应方法;接着从请求中解码出对应的参数,使用反射调用其方法,获取到结果后将结果编码成响应消息返回给客户端。

Drawing 1.png

RPC 服务端处理流程示意图

下面,我们来看一下服务端接收并处理 RPC 请求的具体代码实现。

(1)接收请求

Server 的 Accept 函数会无限循环地调用 net.Listener 的 Accept 函数来获取客户端建立连接的请求,获取到连接请求后,再使用协程来处理请求。代码如下:

func (server *Server) Accept(lis net.Listener) { 
  for { 
    conn, err := lis.Accept() 
    if err != nil { 
      log.Fatal("rpc.Serve: accept:", err.Error())
    }
    // accept连接以后,打开一个goroutine处理请求 
    go server.ServeConn(conn) 
  } 
} 

(2)读取并解析请求

ServeConn 函数会从建立的连接中读取数据,然后创建一个 gobServerCodec,并将其交由 Server 的 ServeCodec 函数处理,如下所示:

func (server *Server) ServeConn(conn io.ReadWriteCloser) { 
  buf := bufio.NewWriter(conn) 
  srv := &gobServerCodec{ 
    rwc:  conn, 
    dec:  gob.NewDecoder(conn), 
    enc:  gob.NewEncoder(buf), 
    encBuf: buf, 
  } 
  // 根据指定的codec进行协议解析 
  server.ServeCodec(srv) 
} 

ServeCodec 函数会循环地调用 readRequest 函数,读取网络连接上的字节流,解析出请求,然后开启协程执行 Server 的 call 函数,处理对应的 RPC 调用。

func (server *Server) ServeCodec(codec ServerCodec) { 
  sending := new(sync.Mutex) 
  for { 
    // 解析请求 
    service, mtype, req, argv, replyv, keepReading, err := server.readRequest (codec)
    if err != nil { 
      if debugLog && err != io.EOF { 
        log.Println("rpc:", err) 
      } 
      if !keepReading { 
        break 
      } 
      // send a response if we actually managed to read a header. 
      // 如果当前请求错误了,我们应该返回信息,然后继续处理 
      if req != nil { 
        server.sendResponse(sending, req, invalidRequest, codec, err.Error())
        server.freeRequest(req) 
      } 
      continue 
    } 
    // 因为需要继续处理后续请求,所以开一个gorutine处理rpc方法 
    go service.call(server, sending, mtype, req, argv, replyv, codec) 
  }
  // 如果连接关闭了需要释放资源 
  codec.Close() 
} 

(3)执行远程方法并返回响应

Server 的 call 函数就是通过 Func.Call 反射调用对应 RPC 过程的方法,它还会调用 send Response 将返回值发送给 RPC 客户端,代码如下:

func (s *service) call(server *Server, sending *sync.Mutex, mtype *methodType, req *Request, argv, replyv reflect.Value, codec ServerCodec) {
  function := mtype.method.Func 
  // 这里是真正调用rpc方法的地方 
  returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv}) 
  errInter := returnValues[0].Interface() 
  errmsg := "" 
  // 处理返回请求了 
  server.sendResponse(sending, req, replyv.Interface(), codec, errmsg) 
  server.freeRequest(req) 
} 
2. 客户端发送 RPC 请求原理

无论是同步调用还是异步调用,每次 RPC 请求都会生成一个 Call 对象,并使用 seq 作为 key 保存在 map 中,服务端返回响应值时再根据响应值中的 seq 从 map 中取出 Call,进行相应处理。客户端发起 RPC 调用的过程大致如下图所示。

Drawing 2.png

客户端发送和接收请求流程示意图

下面我们将依次讲解同步调用和异步调用、请求参数编码和接收服务器响应三个部分的具体实现。

(1)同步调用和异步调用

本课时的案例展示了 Go 原生 RPC 的客户端支持同步和异步两种调用,下面我们来介绍一下这两种调用的函数以及调用的数据结构。

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
}

Call 方法直接调用了 Go 方法,而 Go 方法则是先创建并初始化了 Call 对象,记录下此次调用的方法、参数和返回值,并生成 DoneChannel;然后调用 Client 的 send 方法进行真正的请求发送处理,代码如下:

// 异步调用实现
func (client *Client) Go(serviceMethod string, args interface{}, reply interface{}, done chan *Call) *Call {
 // 初始化 Call 
 call := new(Call)
 call.ServiceMethod = serviceMethod
 call.Args = args
 call.Reply = reply
 if done == nil {
   done = make(chan *Call, 10) // buffered.
 } else {
   if cap(done) == 0 {
     log.Panic("rpc: done channel is unbuffered")
   }
 }
 call.Done = done
  // 调用 Client 的 send 方法
 client.send(call)
 return call
}
type Call struct {
  ServiceMethod string   // 服务名及方法名 格式:服务.方法
  Args     interface{} // 函数的请求参数 (*struct).
  Reply     interface{} // 函数的响应参数 (*struct).
  Error     error    // 方法完成后 error的状态.
  Done     chan *Call // 
}

(2)请求参数编码

Client 的 send 函数首先会判断客户端实例的状态,如果处于关闭状态,则直接返回结果;否则会生成唯一的 seq 值,将 Call 保存到客户端的哈希表 pending 中,然后调用客户端编码器的WriteRequest 来编码请求并发送,代码如下:

func (client *Client) send(call *Call) {
  // ....
  //生成seq,每次调用均生成唯一的seq,在服务端返回结果后会通过该值进行匹配
  seq := client.seq
  client.seq++
  client.pending[seq] = call
  client.mutex.Unlock()

// 请求并发送请求
client.request.Seq = seq
client.request.ServiceMethod = call.ServiceMethod
err := client.codec.WriteRequest(&client.request, call.Args)
if err != nil {
//发送请求错误时,将map中call对象删除.
client.mutex.Lock()
call = client.pending[seq]
delete(client.pending, seq)
client.mutex.Unlock()
}
}

(3)接收返回值

接下来我们来看一下客户端是如何接收并处理服务端返回值的。客户端的 input 函数接收服务端返回的响应值,它进行无限 for 循环,不断调用 codec 也就是 gobClientCodecd 的 ReadResponseHeader 函数,然后根据其返回数据中的 seq 来判断是否是本客户端发出请求的响应值。如果是,则获取对应的 Call 对象,并将其从 pending 哈希表中删除,继续调用 codec 的 ReadReponseBody 方法获取返回值 Reply 对象,并调用 Call 对象的 done 方法,代码如下:

func (client *Client) input() {
  var err error
  var response Response
  for err == nil {
    response = Response{}
    //通过response中的 Seq获取call对象
    seq := response.Seq
    client.mutex.Lock()
    call := client.pending[seq]
    delete(client.pending, seq)
    client.mutex.Unlock()
<span class="hljs-keyword">switch</span> {
<span class="hljs-keyword">case</span> call == <span class="hljs-literal">nil</span>:
<span class="hljs-keyword">case</span> response.Error != <span class="hljs-string">""</span>:
  <span class="hljs-comment">//上述两个case,一个处理call为nil,另外处理服务端返回的错误,直接将错误返回</span>
<span class="hljs-keyword">default</span>:
  <span class="hljs-comment">//通过编码器,将Resonse的body部分解码成reply对象.</span>
  err = client.codec.ReadResponseBody(call.Reply)
  <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
    call.Error = errors.New(<span class="hljs-string">"reading body "</span> + err.Error())
  }
  call.done()
}

}
}

上述代码中,gobClientCodecd 的 ReadResponseHeader、ReadReponseBody 方法和上文中的 WriteRequest 类似,这里不做赘述。Call 对象的 done 方法则通过 Call 的 DoneChannel,将获得返回值的结果通知到调用层,代码如下:

func (call *Call) done() {
 select {
 case call.Done <- call:
   // ok
 default:
   if debugLog {
     log.Println("rpc: discarding Call reply due to insufficient Done chan capacity")
   }
 }
}

客户端接收到 RPC 请求的响应后会进行其他业务逻辑操作,RPC 框架则会对进行 RPC 请求所需要的资源进行回收,下次进行 RPC 请求时则需要再次建立相应的结构体并获取对应的资源。

小结

Go 语言原生 RPC 算是个基础版本的 RPC 框架,代码精简,可扩展性高,但是只实现了 RPC 最基本的网络通信,而超时熔断、链接管理(保活与重连)、服务注册发现等功能还是欠缺的。因此还是达不到生产环境“开箱即用”的水准,不过 GitHub 就有一个基于 RPC 的功能增强版本——rpcx,支持了大部分主流 RPC 的特性。

虽然目前官方已经宣布不再添加新功能,并推荐使用 gRPC,但是作为 Go 标准库中的 RPC 框架,还是有很多地方值得我们借鉴和学习,比如注册服务时如何保存反射信息等。本课时我们从源码角度分析了 Go 语言原生 RPC 框架,希望能给你带来对 RPC 框架的整体认知。

如果 Go 官方团队准备为 Go RPC 添加新功能,那么你最希望添加哪一项目前尚未支持的功能呢?欢迎你在留言区分享你的想法。


精选评论

**个咪的汪:

go官方的rpc文档地址:https://golang.org/pkg/net/rpc/, 异步方式应该是// 异步的调用方式reply, nil)-call.Done

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

办公模板库 素材蛙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值