欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文吃透Go的內(nèi)置RPC原理

 更新時(shí)間:2023年03月03日 08:43:56   作者:捉蟲大師  
這篇文章主要為大家詳細(xì)介紹了Go語言中內(nèi)置RPC的原理。說起?RPC?大家想到的一般是框架,Go?作為編程語言竟然還內(nèi)置了?RPC,著實(shí)讓我有些吃鯨,本文就來一起聊聊吧

從一個(gè) Demo 入手

為了快速進(jìn)入狀態(tài),我們先搞一個(gè) Demo,當(dāng)然這個(gè) Demo 是參考 Go 源碼 src/net/rpc/server.go,做了一丟丟的修改。

首先定義請求的入?yún)⒑统鰠ⅲ?/p>

package common

type Args struct {
	A, B int
}

type Quotient struct {
	Quo, Rem int
}

接著在定義一個(gè)對象,并給這個(gè)對象寫兩個(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?

RPC 是 Remote Procedure Call 的縮寫,一般翻譯為遠(yuǎn)程過程調(diào)用,不過我覺得這個(gè)翻譯有點(diǎn)難懂,啥叫過程?如果查一下 Procedure,就能發(fā)現(xiàn)它就是應(yīng)用程序的意思。

所以翻譯過來應(yīng)該是調(diào)用遠(yuǎn)程程序,說人話就是調(diào)用的方法不在本地,不能通過內(nèi)存尋址找到,只能通過遠(yuǎn)程通信來調(diào)用。

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

說到這里其實(shí)我想吐槽一下,網(wǎng)上經(jīng)常有文章說,既然有 Http,為什么還要有 RPC?如果你理解 RPC,我相信你不會問出這樣的問題,他們是兩個(gè)維度的東西,RPC 關(guān)注的是遠(yuǎn)程調(diào)用的封裝,Http 是一種協(xié)議,RPC 沒有規(guī)定通信協(xié)議,RPC 也可以使用 Http,這不矛盾。這種問法就好像在問既然有了蘋果手機(jī),為什么還要有中國移動(dòng)?

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

  • 首先是遠(yuǎn)程調(diào)用,我們是開了一個(gè) Server,監(jiān)聽了9876端口,然后 Client 與之通信,將這兩個(gè)程序部署在兩臺機(jī)器上,只要網(wǎng)絡(luò)是通的,照樣可以正常工作
  • 其次它符合調(diào)用遠(yuǎn)程方法像調(diào)用本地方法一樣方便,代碼中沒有處理編解碼,也沒有處理通信,只不過方法名以參數(shù)的形式傳入,和一般的 RPC 稍有不同,倒是很像 Dubbo 的泛化調(diào)用

綜上兩點(diǎn),這很 RPC。

下面我將用兩段內(nèi)容分別剖析 Go 內(nèi)置的 RPC Server 與 Client 的原理,來看看 Go 是如何實(shí)現(xiàn)一個(gè) RPC 的。

RPC Server 原理

注冊服務(wù)

這里的服務(wù)指的是一個(gè)具有公開方法的對象,比如上面 Demo 中的 Arith,只需要調(diào)用 Register 就能注冊

rpc.Register(arith)

注冊完成了以下動(dòng)作:

  • 利用反射獲取這個(gè)對象的類型、類名、值、以及公開方法
  • 將其包裝為 service 對象,并存在 server 的 serviceMap 中,serviceMap 的 key 默認(rèn)為類名,比如這里是Arith,也可以調(diào)用另一個(gè)注冊方法 RegisterName 來自定義名稱

注冊 Http Handle

這里你可能會問,為啥 RPC 要注冊 Http Handle。沒錯(cuò),Go 內(nèi)置的 RPC 通信是基于 Http 協(xié)議的,所以需要注冊。只需要一行代碼:

rpc.HandleHTTP()

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

它注冊了兩個(gè)特殊的 Path:/_goRPC_ 和 /debug/rpc,其中有一個(gè)是 Debug 專用,當(dāng)然也可以自定義。

邏輯處理

注冊時(shí)傳入了 RPC 的 server 對象,這個(gè)對象必須實(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)
}

我對這段代碼標(biāo)了號,逐一看:

①:限制了請求的 Method 必須是 CONNECT,如果不是則直接返回錯(cuò)誤,這么做是為什么?看下 Method 字段的注釋就恍然大悟:Go 的 Http Client 是發(fā)不出 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 的連接,劫持后需要手動(dòng)處理連接的關(guān)閉,這個(gè)操作是為了復(fù)用連接

③:先寫一行響應(yīng):

"HTTP/1.0 200 Connected to Go RPC \n\n"

④:開始真正的處理,這里段比較長,大致做了如下幾點(diǎn)事情:

準(zhǔn)備好數(shù)據(jù)、編解碼器

在一個(gè)大循環(huán)里處理每一個(gè)請求,處理流程是:

  • 讀出請求,包括要調(diào)用的service,參數(shù)等
  • 通過反射異步地調(diào)用對應(yīng)的方法
  • 將執(zhí)行結(jié)果編碼寫回連接

說到這里,代碼中有個(gè)對象池的設(shè)計(jì)挺巧妙,這里展開說說。

在高并發(fā)下,Server 端的 Request 對象和 Response 對象會頻繁地創(chuàng)建,這里用了隊(duì)列來實(shí)現(xiàn)了對象池。以 Request 對象池做個(gè)介紹,在 Server 對象中有一個(gè) Request 指針,Request 中有個(gè) next 指針

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

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

在讀取請求時(shí)需要這個(gè)對象,如果池中沒有對象,則 new 一個(gè)出來,有的話就拿到,并將 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
}

請求處理完成時(shí),釋放這個(gè)對象,插入到鏈表的頭部

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

畫個(gè)圖整體感受下:

回到正題,Client 和 Server 之間只有一條連接,如果是異步執(zhí)行,怎么保證返回的數(shù)據(jù)是正確的呢?這里先不說,如果一次性說完了,下一節(jié)的 Client 就沒啥可說的了,你說是吧?

RPC Client 原理

Client 使用第一步是 New 一個(gè) Client 對象,在這一步,它偷偷起了一個(gè)協(xié)程,干什么呢?用來讀取 Server 端的返回,這也是 Go 慣用的伎倆。

每一次 Client 的調(diào)用都被封裝為一個(gè) Call 對象,包含了調(diào)用的方法、參數(shù)、響應(yīng)、錯(cuò)誤、是否完成。

同時(shí) Client 對象有一個(gè) pending map,key 為請求的遞增序號,當(dāng) Client 發(fā)起調(diào)用時(shí),將序號自增,并把當(dāng)前的 Call 對象放到 pending map 中,然后再向連接寫入請求。

寫入的請求先后分別為 Request 和參數(shù),可以理解為 header 和 body,其中 Request 就包含了 Client 的請求自增序號。

Server 端響應(yīng)時(shí)把這個(gè)序號帶回去,Client 接收響應(yīng)時(shí)讀出返回?cái)?shù)據(jù),再去 pending map 里找到對應(yīng)的請求,通知給對應(yīng)的阻塞協(xié)程。

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

Client 、Server 流程都走完,但我們忽略了編解碼細(xì)節(jié),Go RPC 默認(rèn)使用 gob 編解碼器,這里也稍微介紹下 gob。

gob 編解碼

gob 是 Go 實(shí)現(xiàn)的一個(gè) Go 親和的協(xié)議,可以簡單理解這個(gè)協(xié)議只能在 Go 中用。Go Client RPC 對編解碼接口的定義如下:

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é)我不打算寫,因?yàn)槲乙膊幌肟催@一塊,最終結(jié)果就是把結(jié)構(gòu)體編碼成了二進(jìn)制數(shù)據(jù),調(diào)用 writeMessage。

總結(jié)

本文介紹了 Go 內(nèi)置的 RPC Client 和 Server 端原理,能窺探出一點(diǎn)點(diǎn) RPC 的設(shè)計(jì),如果讓你實(shí)現(xiàn)一個(gè) RPC 是不是有些可以參考呢?

到此這篇關(guān)于一文吃透Go的內(nèi)置RPC原理的文章就介紹到這了,更多相關(guān)Go RPC內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • GoLang日志監(jiān)控系統(tǒng)實(shí)現(xiàn)

    GoLang日志監(jiān)控系統(tǒng)實(shí)現(xiàn)

    這篇文章主要介紹了GoLang日志監(jiān)控系統(tǒng)的實(shí)現(xiàn)流程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧
    2022-12-12
  • golang實(shí)現(xiàn)大文件讀取的代碼示例

    golang實(shí)現(xiàn)大文件讀取的代碼示例

    在實(shí)際工作,我們需要讀取大數(shù)據(jù)文件,文件可能上G百G,所以我們不可能一次性的讀取到內(nèi)存,接下來本文給大家介紹了golang實(shí)現(xiàn)大文件讀取的示例,需要的朋友可以參考下
    2024-04-04
  • go語言?http模型reactor示例詳解

    go語言?http模型reactor示例詳解

    這篇文章主要介紹了go語言?http模型reactor,接下來看一段基于reactor的示例,這里運(yùn)行通過?go?run?main.go,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2023-01-01
  • Golang并發(fā)發(fā)送HTTP請求的各種方法

    Golang并發(fā)發(fā)送HTTP請求的各種方法

    在 Golang 領(lǐng)域,并發(fā)發(fā)送 HTTP 請求是優(yōu)化 Web 應(yīng)用程序的一項(xiàng)重要技能,本文探討了實(shí)現(xiàn)此目的的各種方法,從基本的 goroutine 到涉及通道和sync.WaitGroup 的高級技術(shù),需要的朋友可以參考下
    2024-02-02
  • go強(qiáng)制類型轉(zhuǎn)換type(a)以及范圍引起的數(shù)據(jù)差異

    go強(qiáng)制類型轉(zhuǎn)換type(a)以及范圍引起的數(shù)據(jù)差異

    這篇文章主要為大家介紹了go強(qiáng)制類型轉(zhuǎn)換type(a)以及范圍引起的數(shù)據(jù)差異,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-10-10
  • k8s容器互聯(lián)flannel?vxlan通信原理

    k8s容器互聯(lián)flannel?vxlan通信原理

    這篇文章主要為大家介紹了k8s容器互聯(lián)flannel?vxlan通信原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • Golang中crypto/rand庫的使用技巧與最佳實(shí)踐

    Golang中crypto/rand庫的使用技巧與最佳實(shí)踐

    在Golang的眾多隨機(jī)數(shù)生成庫中,crypto/rand?是一個(gè)專為加密安全設(shè)計(jì)的庫,本文主要介紹了Golang中crypto/rand庫的使用技巧與最佳實(shí)踐,感興趣的可以了解一下
    2024-02-02
  • 詳解golang的切片擴(kuò)容機(jī)制

    詳解golang的切片擴(kuò)容機(jī)制

    golang的切片擴(kuò)容機(jī)制是golang面試者繞不開的一扇大門,無論在面試提問,或者面試情景上都繞不開它,今天就說說我理解下的切片擴(kuò)容機(jī)制,感興趣的小伙伴跟著小編一起來看看吧
    2023-07-07
  • Go構(gòu)建高性能的事件管理器實(shí)例詳解

    Go構(gòu)建高性能的事件管理器實(shí)例詳解

    這篇文章主要為大家介紹了Go構(gòu)建高性能的事件管理器實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • 一文了解Go 并發(fā)與并行

    一文了解Go 并發(fā)與并行

    并發(fā)性和并行性是是兩個(gè)既有聯(lián)系又有所區(qū)別的概念,本文主要介紹了Go并發(fā)與并行,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-05-05

最新評論