gRPC golang開發簡介

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

主要基於官網介紹的文檔總結而來。

需要先瞭解 protocol buffers

為什麼使用gRPC

通過gPRC,我們可以僅僅定義一次service 到.proto檔案中,然後使用gRPC支援的任何開發語言開發用戶端或伺服器。

範例代碼和環境的建立

首先要確保golang開發環境的正確配置,go1.5+。

$ go get -u -v  google.golang.org/grpc

本人在測試中遇到報錯,主要原因在於範例需要

    "golang.org/x/net"    "golang.org/x/text"

的支援,本人的解決方案如下

 $GOPATH/src/golang.org/x/

目錄下,如果golang.org/x/ 不存在則手動建立一個。
然後

git clone https://github.com/golang/net.gitgit clone https://github.com/golang/text.git

範例測試

$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide$ go run server/server.go$ go run client/client.go

下面對範例的代碼進行分析

服務定義

gRPC使用 protocol buffers定義服務。
要定義服務,需要在.proto檔案中做service定義如下:

service RouteGuide {   ...}

然後可以在servie的定義rpc方法,指定對應的request和response類型。gPRC允許開發人員定義4中service方法,這4中方法在範例RouteGuide 中都有用到。

  • 最簡單的RPC方法,用戶端通過調用該方法發送request到服務端,等待伺服器的response,類似正常的函數調用。

    // Obtains the feature at a given position.rpc GetFeature(Point) returns (Feature) {}
  • 服務端單邊stream的RPC( server-side streaming RPC):用戶端調用該方法到服務端,伺服器返回一個stream,用戶端從這個stream中讀取資料直到沒有資料可讀。從範例代碼中可以看到該方法的主要特點是在response類型前加stream。

    // Obtains the Features available within the given Rectangle.  Results are// streamed rather than returned at once (e.g. in a response message with a// repeated field), as the rectangle may cover a large area and contain a// huge number of features.rpc ListFeatures(Rectangle) returns (stream Feature) {}
  • 用戶端單邊stream的RPC(A client-side streaming RPC):用戶端通過使用stream將一系列的資料發送到服務端。用戶端資料發送完畢後就等待服務端把資料全部讀完後發送相應過來。從範例代碼中可以看到該方法主要特點是在request類型前面加stream.:

    // Accepts a stream of Points on a route being traversed, returning a// RouteSummary when traversal is completed.rpc RecordRoute(stream Point) returns (RouteSummary) {}
  • 雙邊stream RPC(bidirectional streaming RPC)。用戶端和服務端都通過讀寫流(read-write stream)向對方發送一系列的訊息。這兩個streams是完全獨立的,所以用戶端和服務端可以隨意的進行讀寫操作:例如,服務端可以等待用戶端的是資料都接收完畢後再往response裡寫資料,或者可以先讀取一條訊息再寫入一條資訊或者是其他的一些讀寫組合方式。從範例代碼中可以看到該方法的主要特點就是在request和response前面都加stream。
// Accepts a stream of RouteNotes sent while a route is being traversed,// while receiving other RouteNotes (e.g. from other users).rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

範例中的.proto檔案包含了服務端方法中使用的request和response類型所使用的類型的協議池訊息類型定義( protocol buffer message type definitions )。

// Points are represented as latitude-longitude pairs in the E7 representation// (degrees multiplied by 10**7 and rounded to the nearest integer).// Latitudes should be in the range +/- 90 degrees and longitude should be in// the range +/- 180 degrees (inclusive).message Point {  int32 latitude = 1;  int32 longitude = 2;}

產生用戶端和服務端代碼

根據.proto檔案產生用戶端和服務端所需的gRPC介面代碼

protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide

建立服務端

服務端代碼主要做兩方面的工作:

  • 實現上一步驟.proto產生的服務端介面。
  • 運行一個gRPC服務來監聽用戶端的請求並且把請求分發到正確的服務端實現裡。

實現RouteGuide

As you can see, our server has a routeGuideServer struct type that implements the generated RouteGuideServer interface:
可以看出我們的服務端有一個routeGuideServer 的結構體類型實現了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 {        ...}...

Simple RPC

GetFeature,從用戶端擷取一個Point然後從資料庫中返回對應的特徵資訊。

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的context對象以及用戶端發過來的點協議池(Point protocol buffer)請求。這個方法返回一個特徵協議池(Feature protocol buffer)對象,對象中包含響應資訊和錯誤。在這個方法中,我們為Feature轉入了正確的資訊然後和nil error一起返回,告訴gRPC伺服器已經完成對RPC的處理,Feature可以返回給用戶端了。

Server-side streaming RPC

ListFeatures是一個服務端stream的RPC,所以我們需要返回多個Features到用戶端。

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}

可以看出,該方法擷取一個request對象以及一個特殊的RouteGuide_ListFeaturesServer 來寫相應。這個方法中我們用Send方法把所有需要返回的Feature特徵寫入到RouteGuide_ListFeaturesServer 中。最後返回一個nil error告訴gRPC服務端已經寫好相應。如果期間有什麼錯誤發生,我們返回一個非nil的error,gRPC會轉換為正確的RPC狀態發送到線路中。

Client-side streaming RPC

.
用戶端流方法RecordRoute中,我們從用戶端擷取一系列的Point然後返回一個RouteSummary 對象包含旅行資訊。從代碼中可以看到該方法裡面沒有任何的請求參數,而是一個RouteGuide_RecordRouteServer 流對象。服務端可以用Rev()方法從RouteGuide_RecordRouteServer 對象中讀取訊息並使用Write()方法往裡面寫訊息。

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’s 的Recv方法不停的從用戶端的請求中讀取資料到requesst對象直到沒有資料可讀。伺服器需要檢測每次Recv返回的error,如果是nil,表示這個stream正常可以繼續讀,如果是io.EOF表示流已經停止了此時服務端可以返回RouteSummary。

Bidirectional streaming RPC

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)                ... // look for notes to be sent to client        for _, note := range s.routeNotes[key] {            if err := stream.Send(note); err != nil {                return err            }        }    }}

這個方法中使用RouteGuide_RouteChatServer 流對象,可以用來讀訊息和寫訊息。然而這次我們通過流返回資料的同時用戶端仍然在往他們的訊息流程中寫訊息。
該方法中往訊息流程中寫訊息使用的是Send() 方法而不是 SendAndClose()
官網中介紹原因如下:具體意思暫時沒有搞明白。

TODO:The syntax for reading and writing here is very similar to our client-streaming method, except the server uses the stream’s Send() method rather than SendAndClose() because it’s writing multiple responses. Although each side will always get the other’s messages in the order they were written, both the client and server can read and write in any order — the streams operate completely independently.

Starting the server

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 TLSgrpcServer.Serve(lis)

如代碼所示,我們建立和啟動一個伺服器需要下面4個步驟:

  • 指定連接埠號碼,用來監聽用戶端的請求,使用
    err := net.Listen("tcp", fmt.Sprintf(":%d", *port)).
    • 建立一個gRPC伺服器執行個體
      grpc.NewServer().
  • 註冊伺服器實現到上一步驟建立的gRPC伺服器執行個體上。

  • 調用Serve啟動服務,阻塞等待直到該進程被殺死或伺服器的stop被調用。

使用TLS

func main() {    flag.Parse()    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))    if err != nil {        grpclog.Fatalf("failed to listen: %v", err)    }    var opts []grpc.ServerOption    if *tls {        creds, err := credentials.NewServerTLSFromFile(*certFile, *keyFile)        if err != nil {            grpclog.Fatalf("Failed to generate credentials %v", err)        }        opts = []grpc.ServerOption{grpc.Creds(creds)}    }    grpcServer := grpc.NewServer(opts...)    pb.RegisterRouteGuideServer(grpcServer, newServer())    grpcServer.Serve(lis)}

Creating the client

建立用戶端

    flag.Parse()    var opts []grpc.DialOption    if *tls {        var sn string        if *serverHostOverride != "" {            sn = *serverHostOverride        }        var creds credentials.TransportCredentials        if *caFile != "" {            var err error            creds, err = credentials.NewClientTLSFromFile(*caFile, sn)            if err != nil {                grpclog.Fatalf("Failed to create TLS credentials %v", err)            }        } else {            creds = credentials.NewClientTLSFromCert(nil, sn)        }        opts = append(opts, grpc.WithTransportCredentials(creds))    } else {        opts = append(opts, grpc.WithInsecure())    }    conn, err := grpc.Dial(*serverAddr, opts...)    if err != nil {        grpclog.Fatalf("fail to dial: %v", err)    }    defer conn.Close()    client := pb.NewRouteGuideClient(conn)

為了能夠調用服務端的方法,我們首先建立一個gRPC通道來和服務端溝通。通過傳入伺服器位址和連接埠號碼給grpc.Dial()來建立。如代碼,我們還可以使用DialOptions來設定grpc中的認證方法。
一旦gRPC通道建立起來後,我們需要一個用戶端來執行RPC,通過.proto建立的pb包中提供的NewRouteGuideClient方法來建立。

Calling service methods

對應服務端的四種方法,用戶端也要採用不同的調用方法。

Simple RPC

feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})if err != nil {        ...}

從代碼中看出,用戶端調用方法GetFeature(在),傳遞通訊協定池(protocol buffer object)對象pb.Point作為參數,同時傳遞一個context.Context 對象,可以讓我們方便的改變RPC的行為,例如逾時或取消RPC。

Server-side streaming RPC

rect := &pb.Rectangle{ ... }  // initialize a pb.Rectanglestream, 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)}

cient.ListFeaturens參見.proto產生的route_guide.pb.go

func (c *routeGuideClient) ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error) {

在這個方法中,同樣的傳遞一個context對象和一個請求,但是返回一個RouteGuide_ListFeaturesClient執行個體,用戶端可以從這個執行個體中讀取得到服務端的響應。
我們使用RouteGuide_ListFeaturesClient的Recv方法來從服務端的響應中讀取到協議池對象Feature中直到沒有資料可讀。同樣的用戶端在讀取時需要檢測返回的err,如果為nil,說明此時stream是正常的繼續可讀,如果為io.EOF表示資料已經到結尾了。

Client-side streaming RPC

// Create a random number of random pointsr := rand.New(rand.NewSource(time.Now().UnixNano()))pointCount := int(r.Int31n(100)) + 2 // Traverse at least two pointsvar points []*pb.Pointfor i := 0; i < pointCount; i++ {    points = append(points, randomPoint(r))}log.Printf("Traversing %d points.", len(points))stream, err := client.RecordRoute(context.Background())if err != nil {    log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)}for _, point := range points {    if err := stream.Send(point); err != nil {        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)

同樣參見route_guide.pb.go中RecordRoute的定義

func (c *routeGuideClient) RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error) {    stream, err := grpc.NewClientStream(ctx, &_RouteGuide_serviceDesc.Streams[1], c.cc, "/routeguide.RouteGuide/RecordRoute", opts...)

RecordRoute方法僅僅需要傳遞一個context參數,然後返回一個RouteGuide_RecordRouteClient流對象用於用戶端寫訊息和讀訊息。

RouteGuide_RecordRouteClient的Send()方法用於向用戶端發送請求,一旦完成用戶端的所有請求,用戶端需要調用CloseAndRecv方法來讓gRPC知道用戶端已經完成請求並且期望獲得一個響應。
如果CloseAndRecv()返回的err不為nil,那麼返回的第一個值就是一個有效服務端響應。

 Bidirectional streaming RPC

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

和RecordRoute類型,方法RouteChat僅需要傳遞一個context對象,返回一個RouteGuide_RouteChatClient用於用戶端讀訊息和寫訊息。

func (c *routeGuideClient) RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error) {    stream, err := grpc.NewClientStream(ctx, &_RouteGuide_serviceDesc.Streams[2], c.cc, "/routeguide.RouteGuide/RouteChat", opts...)

不過和RecordRoute不同的是,用戶端在往用戶端的stream裡寫訊息的同時,服務端也在往服務端的stream中寫訊息。另外,該方法中用戶端中讀和寫是分開獨立啟動並執行,沒有先後順序,還有就是用戶端寫訊息完畢後使用CloseSend而不是CloseAndRecv

後記

之前一直在CSDN上寫文章,後面會逐步轉換到簡書上,還請大家多多支援。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.