本教程提供了Go使用gRPC的基礎教程git
在教程中你將會學到如何:github
.proto
文件中定義一個服務。繼續以前,請確保你已經對gRPC概念有所瞭解,而且熟悉protocol buffer。須要注意的是教程中的示例使用的是proto3
版本的protocol buffer:你能夠在Protobuf語言指南與Protobuf生成Go代碼指南中瞭解到更多相關知識。golang
咱們的示例是一個簡單的路線圖應用,客戶端能夠獲取路線特徵信息、建立他們的路線摘要,還能夠與服務器或者其餘客戶端交換好比交通狀態更新這樣的路線信息。數據庫
藉助gRPC,咱們能夠在.proto
文件中定義咱們的服務,並以gRPC支持的任何語言來實現客戶端和服務器,客戶端和服務器又能夠在從服務器到你本身的平板電腦的各類環境中運行-gRPC還會爲你解決全部不一樣語言和環境之間通訊的複雜性。咱們還得到了使用protocol buffer的全部優勢,包括有效的序列化(速度和體積兩方面都比JSON更有效率),簡單的IDL(接口定義語言)和輕鬆的接口更新。服務器
首先須要安裝gRPC golang版本的軟件包,同時官方軟件包的examples
目錄裏就包含了教程中示例路線圖應用的代碼。app
$ go get google.golang.org/grpc
複製代碼
而後切換到``grpc-go/examples/route_guide:
目錄:dom
$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide
複製代碼
安裝編譯器最簡單的方式是去https://github.com/protocolbuffers/protobuf/releases 下載預編譯好的protoc二進制文件,倉庫中能夠找到每一個平臺對應的編譯器二進制文件。這裏咱們以Mac Os
爲例,從https://github.com/protocolbuffers/protobuf/releases/download/v3.6.0/protoc-3.6.0-osx-x86_64.zip 下載並解壓文件。tcp
更新PATH
系統變量,或者確保protoc
放在了PATH
包含的目錄中了。ide
$ go get -u github.com/golang/protobuf/protoc-gen-go
複製代碼
編譯器插件protoc-gen-go
將安裝在$GOBIN
中,默認位於$GOPATH/bin
。編譯器protoc
必須在$PATH
中能找到它:函數
$ export PATH=$PATH:$GOPATH/bin
複製代碼
首先第一步是使用protocol buffer定義gRPC服務還有方法的請求和響應類型,你能夠在下載的示例代碼examples/route_guide/routeguide/route_guide.proto
中看到完整的.proto
文件。
要定義服務,你須要在.proto
文件中指定一個具名的service
service RouteGuide {
...
}
複製代碼
而後在服務定義中再來定義rpc
方法,指定他們的請求和響應類型。gRPC容許定義四種類型的服務方法,這四種服務方法都會應用到咱們的RouteGuide
服務中。
// 得到給定位置的特徵
rpc GetFeature(Point) returns (Feature) {} 複製代碼
//得到給定Rectangle中可用的特徵。結果是
//流式傳輸而不是當即返回
//由於矩形可能會覆蓋較大的區域幷包含大量特徵。
rpc ListFeatures(Rectangle) returns (stream Feature) {} 複製代碼
// 接收路線上被穿過的一系列點位, 當行程結束時
// 服務端會返回一個RouteSummary類型的消息.
rpc RecordRoute(stream Point) returns (RouteSummary) {} 複製代碼
//接收路線行進中發送過來的一系列RouteNotes類型的消息,同時也接收其餘RouteNotes(例如:來自其餘用戶)
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} 複製代碼
咱們的.proto
文件中也須要全部請求和響應類型的protocol buffer消息類型定義。好比說下面的Point
消息類型:
// Points被表示爲E7表示形式中的經度-緯度對。
//(度數乘以10 ** 7並四捨五入爲最接近的整數)。
// 緯度應在+/- 90度範圍內,而經度應在
// 範圍+/- 180度(含)
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
複製代碼
接下來要從咱們的.proto
服務定義生成gRPC客戶端和服務端的接口。咱們使用protoc
編譯器和上面安裝的編譯器插件來完成這些工做:
在示例route_guide
的目錄下運行:
protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide
複製代碼
運行命令後會在示例route_guide
目錄的routeguide
目錄下生成route_guide.pb.go
文件。
pb.go
文件裏面包含:
RouteGuide
服務中定義的方法。RouteGuideServer
,接口類型中包含了RouteGuide
服務中定義的全部方法。首先讓咱們看一下怎麼建立RouteGuide
服務器。有兩種方法來讓咱們的RouteGuide
服務工做:
你能夠在剛纔安裝的gPRC包的grpc-go/examples/route_guide/server/server.go找到咱們示例中RouteGuide`服務的實現代碼。下面讓咱們看看他是怎麼工做的。
如你所見,實現代碼中有一個routeGuideServer
結構體類型,它實現了protoc
編譯器生成的pb.go
文件中定義的RouteGuideServer
接口。
type routeGuideServer struct {
...
}
...
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
...
}
...
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
...
}
...
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
...
}
...
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
...
}
...
複製代碼
routeGuideServer
實現咱們全部的服務方法。首先,讓咱們看一下最簡單的類型GetFeature
,它只是從客戶端獲取一個Point
,並從其Feature
數據庫中返回相應的Feature
信息。
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
return feature, nil
}
}
// No feature was found, return an unnamed feature
return &pb.Feature{"", point}, nil
}
複製代碼
這個方法傳遞了RPC上下文對象和客戶端的Point
protocol buffer請求消息,它在響應信息中返回一個Feature
類型的protocol buffer消息和錯誤。在該方法中,咱們使用適當的信息填充Feature
,而後將其返回並返回nil
錯誤,以告知gRPC咱們已經完成了RPC的處理,而且能夠將`Feature返回給客戶端。
如今,讓咱們看一下服務方法中的一個流式RPC。 ListFeatures
是服務器端流式RPC,所以咱們須要將多個Feature
發送回客戶端。
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
for _, feature := range s.savedFeatures {
if inRange(feature.Location, rect) {
if err := stream.Send(feature); err != nil {
return err
}
}
}
return nil
}
複製代碼
如你所見,此次咱們沒有得到簡單的請求和響應對象,而是得到了一個請求對象(客戶端要在其中查找Feature
的Rectangle
)和一個特殊的RouteGuide_ListFeaturesServe
r對象來寫入響應。
在該方法中,咱們填充了須要返回的全部Feature
對象,並使用Send()
方法將它們寫入RouteGuide_ListFeaturesServer
。最後,就像在簡單的RPC中同樣,咱們返回nil
錯誤來告訴gRPC咱們已經完成了響應的寫入。若是此調用中發生任何錯誤,咱們將返回非nil
錯誤; gRPC層會將其轉換爲適當的RPC狀態,以在線上發送。
如今,讓咱們看一些更復雜的事情:客戶端流方法RecordRoute
,從客戶端獲取點流,並返回一個包含行程信息的RouteSummary
。如你所見,這一次該方法根本沒有request
參數。相反,它得到一個RouteGuide_RecordRouteServer
流,服務器可使用該流來讀取和寫入消息-它可使用Recv()
方法接收客戶端消息,並使用SendAndClose()
方法返回其單個響應。
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
var pointCount, featureCount, distance int32
var lastPoint *pb.Point
startTime := time.Now()
for {
point, err := stream.Recv()
if err == io.EOF {
endTime := time.Now()
return stream.SendAndClose(&pb.RouteSummary{
PointCount: pointCount,
FeatureCount: featureCount,
Distance: distance,
ElapsedTime: int32(endTime.Sub(startTime).Seconds()),
})
}
if err != nil {
return err
}
pointCount++
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
featureCount++
}
}
if lastPoint != nil {
distance += calcDistance(lastPoint, point)
}
lastPoint = point
}
}
複製代碼
在方法主體中,咱們使用RouteGuide_RecordRouteServer
的Recv()
方法不停地讀取客戶端的請求到一個請求對象中(在本例中爲Point
),直到沒有更多消息爲止:服務器須要要在每次調用後檢查從Recv()
返回的錯誤。若是爲nil
,則流仍然良好,而且能夠繼續讀取;若是是io.EOF,則表示消息流已結束,服務器能夠返回其RouteSummary。若是錯誤爲其餘值,咱們將返回錯誤「原樣」,以便gRPC層將其轉換爲RPC狀態。
最後讓咱們看一下雙向流式RPC方法RouteChat()
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
s.mu.Lock()
s.routeNotes[key] = append(s.routeNotes[key], in)
// Note: this copy prevents blocking other clients while serving this one.
// We don't need to do a deep copy, because elements in the slice are
// insert-only and never modified.
rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
copy(rn, s.routeNotes[key])
s.mu.Unlock()
for _, note := range rn {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
複製代碼
此次,咱們獲得一個RouteGuide_RouteChatServer
流,就像在客戶端流示例中同樣,該流可用於讀取和寫入消息。可是,此次,當客戶端仍在向其消息流中寫入消息時,咱們會向流中寫入要返回的消息。
此處的讀寫語法與咱們的客戶端流式傳輸方法很是類似,不一樣之處在於服務器使用流的Send()
方法而不是SendAndClose()
,由於服務器會寫入多個響應。儘管雙方老是會按照對方的寫入順序來獲取對方的消息,可是客戶端和服務器均可以以任意順序進行讀取和寫入-流徹底獨立地運行(意思是服務器能夠接受完請求後再寫流,也能夠接收一條請求寫一條響應。一樣的客戶端能夠寫完請求了再讀響應,也能夠發一條請求讀一條響應)
一旦實現了全部方法,咱們還須要啓動gRPC服務器,以便客戶端能夠實際使用咱們的服務。如下代碼段顯示瞭如何啓動RouteGuide
服務。
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})
... // determine whether to use TLS
grpcServer.Serve(lis)
複製代碼
爲了構建和啓動服務器咱們須要:
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))。
grpc.NewServer()
建立一個gRPC server的實例。Serve()
進行阻塞等待,直到進程被殺死或調用Stop()
爲止。在這一部分中咱們將爲RouteGuide
服務建立Go客戶端,你能夠在grpc-go/examples/route_guide/client/client.go 看到完整的客戶端代碼。
要調用服務的方法,咱們首先須要建立一個gRPC通道與服務器通訊。咱們經過把服務器地址和端口號傳遞給grpc.Dial()
來建立通道,像下面這樣:
conn, err := grpc.Dial(*serverAddr)
if err != nil {
...
}
defer conn.Close()
複製代碼
若是你請求的服務須要認證,你能夠在grpc.Dial
中使用DialOptions
設置認證憑證(好比:TLS,GCE憑證,JWT憑證)--不過咱們的RouteGuide
服務不須要這些。
設置gRPC通道後,咱們須要一個客戶端存根來執行RPC。咱們使用從.proto
生成的pb
包中提供的NewRouteGuideClient
方法獲取客戶端存根。
client := pb.NewRouteGuideClient(conn)
複製代碼
生成的pb.go
文件定義了客戶端接口類型RouteGuideClient
並用客戶端存根的結構體類型實現了接口中的方法,因此經過上面獲取到的客戶端存根client
能夠直接調用下面接口類型中列出的方法。
type RouteGuideClient interface {
GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error)
ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error)
RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error)
RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error)
}
複製代碼
每一個實現方法會再去請求gRPC服務端相對應的方法獲取服務端的響應,好比:
func (c *routeGuideClient) GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) {
out := new(Feature)
err := c.cc.Invoke(ctx, "/routeguide.RouteGuide/GetFeature", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
複製代碼
RouteGuideClient
接口的完整實現能夠在生成的pb.go
文件裏找到。
如今讓咱們看看如何調用服務的方法。注意在gRPC-Go中,PRC是在阻塞/同步模式下的運行的,也就是說RPC調用會等待服務端響應,服務端將返回響應或者是錯誤。
調用普通RPC方法GetFeature
如同直接調用本地的方法。
feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
...
}
複製代碼
如你所見,咱們在以前得到的存根上調用該方法。在咱們的方法參數中,咱們建立並填充一個protocol buffer對象(在本例中爲Point對象)。咱們還會傳遞一個context.Context
對象,該對象可以讓咱們在必要時更改RPC的行爲,例如超時/取消正在調用的RPC(cancel an RPC in flight)。若是調用沒有返回錯誤,則咱們能夠從第一個返回值中讀取服務器的響應信息。
這裏咱們會調用服務端流式方法ListFeatures
,方法返回的流中包含了地理特徵信息。若是你讀過上面的建立客戶端的章節,這裏有些東西看起來會很熟悉--流式RPC在兩端實現的方式很相似。
rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
...
}
for {
feature, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
}
log.Println(feature)
}
複製代碼
和簡單RPC調用同樣,調用時傳遞了一個方法的上下文和一個請求。可是咱們取回的是一個RouteGuide_ListFeaturesClient
實例而不是一個響應對象。客戶端可使用RouteGuide_ListFeaturesClient
流讀取服務器的響應。
咱們使用RouteGuide_ListFeaturesClient
的Recv()
方法不停地將服務器的響應讀入到一個protocol buffer響應對象中(本例中的Feature
對象),直到沒有更多消息爲止:客戶端須要在每次調用後檢查從Recv()
返回的錯誤err
。若是爲nil
,則流仍然良好,而且能夠繼續讀取;若是是io.EOF
,則消息流已結束;不然就是必定RPC錯誤,該錯誤會經過err
傳遞給調用程序。
客戶端流方法RecordRoute
與服務器端方法類似,不一樣之處在於,咱們僅向該方法傳遞一個上下文並得到一個RouteGuide_RecordRouteClient
流,該流可用於寫入和讀取消息。
// 隨機的建立一些Points
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points
var points []*pb.Point
for i := 0; i < pointCount; i++ {
points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
stream, err := client.RecordRoute(context.Background())// 調用服務中定義的客戶端流式RPC方法
if err != nil {
log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
if err := stream.Send(point); err != nil {// 向流中寫入多個請求消息
if err == io.EOF {
break
}
log.Fatalf("%v.Send(%v) = %v", stream, point, err)
}
}
reply, err := stream.CloseAndRecv()// 從流中取回服務器的響應
if err != nil {
log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil)
}
log.Printf("Route summary: %v", reply)
複製代碼
RouteGuide_RecordRouteClient
有一個Send()
。咱們可使用它發送請求給服務端。一旦咱們使用Send()寫入流完成後,咱們須要在流上調用
CloseAndRecv()方法讓gRPC知道咱們已經完成了請求的寫入而且指望獲得一個響應。咱們從
CloseAndRecv()方法返回的
err中能夠得到RPC狀態。若是狀態是
nil,
CloseAndRecv()`的第一個返回值就是一個有效的服務器響應。
最後,讓咱們看一下雙向流式RPC RouteChat()
。與RecordRoute
同樣,咱們只向方法傳遞一個上下文對象,而後獲取一個可用於寫入和讀取消息的流。可是,這一次咱們在服務器仍將消息寫入消息流的同時,經過方法的流返回值。
stream, err := client.RouteChat(context.Background())
waitc := make(chan struct{})
go func() {
for {
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
log.Fatalf("Failed to receive a note : %v", err)
}
log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
}
}()
for _, note := range notes {
if err := stream.Send(note); err != nil {
log.Fatalf("Failed to send a note: %v", err)
}
}
stream.CloseSend()
<-waitc
複製代碼
除了在完成調用後使用流的CloseSend()
方法外,此處的讀寫語法與咱們的客戶端流方法很是類似。儘管雙方老是會按照對方的寫入順序來獲取對方的消息,可是客戶端和服務器均可以以任意順序進行讀取和寫入-兩端的流徹底獨立地運行。
要編譯和運行服務器,假設你位於$ GOPATH/src/google.golang.org/grpc/examples/route_guide
文件夾中,只需:
$ go run server/server.go
複製代碼
一樣,運行客戶端:
$ go run client/client.go
複製代碼