gRpc簡介
gRPC 是Google公司開發的一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。
gRpc官網地址:https://www.grpc.io
gRpc中文文檔地址:http://doc.oschina.net/grpc
gRPC是一款RPC框架,那么先了解Rpc是什么。
Rpc基本概念
RPC(Remote Procedure Call)遠程過程調用,是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議,簡單的理解是一個節點請求另一個節點提供的服務。RPC只是一套協議,基於這套協議規范來實現的框架都可以稱為 RPC 框架,比較典型的有 Dubbo、Thrift 和 gRPC。
RPC 機制和實現過程
RPC 是遠程過程調用的方式之一,涉及調用方和被調用方兩個進程的交互。因為 RPC 提供類似於本地方法調用的形式,所以對於調用方來說,調用 RPC 方法和調用本地方法並沒有明顯區別。
RPC的機制的誕生和基礎概念
1984 年,Birrell 和 Nelson 在 ACM Transactions on Computer Systems 期刊上發表了名為“Implementing remote procedure calls”的論文,該文對 RPC 的機制做了經典的詮釋:
RPC 遠程過程調用是指計算機 A 上的進程,調用另外一台計算機 B 上的進程的方法。其中A 上面的調用進程被掛起,而 B 上面的被調用進程開始執行對應方法,並將結果返回給 A,計算機 A 接收到返回值后,調用進程繼續執行。
發起 RPC 的進程通過參數等方式將信息傳送給被調用方,然后被調用方處理結束后,再通過返回值將信息傳遞給調用方。這一過程對於開發人員來說是透明的,開發人員一般也無須知道雙方底層是如何進行消息通信和信息傳遞的,這樣可以讓業務開發人員更專注於業務開發,而非底層細節。
RPC 讓程序之間的遠程過程調用具有與本地調用類似的形式。比如說某個程序需要讀取某個文件的數據,開發人員會在代碼中執行 read 系統調用來獲取數據。
當 read 實際是本地調用時,read 函數由鏈接器從依賴庫中提取出來,接着鏈接器會將它鏈接到該程序中。雖然 read 中執行了特殊的系統調用,但它本身依然是通過將參數壓入堆棧的常規方式調用的,調用方並不知道 read 函數的具體實現和行為。
當 read 實際是一個遠程過程時(比如調用遠程文件服務器提供的方法),調用方程序中需要引入 read 的接口定義,稱為客戶端存根(client-stub)。遠程過程 read 的客戶端存根與本地方法的 read 函數類似,都執行了本地函數調用。不同的是它底層實現上不是進行操作系統調用讀取本地文件來提供數據,而是將參數打包成網絡消息,並將此網絡消息發送到遠程服務器,交由遠程服務執行對應的方法,在發送完調用請求后,客戶端存根隨即阻塞,直到收到服務器發回的響應消息為止。
下圖展示了遠程方法調用過程中的客戶端和服務端各個階段的操作。
RPC 示意圖
當客戶端發送請求的網絡消息到達服務器時,服務器上的網絡服務將其傳遞給服務器存根(server-stub)。服務器存根與客戶端存根一一對應,是遠程方法在服務端的體現,用來將網絡請求傳遞來的數據轉換為本地過程調用。服務器存根一般處於阻塞狀態,等待消息輸入。
當服務器存根收到網絡消息后,服務器將方法參數從網絡消息中提取出來,然后以常規方式調用服務器上對應的實現過程。從實現過程角度看,就好像是由客戶端直接調用一樣,參數和返回地址都位於調用堆棧中,一切都很正常。實現過程執行完相應的操作,隨后用得到的結果設置到堆棧中的返回值,並根據返回地址執行方法結束操作。以 read 為例,實現過程讀取本地文件數據后,將其填充到 read 函數返回值所指向的緩沖區。
read 過程調用完后,實現過程將控制權轉移給服務器存根,它將結果(緩沖區的數據)打包為網絡消息,最后通過網絡響應將結果返回給客戶端。網絡響應發送結束后,服務器存根會再次進入阻塞狀態,等待下一個輸入的請求。
客戶端接收到網絡消息后,客戶操作系統會將該消息轉發給對應的客戶端存根,隨后解除對客戶進程的阻塞。客戶端存根從阻塞狀態恢復過來,將接收到的網絡消息轉換為調用結果,並將結果復制到客戶端調用堆棧的返回結果中。當調用者在遠程方法調用 read 執行完畢后重新獲得控制權時,它唯一知道的是 read 返回值已經包含了所需的數據,但並不知道該 read 操作到底是在本地操作系統讀取的文件數據,還是通過遠程過程調用遠端服務讀取文件數據。
總結下RPC執行步驟:
1. 調用客戶端句柄,執行傳遞參數。
2. 調用本地系統內核發送網絡消息。
3. 消息傳遞到遠程主機,就是被調用的服務端。
4. 服務端句柄得到消息並解析消息。
5. 服務端執行被調用方法,並將執行完畢的結果返回給服務器句柄。
6. 服務器句柄返回結果,並調用遠程系統內核。
7. 消息經過網絡傳遞給客戶端。
8. 客戶端接受數據。
RPC框架的組成
一個完整的 RPC 框架包含了服務注冊發現、負載、容錯、序列化、協議編碼和網絡傳輸等組件。不同的 RPC 框架包含的組件可能會有所不同,但是一定都包含 RPC 協議相關的組件,RPC 協議包括序列化、協議編解碼器和網絡傳輸棧,如下圖所示:
RPC 協議一般分為公有協議和私有協議。例如,HTTP、SMPP、WebService 等都是公有協議。如果是某個公司或者組織內部自定義、自己使用的,沒有被國際標准化組織接納和認可的協議,往往划為私有協議,例如 Thrift 協議和螞蟻金服的 Bolt 協議。
分布式架構所需要的企業內部通信模塊,往往采用私有協議來設計和研發。相較公有協議,私有協議雖然有很多弊端,比如在通用性上、公網傳輸的能力上,但是高度定制化的私有協議可以最大限度地降低成本,提升性能,提高靈活性與效率。定制私有協議,可以有效地利用協議里的各個字段,靈活滿足各種通信功能需求,比如:CRC 校驗、Server Fail-Fast 機制和自定義序列化器。
在協議設計上,你還需要考慮以下三個關鍵問題:
1. 協議包括的必要字段與主要業務負載字段。協議里設計的每個字段都應該被使用到,避免無效字段。
2. 通信功能特性的支持。比如,CRC 校驗、安全校驗、數據壓縮機制等。
3. 協議的升級機制。畢竟是私有協議,沒有長期的驗證,字段新增或者修改,是有可能發生的,因此升級機制是必須考慮的。
RPC和HTTP區別
RPC 和 HTTP都是微服務間通信較為常用的方案之一,其實RPC 和 HTTP 並不完全是同一個層次的概念,它們之間還是有所區別的。
1. RPC 是遠程過程調用,其調用協議通常包括序列化協議和傳輸協議。序列化協議有基於純文本的 XML 和 JSON、二進制編碼的Protobuf和Hessian。傳輸協議是指其底層網絡傳輸所使用的協議,比如 TCP、HTTP。
2. 可以看出HTTP是RPC的傳輸協議的一個可選方案,比如說 gRPC 的網絡傳輸協議就是 HTTP。HTTP 既可以和 RPC 一樣作為服務間通信的解決方案,也可以作為 RPC 中通信層的傳輸協議(此時與之對比的是 TCP 協議)。
常見的 PRC 框架
目前流行的開源 RPC 框架還是比較多的,有阿里巴巴的 Dubbo、Google 的 gRPC、Facebook 的 Thrift 和 Twitter 的 Finagle 等。
1. Go RPC:Go 語言原生支持的 RPC 遠程調用機制,簡單便捷。
2. gRPC:Google 發布的開源 RPC 框架,是基於 HTTP 2.0 協議的,並支持眾多常見的編程語言,它提供了強大的流式調用能力,目前已經成為最主流的 RPC 框架之一。
3. Thrift:Facebook 的開源 RPC 框架,主要是一個跨語言的服務開發框架,作為老牌開源 RPC 協議,以其高性能和穩定性成為眾多開源項目提供數據的方案選項。
關於更多RPC內容可以看這篇:https://www.cs.rutgers.edu/~pxk/417/notes/03-rpc.html
Go語言RPC過程調用
Go語言原生有RPC包,RPC過程調用實現起來非常簡單。服務端只需實現對外提供的遠程過程方法和結構體,然后將其注冊到 RPC 服務中,客戶端就可以通過其服務名稱和方法名稱進行 RPC 方法調用。
包文檔地址:https://studygolang.com/pkgdoc
這里面有兩個重要方法:
1. 服務端
2. 客戶端調用
編寫服務端代碼 server.go
package main import ( "fmt" "net/rpc" "io" "net" "net/http" ) //創建一個int類型對象 type Panda int /** * argType是客戶端發送過來的內容 * replyType是服務端返回給客戶端的內容 */ func (this *Panda)GetInfo(argType int, replyType *int) error {//GetInfo首字母大小 因為要被外部訪問 fmt.Println("打印對方發送過來的數據:",argType) //執行 *replyType = argType + 123456 return nil } func pandatext(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "hello panda") } func main() { //客戶端頁面的請求 http.HandleFunc("/panda", pandatext) //將類實例化為對象 pd := new(Panda) //服務端注冊一個對象,該對象就作為一個服務被暴露出去 rpc.Register(pd) //連接到網絡 rpc.HandleHTTP() //監聽端口 ln,err := net.Listen("tcp", ":10086") if err != nil { fmt.Println("network error") } http.Serve(ln,nil) }
在編寫客戶端代碼client.go
package main import ( "fmt" "net/rpc" ) func main() { //建立網絡連接 cli, err := rpc.DialHTTP("tcp", "127.0.0.1:10086") if err != nil { fmt.Println("network failed") } var pd int //客戶端調用服務端GetInfo方法,並傳遞參數 err = cli.Call("Panda.GetInfo", 10086, &pd) if err != nil { fmt.Println("call() failed") } fmt.Println("服務端輸出的值:", pd) }
編寫好之后先運行server.go 在運行client.go文件
就可以可以看到返回結果
gRPC特點
在gRPC的客戶端應用可以想調用本地對象一樣直接調用另一台不同的機器上的服務端的應用的對象或者方法,這樣在創建分布式應用的時候更容易。下面看看gRPC的特點:
1. 語言無關,支持多種語言;
2. 基於 IDL 文件定義服務,通過 proto3 工具生成指定語言的數據結構、服務端接口以及客戶端 Stub;
3. 通信協議基於標准的 HTTP/2 設計,支持雙向流、消息頭壓縮、單 TCP 的多路復用、服務端推送等特性,這些特性使得 gRPC 在移動端設備上更加省電和節省網絡流量;
4. 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一種語言無關的高性能序列化框架,基於 HTTP/2 + PB, 保障了 RPC 調用的高性能。
gRPC使用說明

gRPC安裝
使用go命令下載
go get -u google.golang.org/grpc
如果上面不行就使用git下載。
使用git下載:
git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net git clone https://github.com/golang/text.git $GOPATH/src/golang.org/x/text
git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genprot
因為gRPC要使用proto及相關依賴,安裝protobuf可以看我這篇文章:https://www.cnblogs.com/songgj/p/11560565.html
#進入服務端 (先啟動)
cd /Users/songguojun/go/src/google.golang.org/grpc/examples/helloworld/greeter_server/ #進入客戶端端 (服務端啟動后在啟動) cd /Users/songguojun/go/src/google.golang.org/grpc/examples/helloworld/greeter_client/
運行結果,輸出hello world表面可以通信。

gRPC案例
下面使用gRPC來實現一個客戶端和服務端的通行。
我本地代碼結構
1. 先使用protobuf定義服務。
創建myProtobuf.proto文件,編輯如下內容。
syntax = "proto3" ; //package myproto ;
#上面注釋掉是因為報錯 參考https://www.cnblogs.com/tomtellyou/p/12936651.html這篇文章第七點
#https://learnku.com/articles/43758
option go_package = ".;protoes"; //定義服務 service HelloServer { rpc SayHello (HelloReq) returns (HelloRsp){} rpc SayName (NameReq) returns (NameRsp){} } //客戶端發送給服務端 message HelloReq { string name = 1 ; } //服務端返回給客戶端 message HelloRsp { string msg = 1 ; } //客戶端發送給服務端 message NameReq { string name = 1 ; } //服務端返回給客戶端 message NameRsp { string msg = 1 ; }
定義了兩個服務SayHello,SayName及對應的四個消息(message)。
然后在執行命令生成pd.go文件
protoc --go_out=plugins=grpc:./ *.proto #添加grpc插件
2. 編寫服務端server.go
package main import ( "fmt" "net" "google.golang.org/grpc" pd "demo/myproto" //導入proto "context" ) type server struct {} func (this *server) SayHello(ctx context.Context, in *pd.HelloReq) (out *pd.HelloRsp,err error) { return &pd.HelloRsp{Msg:"hello"}, nil } func (this *server) SayName(ctx context.Context, in *pd.NameReq) (out *pd.NameRsp,err error){ return &pd.NameRsp{Msg:in.Name + "it is name"}, nil } func main() { ln, err := net.Listen("tcp", ":10088") if err != nil { fmt.Println("network error", err) } //創建grpc服務 srv := grpc.NewServer() //注冊服務 pd.RegisterHelloServerServer(srv, &server{}) err = srv.Serve(ln) if err != nil { fmt.Println("Serve error", err) } }
3. 編寫客戶端client.go
package main import ( "fmt" "google.golang.org/grpc" pd "demo/myproto" //導入proto "context" ) func main() { //客戶端連接服務端 conn, err := grpc.Dial("127.0.0.1:10088", grpc.WithInsecure()) if err != nil { fmt.Println("network error", err) } //網絡延遲關閉 defer conn.Close() //獲得grpc句柄 c := pd.NewHelloServerClient(conn) //通過句柄進行調用服務端函數SayHello re1, err := c.SayHello(context.Background(),&pd.HelloReq{Name:"songguojun"}) if err != nil { fmt.Println("calling SayHello() error", err) } fmt.Println(re1.Msg) //通過句柄進行調用服務端函數SayName re2, err := c.SayName(context.Background(),&pd.NameReq{Name:"songguojun"}) if err != nil { fmt.Println("calling SayName() error", err) } fmt.Println(re2.Msg) }
運行結果如下
gRPC四種通信方式
1. 簡單Rpc(Simple RPC):就是一般的rpc調用,一個請求對象對應一個返回對象。
2. 服務端流式rpc(Server-side streaming RPC):一個請求對象,服務端可以傳回多個結果對象。
3. 客戶端流式rpc(Client-side streaming RPC):客戶端傳入多個請求對象,服務端返回一個響應結果。
4. 雙向流式rpc(Bidirectional streaming RPC):結合客戶端流式rpc和服務端流式rpc,可以傳入多個對象,返回多個響應對象。