最近,服務化和微服務化逐漸成為中大型分布式系統的主流方式,而RPC在其中也扮演著至關重要的角色。這裡,我們就簡單介紹一下什麼是RPC,以及通過gRPC的一個簡單的例子,來看看如何通過gRPC進行開發。
1. 什麼是RPC
RPC(Remote Procedure Call),即遠程程式調用,是處理序間通訊的一種方式。區別於本地調用(Local Call)中調用者調用同一個地址空間上的函數,RPC允許程式調用另一個地址空間的(通常是共用網路的另一台機器上)的函數,而不用程式員顯式編碼這個遠程調用的細節,使得在程式員的角度看來,遠程調用和本地調用具有一樣的效果。
RPC這個概念很早就已經出現了,在Bruce Jay Nelson在1984年的論文Implementing RPC中就給出了一個具體的實現。同時在這片論文中,Bruce給出了RPC的三個好處:
- 簡單與簡潔的文法:這使得構建分布式系統變得更加容易,更加準確;
- 高效:通過RPC進行函數調用足夠簡單,使得通訊更加迅速;
- 通用:在單機系統中過程往往是不同演算法部分最重要的通訊機制。
其實簡單說,從調用者的角度來看,RPC和普通的本地調用沒有什麼區別,都是調用別的函數的過程;但在實現的角度來看,RPC就是通過網路在不同的機器之間進行通訊,完成普通調用在同一個地址空間就可以完成的參數傳遞以及結果回傳。Bruce的文章發表在1984年,足見他的觀點的高瞻遠矚,我們今天使用的RPC架構基本就是按照這個設計實現的。
從上面的介紹中我們可以看出,RPC和普通本地調用不同的地方在於參數和結果的傳遞方式。RPC是通過網路進行傳遞的,因此,對於一個RPC系統來說就需要仔細思考其中的實現細節。這裡我們不過多進行涉及,僅僅瞭解概念就好。
在論文中,Bruce指出實現一個RPC系統需要如下的幾個部分:
- User;
- User-Stub;
- RPCRuntime;
- Server-Stub;
- Server。
這五個部分的具體結構:
其中User、User-Stub和一個RPCRuntime執行個體運行在調用者(caller)機器上,而Server、Server-Stub和另一個RPCRuntime執行個體運行在被調用者(callee)機器上。
具體的調用過程如下:
當一個User想發起一個遠程調用的時候,它其實首先先本地調用User-Stub中的相關程式,而User-Stub負責得到這次調用具體的遠程程式是什麼以及將調用的參數傳遞給caller端的RPCRuntime,RPCRuntime會將參數通過網路傳遞給目標機器的RPCRuntime。而目標機器上的RPCRuntime收到這個請求後把參數傳遞給Server-Stub,之後Server-Stub解析參數並發起一個普通的本地調用,使得Server進行執行。當Server執行完之後,將結果返回給Server-Stub,然後再通過網路回傳給caller。caller端的User-Stub解析結果後將結果返回給User。
現在的RPC架構基本都支援不同的語言,也就是說User和Server可以是不同的語言實現的程式,那麼就需要RPC架構在中間進行一個介面的定義與統一。這裡就是使用了IDL(Interface Definition Language)來定義介面的,然後通過架構提供的工具來分別對應User和Server產生相應語言的Stub。
2. gRPC登場
gRPC是Google開源的一個RPC架構,它使用protocol buffers作為IDL以及底層訊息轉換格式。就像上面介紹的一樣,gRPC的結構
不多說,我們通過一個簡單的例子看看如何使用gRPC。
3. HelloWorld in gRPC
為了使用gRPC,我們需要Go 1.6或更高的版本:
$ go version
如果沒有安裝Go的話,可以參考這個。安裝完Go後,需要設定好GOPATH。
接下來,我們需要安裝gRPC。我們可以使用下面的命令進行安裝:
$ go get -u google.golang.org/grpc
不過這樣安裝需要科學上網。如果不能科學上網的話,也可以通過github.com
來安裝。
首先進入第一個$GOPATH目錄,go get
預設安裝在第一GOPATH下,建立google.golang.org
目錄,拉取golang
在github
上的鏡像庫:
$ cd /User/valineliu/go/src$ mkdir google.golang.org$ cd google.golang.org/$ git clone https://github.com/grpc/grpc-go$ mv grpc-go/ grpc
然後再安裝Protobuf Buffers v3。protobuf buffers作為gRPC的IDL和底層訊息轉換的工具,我們需要安裝對應的protoc編譯器。
首先在這裡下載相關版本的檔案:
$ wget https://github.com/google/protobuf/releases/download/<version>/protobuf-all-<version>.zip$ unzip protobuf-all-<version>.zip$ cd protobuf-all-<version>$ ./configure$ make$ make install
這樣就完成了protoc的安裝。
接下來安裝protoc的Go外掛程式:
$ go get -u github.com/golang/protobuf/protoc-gen-go
protoc-gen-go是protoc對應Go的一個外掛程式,用來根據IDL描述檔案產生Go語言的代碼。上面的命令會將其安裝在$GOPATH/bin目錄下,然後將其添加到PATH中:
$ export PATH=$PATH:$GOPATH/bin
這樣所有的組件都安裝完畢了,接下來開始我們的HelloWorld
。
項目的目錄結構如下:
GOPATH |__src |__helloworld |__client |__helloworld |__server ...
其中client
目錄用來存放Client端的代碼,helloworld
目錄用來存放服務的IDL定義,server
目錄用來存放Server端的代碼。
首先,我們需要定義我們的服務。進入helloworld
目錄,建立檔案:
$ cd helloworld$ vim helloworld.proto
這裡我是使用vim進行編輯的。定義如下:
syntax = "proto3";package helloworld;message HelloRequest { string name=1;}message HelloReply { string message =1;}service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {}}
這裡我們先不看具體的含義。然後,我們使用protoc
對這個檔案進行編譯:
$ protoc -I. --go_out=plugins=grpc:. helloworld.proto
這樣,目錄下多了一個檔案:helloworld.pb.go
。
然後進入server
目錄,建立檔案:
$ cd ../server$ vim server.go
server.go
的內容如下:
package mainimport ( "log" "net" "golang.org/x/net/context" "google.golang.org/grpc" pb "helloworld/helloworld" "google.golang.org/grpc/reflection")const ( port = ":50051")type server struct{}func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: "Hello " + in.Name}, nil}func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v",err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v",err) }}
這就是Server端的代碼,同樣,我們暫時不考慮具體的細節。
然後進入client
目錄建立檔案:
$ cd ../client$ vim client.go
client.go
的內容如下:
package mainimport ( "log" "os" "time" "golang.org/x/net/context" "google.golang.org/grpc" pb "helloworld/helloworld")const ( address = "localhost:50051" defaultName = "world")func main() { conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v",err) } defer conn.Close() c := pb.NewGreeterClient(conn) name := defaultName if len(os.Args) > 1 { name = os.Args[1] } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.SayHello(ctx, &pb.HelloRequest{Name:name}) if err!=nil { log.Fatalf("could not greet: %v",err) } log.Printf("Greeting: %s",r.Message)}
這就是Client端的代碼。
這樣所有的代碼就編寫完了,然後我們就要讓它跑起來。
首先進入server
目錄,啟動我們的伺服器:
$ cd ../server$ go run server.go
,伺服器啟動起來了:
然後另外開啟一個終端,進入client
目錄,發起一個RPG遠程調用:
$ cd $GOPATH/src/helloworld/client$ go run client.go // one $ go run client.go firework //two
結果
成功!我們的第一個小例子完成了,這篇先到這,To Be Continue。