一文喫透 Go 內置 RPC 原理

hello 大家好呀,我是小樓,這是系列文《Go 底層原理剖析》的第三篇,依舊分析 Http 模塊。我們今天來看 Go 內置的 RPC。說起 RPC 大家想到的一般是框架,Go 作爲編程語言竟然還內置了 RPC,着實讓我有些喫鯨。

從一個 Demo 入手

爲了快速進入狀態,我們先搞一個 Demo,當然這個 Demo 是參考 Go 源碼 src/net/rpc/server.go,做了一丟丟的修改。

package common

type Args struct {
 A, B int
}

type Quotient struct {
 Quo, Rem int
}
type Arith struct{}

func (t *Arith) Multiply(args *common.Args, reply *int) error {
 *reply = args.A * args.B
 return nil
}

func (t *Arith) Divide(args *common.Args, quo *common.Quotient) error {
 if args.B == 0 {
  return errors.New("divide by zero")
 }
 quo.Quo = args.A / args.B
 quo.Rem = args.A % args.B
 return nil
}
func main() {
 arith := new(Arith)
 rpc.Register(arith)
 rpc.HandleHTTP()
 l, e := net.Listen("tcp"":9876")
 if e != nil {
  panic(e)
 }

 go http.Serve(l, nil)

 var wg sync.WaitGroup
 wg.Add(1)
 wg.Wait()
}
func main() {
 client, err := rpc.DialHTTP("tcp""127.0.0.1:9876")
 if err != nil {
  panic(err)
 }

 args := common.Args{A: 7, B: 8}
 var reply int
  // 同步調用
 err = client.Call("Arith.Multiply"&args, &reply)
 if err != nil {
  panic(err)
 }
 fmt.Printf("Call Arith: %d * %d = %d\n", args.A, args.B, reply)

  // 異步調用
 quotient := new(common.Quotient)
 divCall := client.Go("Arith.Divide", args, quotient, nil)
 replyCall := <-divCall.Done

 fmt.Printf("Go Divide: %d divide %d = %+v %+v\n", args.A, args.B, replyCall.Reply, quotient)
}

如果不出意外,RPC 調用成功

這 RPC 嗎

在剖析原理之前,我們先想想什麼是 RPC?

RPC 是 Remote Procedure Call 的縮寫,一般翻譯爲遠程過程調用,不過我覺得這個翻譯有點難懂,啥叫過程?如果查一下 Procedure,就能發現它就是應用程序的意思。

所以翻譯過來應該是調用遠程程序,說人話就是調用的方法不在本地,不能通過內存尋址找到,只能通過遠程通信來調用。

一般來說 RPC 框架存在的意義是讓你調用遠程方法像調用本地方法一樣方便,也就是將複雜的編解碼、通信過程都封裝起來,讓代碼寫起來更簡單。

說到這裏其實我想吐槽一下,網上經常有文章說,既然有 Http,爲什麼還要有 RPC?如果你理解 RPC,我相信你不會問出這樣的問題,他們是兩個維度的東西,RPC 關注的是遠程調用的封裝,Http 是一種協議,RPC 沒有規定通信協議,RPC 也可以使用 Http,這不矛盾。這種問法就好像在問既然有了蘋果手機,爲什麼還要有中國移動?

扯遠了,我們回頭看一下上述的例子是否符合我們對 RPC 的定義。

綜上兩點,這很 RPC。

下面我將用兩段內容分別剖析 Go 內置的 RPC Server 與 Client 的原理,來看看 Go 是如何實現一個 RPC 的。

RPC Server 原理

註冊服務

這裏的服務指的是一個具有公開方法的對象,比如上面 Demo 中的 Arith,只需要調用 Register 就能註冊

rpc.Register(arith)

註冊完成了以下動作:

註冊 Http Handle

這裏你可能會問,爲啥 RPC 要註冊 Http Handle。沒錯,Go 內置的 RPC 通信是基於 Http 協議的,所以需要註冊。只需要一行代碼:

rpc.HandleHTTP()

它調用的是 Http 的 Handle 方法,也就是 HandleFunc 的底層實現,這塊如果不清楚,可以看我之前的文章《一文讀懂 Go Http Server 原理》

它註冊了兩個特殊的 Path:/_goRPC_/debug/rpc,其中有一個是 Debug 專用,當然也可以自定義。

邏輯處理

註冊時傳入了 RPC 的 server 對象,這個對象必須實現 Handler 的 ServeHTTP 接口,也就是 RPC 的處理邏輯入口在這個 ServeHTTP 中:

type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}

我們看 RPC Server 是如何實現這個接口的:

// ServeHTTP implements an http.Handler that answers RPC requests.
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 // ①
  if req.Method != "CONNECT" {
  w.Header().Set("Content-Type""text/plain; charset=utf-8")
  w.WriteHeader(http.StatusMethodNotAllowed)
  io.WriteString(w, "405 must CONNECT\n")
  return
 }
  // ②
 conn, _, err := w.(http.Hijacker).Hijack()
 if err != nil {
  log.Print("rpc hijacking ", req.RemoteAddr, ": ", err.Error())
  return
 }
  // ③
 io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")
 // ④
 server.ServeConn(conn)
}

我對這段代碼標了號,逐一看:

type Request struct {
 // Method specifies the HTTP method (GET, POST, PUT, etc.).
 // For client requests, an empty string means GET.
 //
 // Go's HTTP client does not support sending a request with
 // the CONNECT method. See the documentation on Transport for
 // details.
 Method string
}
"HTTP/1.0 200 Connected to Go RPC \n\n"

說到這裏,代碼中有個對象池的設計挺巧妙,這裏展開說說。

在高併發下,Server 端的 Request 對象和 Response 對象會頻繁地創建,這裏用了隊列來實現了對象池。以 Request 對象池做個介紹,在 Server 對象中有一個 Request 指針,Request 中有個 next 指針

type Server struct {
 ...
 freeReq    *Request
 ..
}

type Request struct {
 ServiceMethod string 
 Seq           uint64
 next          *Request
}

在讀取請求時需要這個對象,如果池中沒有對象,則 new 一個出來,有的話就拿到,並將 Server 中的指針指向 next:

func (server *Server) getRequest() *Request {
 server.reqLock.Lock()
 req := server.freeReq
 if req == nil {
  req = new(Request)
 } else {
  server.freeReq = req.next
  *req = Request{}
 }
 server.reqLock.Unlock()
 return req
}

請求處理完成時,釋放這個對象,插入到鏈表的頭部

func (server *Server) freeRequest(req *Request) {
 server.reqLock.Lock()
 req.next = server.freeReq
 server.freeReq = req
 server.reqLock.Unlock()
}

畫個圖整體感受下:

回到正題,Client 和 Server 之間只有一條連接,如果是異步執行,怎麼保證返回的數據是正確的呢?這裏先不說,如果一次性說完了,下一節的 Client 就沒啥可說的了,你說是吧?

RPC Client 原理

Client 使用第一步是 New 一個 Client 對象,在這一步,它偷偷起了一個協程,幹什麼呢?用來讀取 Server 端的返回,這也是 Go 慣用的伎倆。

每一次 Client 的調用都被封裝爲一個 Call 對象,包含了調用的方法、參數、響應、錯誤、是否完成。

同時 Client 對象有一個 pending map,key 爲請求的遞增序號,當 Client 發起調用時,將序號自增,並把當前的 Call 對象放到 pending map 中,然後再向連接寫入請求。

寫入的請求先後分別爲 Request 和參數,可以理解爲 header 和 body,其中 Request 就包含了 Client 的請求自增序號。

Server 端響應時把這個序號帶回去,Client 接收響應時讀出返回數據,再去 pending map 裏找到對應的請求,通知給對應的阻塞協程。

這不就能把請求和響應串到一起了嗎?這一招很多 RPC 框架也是這麼玩的。

Client 、Server 流程都走完,但我們忽略了編解碼細節,Go RPC 默認使用 gob 編解碼器,這裏也稍微介紹下 gob。

gob 編解碼

gob 是 Go 實現的一個 Go 親和的協議,可以簡單理解這個協議只能在 Go 中用。Go Client RPC 對編解碼接口的定義如下:

type ClientCodec interface {
 WriteRequest(*Request, interface{}) error
 ReadResponseHeader(*Response) error
 ReadResponseBody(interface{}) error

 Close() error
}

同理,Server 端也有一個定義:

type ServerCodec interface {
 ReadRequestHeader(*Request) error
 ReadRequestBody(interface{}) error
 WriteResponse(*Response, interface{}) error
  
 Close() error
}

gob 是其一個實現,這裏只看 Client:

func (c *gobClientCodec) WriteRequest(r *Request, body interface{}) (err error) {
 if err = c.enc.Encode(r); err != nil {
  return
 }
 if err = c.enc.Encode(body); err != nil {
  return
 }
 return c.encBuf.Flush()
}

func (c *gobClientCodec) ReadResponseHeader(r *Response) error {
 return c.dec.Decode(r)
}

func (c *gobClientCodec) ReadResponseBody(body interface{}) error {
 return c.dec.Decode(body)
}

追蹤到底層就是 Encoder 的 EncodeValue 和 DecodeValue 方法,Encode 的細節我不打算寫,因爲我也不想看這一塊,最終結果就是把結構體編碼成了二進制數據,調用 writeMessage。

總結

本文介紹了 Go 內置的 RPC Client 和 Server 端原理,能窺探出一點點 RPC 的設計,如果讓你實現一個 RPC 是不是有些可以參考呢?

本來草稿中貼了很多代碼,但我覺得那樣解讀很難讀下去,於是就刪了又刪。

不過還有一點是我想寫但沒有寫出來的,本文只講了 Go 內置 RPC 是什麼,怎麼實現的,至於它的優缺點,能不能在生產中使用,倒是沒有講,下次寫一篇文章專門講一下,有興趣可以持續關注,我們下期再見,歡迎轉發、收藏、點贊。

  • 搜索關注微信公衆號 "捉蟲大師",後端技術分享,架構設計、性能優化、源碼閱讀、問題排查、踩坑實踐

  • 進技術交流羣加微信 MrRoshi

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ExfVdEM7_YWlQF6GhFn74A