您好,登錄后才能下訂單哦!
這篇文章主要介紹“Go的內置RPC原理是什么”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Go的內置RPC原理是什么”文章能幫助大家解決問題。
為了快速進入狀態,我們先搞一個 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 }
然后起一個 RPC server:
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() }
最后初始化 RPC Client,并發起調用:
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 是 Remote Procedure Call 的縮寫,一般翻譯為遠程過程調用,不過我覺得這個翻譯有點難懂,啥叫過程?如果查一下 Procedure,就能發現它就是應用程序的意思。
所以翻譯過來應該是調用遠程程序,說人話就是調用的方法不在本地,不能通過內存尋址找到,只能通過遠程通信來調用。
一般來說 RPC 框架存在的意義是讓你調用遠程方法像調用本地方法一樣方便,也就是將復雜的編解碼、通信過程都封裝起來,讓代碼寫起來更簡單。
說到這里其實我想吐槽一下,網上經常有文章說,既然有 Http,為什么還要有 RPC?如果你理解 RPC,我相信你不會問出這樣的問題,他們是兩個維度的東西,RPC 關注的是遠程調用的封裝,Http 是一種協議,RPC 沒有規定通信協議,RPC 也可以使用 Http,這不矛盾。這種問法就好像在問既然有了蘋果手機,為什么還要有中國移動?
扯遠了,我們回頭看一下上述的例子是否符合我們對 RPC 的定義。
首先是遠程調用,我們是開了一個 Server,監聽了9876端口,然后 Client 與之通信,將這兩個程序部署在兩臺機器上,只要網絡是通的,照樣可以正常工作
其次它符合調用遠程方法像調用本地方法一樣方便,代碼中沒有處理編解碼,也沒有處理通信,只不過方法名以參數的形式傳入,和一般的 RPC 稍有不同,倒是很像 Dubbo 的泛化調用
綜上兩點,這很 RPC。
下面我將用兩段內容分別剖析 Go 內置的 RPC Server 與 Client 的原理,來看看 Go 是如何實現一個 RPC 的。
這里的服務指的是一個具有公開方法的對象,比如上面 Demo 中的 Arith
,只需要調用 Register 就能注冊
rpc.Register(arith)
注冊完成了以下動作:
利用反射獲取這個對象的類型、類名、值、以及公開方法
將其包裝為 service 對象,并存在 server 的 serviceMap 中,serviceMap 的 key 默認為類名,比如這里是Arith,也可以調用另一個注冊方法 RegisterName
來自定義名稱
這里你可能會問,為啥 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) }
我對這段代碼標了號,逐一看:
①:限制了請求的 Method 必須是 CONNECT,如果不是則直接返回錯誤,這么做是為什么?看下 Method 字段的注釋就恍然大悟:Go 的 Http Client 是發不出 CONNECT 的請求,也就是 RPC 的 Server 是沒辦法通過 Go 的 Http Client 訪問,限制必須得使用 RPC Client
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 }
②:Hijack 是劫持 Http 的連接,劫持后需要手動處理連接的關閉,這個操作是為了復用連接
③:先寫一行響應:
"HTTP/1.0 200 Connected to Go RPC \n\n"
④:開始真正的處理,這里段比較長,大致做了如下幾點事情:
準備好數據、編解碼器
在一個大循環里處理每一個請求,處理流程是:
讀出請求,包括要調用的service,參數等
通過反射異步地調用對應的方法
將執行結果編碼寫回連接
說到這里,代碼中有個對象池的設計挺巧妙,這里展開說說。
在高并發下,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 就沒啥可說的了,你說是吧?
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 是 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原理是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。