您好,登錄后才能下訂單哦!
這篇文章主要介紹“Go的內(nèi)置RPC原理是什么”的相關(guān)知識(shí),小編通過(guò)實(shí)際案例向大家展示操作過(guò)程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望這篇“Go的內(nèi)置RPC原理是什么”文章能幫助大家解決問(wèn)題。
為了快速進(jìn)入狀態(tài),我們先搞一個(gè) Demo,當(dāng)然這個(gè) Demo 是參考 Go 源碼 src/net/rpc/server.go
,做了一丟丟的修改。
首先定義請(qǐng)求的入?yún)⒑统鰠ⅲ?/p>
package common type Args struct { A, B int } type Quotient struct { Quo, Rem int }
接著在定義一個(gè)對(duì)象,并給這個(gè)對(duì)象寫(xiě)兩個(gè)方法
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 }
然后起一個(gè) 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,并發(fā)起調(diào)用:
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 // 同步調(diào)用 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) // 異步調(diào)用 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 調(diào)用成功
在剖析原理之前,我們先想想什么是 RPC?
RPC 是 Remote Procedure Call 的縮寫(xiě),一般翻譯為遠(yuǎn)程過(guò)程調(diào)用,不過(guò)我覺(jué)得這個(gè)翻譯有點(diǎn)難懂,啥叫過(guò)程?如果查一下 Procedure,就能發(fā)現(xiàn)它就是應(yīng)用程序的意思。
所以翻譯過(guò)來(lái)應(yīng)該是調(diào)用遠(yuǎn)程程序,說(shuō)人話就是調(diào)用的方法不在本地,不能通過(guò)內(nèi)存尋址找到,只能通過(guò)遠(yuǎn)程通信來(lái)調(diào)用。
一般來(lái)說(shuō) RPC 框架存在的意義是讓你調(diào)用遠(yuǎn)程方法像調(diào)用本地方法一樣方便,也就是將復(fù)雜的編解碼、通信過(guò)程都封裝起來(lái),讓代碼寫(xiě)起來(lái)更簡(jiǎn)單。
說(shuō)到這里其實(shí)我想吐槽一下,網(wǎng)上經(jīng)常有文章說(shuō),既然有 Http,為什么還要有 RPC?如果你理解 RPC,我相信你不會(huì)問(wèn)出這樣的問(wèn)題,他們是兩個(gè)維度的東西,RPC 關(guān)注的是遠(yuǎn)程調(diào)用的封裝,Http 是一種協(xié)議,RPC 沒(méi)有規(guī)定通信協(xié)議,RPC 也可以使用 Http,這不矛盾。這種問(wèn)法就好像在問(wèn)既然有了蘋(píng)果手機(jī),為什么還要有中國(guó)移動(dòng)?
扯遠(yuǎn)了,我們回頭看一下上述的例子是否符合我們對(duì) RPC 的定義。
首先是遠(yuǎn)程調(diào)用,我們是開(kāi)了一個(gè) Server,監(jiān)聽(tīng)了9876端口,然后 Client 與之通信,將這兩個(gè)程序部署在兩臺(tái)機(jī)器上,只要網(wǎng)絡(luò)是通的,照樣可以正常工作
其次它符合調(diào)用遠(yuǎn)程方法像調(diào)用本地方法一樣方便,代碼中沒(méi)有處理編解碼,也沒(méi)有處理通信,只不過(guò)方法名以參數(shù)的形式傳入,和一般的 RPC 稍有不同,倒是很像 Dubbo 的泛化調(diào)用
綜上兩點(diǎn),這很 RPC。
下面我將用兩段內(nèi)容分別剖析 Go 內(nèi)置的 RPC Server 與 Client 的原理,來(lái)看看 Go 是如何實(shí)現(xiàn)一個(gè) RPC 的。
這里的服務(wù)指的是一個(gè)具有公開(kāi)方法的對(duì)象,比如上面 Demo 中的 Arith
,只需要調(diào)用 Register 就能注冊(cè)
rpc.Register(arith)
注冊(cè)完成了以下動(dòng)作:
利用反射獲取這個(gè)對(duì)象的類(lèi)型、類(lèi)名、值、以及公開(kāi)方法
將其包裝為 service 對(duì)象,并存在 server 的 serviceMap 中,serviceMap 的 key 默認(rèn)為類(lèi)名,比如這里是Arith,也可以調(diào)用另一個(gè)注冊(cè)方法 RegisterName
來(lái)自定義名稱(chēng)
這里你可能會(huì)問(wèn),為啥 RPC 要注冊(cè) Http Handle。沒(méi)錯(cuò),Go 內(nèi)置的 RPC 通信是基于 Http 協(xié)議的,所以需要注冊(cè)。只需要一行代碼:
rpc.HandleHTTP()
它調(diào)用的是 Http 的 Handle 方法,也就是 HandleFunc 的底層實(shí)現(xiàn),這塊如果不清楚,可以看我之前的文章《一文讀懂 Go Http Server 原理》。
它注冊(cè)了兩個(gè)特殊的 Path:/_goRPC_
和 /debug/rpc
,其中有一個(gè)是 Debug 專(zhuān)用,當(dāng)然也可以自定義。
注冊(cè)時(shí)傳入了 RPC 的 server 對(duì)象,這個(gè)對(duì)象必須實(shí)現(xiàn) Handler 的 ServeHTTP 接口,也就是 RPC 的處理邏輯入口在這個(gè) ServeHTTP 中:
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
我們看 RPC Server 是如何實(shí)現(xiàn)這個(gè)接口的:
// 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) }
我對(duì)這段代碼標(biāo)了號(hào),逐一看:
①:限制了請(qǐng)求的 Method 必須是 CONNECT,如果不是則直接返回錯(cuò)誤,這么做是為什么?看下 Method 字段的注釋就恍然大悟:Go 的 Http Client 是發(fā)不出 CONNECT 的請(qǐng)求,也就是 RPC 的 Server 是沒(méi)辦法通過(guò) Go 的 Http Client 訪問(wèn),限制必須得使用 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 的連接,劫持后需要手動(dòng)處理連接的關(guān)閉,這個(gè)操作是為了復(fù)用連接
③:先寫(xiě)一行響應(yīng):
"HTTP/1.0 200 Connected to Go RPC \n\n"
④:開(kāi)始真正的處理,這里段比較長(zhǎng),大致做了如下幾點(diǎn)事情:
準(zhǔn)備好數(shù)據(jù)、編解碼器
在一個(gè)大循環(huán)里處理每一個(gè)請(qǐng)求,處理流程是:
讀出請(qǐng)求,包括要調(diào)用的service,參數(shù)等
通過(guò)反射異步地調(diào)用對(duì)應(yīng)的方法
將執(zhí)行結(jié)果編碼寫(xiě)回連接
說(shuō)到這里,代碼中有個(gè)對(duì)象池的設(shè)計(jì)挺巧妙,這里展開(kāi)說(shuō)說(shuō)。
在高并發(fā)下,Server 端的 Request 對(duì)象和 Response 對(duì)象會(huì)頻繁地創(chuàng)建,這里用了隊(duì)列來(lái)實(shí)現(xiàn)了對(duì)象池。以 Request 對(duì)象池做個(gè)介紹,在 Server 對(duì)象中有一個(gè) Request 指針,Request 中有個(gè) next 指針
type Server struct { ... freeReq *Request .. } type Request struct { ServiceMethod string Seq uint64 next *Request }
在讀取請(qǐng)求時(shí)需要這個(gè)對(duì)象,如果池中沒(méi)有對(duì)象,則 new 一個(gè)出來(lái),有的話就拿到,并將 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 }
請(qǐng)求處理完成時(shí),釋放這個(gè)對(duì)象,插入到鏈表的頭部
func (server *Server) freeRequest(req *Request) { server.reqLock.Lock() req.next = server.freeReq server.freeReq = req server.reqLock.Unlock() }
畫(huà)個(gè)圖整體感受下:
回到正題,Client 和 Server 之間只有一條連接,如果是異步執(zhí)行,怎么保證返回的數(shù)據(jù)是正確的呢?這里先不說(shuō),如果一次性說(shuō)完了,下一節(jié)的 Client 就沒(méi)啥可說(shuō)的了,你說(shuō)是吧?
Client 使用第一步是 New 一個(gè) Client 對(duì)象,在這一步,它偷偷起了一個(gè)協(xié)程,干什么呢?用來(lái)讀取 Server 端的返回,這也是 Go 慣用的伎倆。
每一次 Client 的調(diào)用都被封裝為一個(gè) Call 對(duì)象,包含了調(diào)用的方法、參數(shù)、響應(yīng)、錯(cuò)誤、是否完成。
同時(shí) Client 對(duì)象有一個(gè) pending map,key 為請(qǐng)求的遞增序號(hào),當(dāng) Client 發(fā)起調(diào)用時(shí),將序號(hào)自增,并把當(dāng)前的 Call 對(duì)象放到 pending map 中,然后再向連接寫(xiě)入請(qǐng)求。
寫(xiě)入的請(qǐng)求先后分別為 Request 和參數(shù),可以理解為 header 和 body,其中 Request 就包含了 Client 的請(qǐng)求自增序號(hào)。
Server 端響應(yīng)時(shí)把這個(gè)序號(hào)帶回去,Client 接收響應(yīng)時(shí)讀出返回?cái)?shù)據(jù),再去 pending map 里找到對(duì)應(yīng)的請(qǐng)求,通知給對(duì)應(yīng)的阻塞協(xié)程。
這不就能把請(qǐng)求和響應(yīng)串到一起了嗎?這一招很多 RPC 框架也是這么玩的。
Client 、Server 流程都走完,但我們忽略了編解碼細(xì)節(jié),Go RPC 默認(rèn)使用 gob 編解碼器,這里也稍微介紹下 gob。
gob 是 Go 實(shí)現(xiàn)的一個(gè) Go 親和的協(xié)議,可以簡(jiǎn)單理解這個(gè)協(xié)議只能在 Go 中用。Go Client RPC 對(duì)編解碼接口的定義如下:
type ClientCodec interface { WriteRequest(*Request, interface{}) error ReadResponseHeader(*Response) error ReadResponseBody(interface{}) error Close() error }
同理,Server 端也有一個(gè)定義:
type ServerCodec interface { ReadRequestHeader(*Request) error ReadRequestBody(interface{}) error WriteResponse(*Response, interface{}) error Close() error }
gob 是其一個(gè)實(shí)現(xiàn),這里只看 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 的細(xì)節(jié)我不打算寫(xiě),因?yàn)槲乙膊幌肟催@一塊,最終結(jié)果就是把結(jié)構(gòu)體編碼成了二進(jìn)制數(shù)據(jù),調(diào)用 writeMessage。
關(guān)于“Go的內(nèi)置RPC原理是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。