gRPC實戰--用Golang編寫經過gRPC進行通訊的服務

What is gRPC: gRPC,顧名思義, Google遠程過程調用。這是Google建立的一種遠程通訊協議,可以讓不一樣的服務輕鬆高效地相互通訊。它提供與服務之間的同步和異步通訊。要了解有關gRPC的更多信息,請訪問 gRPC.iolinux

gRPC最適合內部通訊。它使客戶調用變得更加簡潔,咱們無需擔憂序列化,類型安全以及全部這些事情,由於gRPC爲咱們作到了這一點。git

gPRC使用protobuf,一種類型安全的二進制傳輸格式,旨在實現有效的網絡通訊。要了解有關protobuf的更多信息,請訪問此連接github

性能基準測試結果代表,若是開發人員須要性能和本地調用體驗,則gRPC比http/http2更好。具體測評細節查看該文章。golang

使用Golang構建微服務

咱們選擇Golang(也稱爲Go)做爲此服務的編程語言,選擇gRPC做爲其餘服務的通訊協議,以與咱們的服務進行對話,並使用通過驗證的OAuth 2.0協議上的OpenId身份層來保護咱們的服務。編程

建立Message

爲此,首先咱們須要在gRPC中建立一個簡單的實體表示形式,稱爲message。用gRPC術語表示的消息能夠用做從另外一個服務到一個服務的消息(使用protobuf語法定義的消息)。您能夠想象一隻鴿子從一個承載「冬天來了」消息的服務到另外一個服務,而該服務正在消費該消息來執行上下文操做。小程序

如今,在上面的示例中,發送鴿子的服務是gRPC客戶端,「冬天來了」是咱們的消息,而使用該消息的服務是gRPC服務器在偵聽該消息。關於消息的好處是它能夠來回傳送。windows

message Repository {
    int64 id  = 1;
    string name = 2;
    int64 userId = 3;
    bool isPrivate = 4;
}

定義 gRPC 服務

如今咱們已經建立了一個名爲存儲庫的message以用於通訊,下一步是定義gRPC服務。安全

service RepositoryService {
    //For now we'll try to implement "insert" operation.
    rpc add (Repository) returns (AddRepositoryResponse);
}
 
message AddRepositoryResponse {
    Repository addedRepository = 1;
    Error error = 2;
}
message Error {
    string code = 1;
    string message = 2;
}

在這裏,咱們告訴gRPC編譯器,以「service」關鍵字開頭的代碼段應被視爲gRPC服務。帶有「rpc」關鍵字的方法表示它是一個遠程過程調用,而且編譯器應爲客戶端和服務器運行時生成適當的存根。服務器

咱們還定義了2條消息,告訴鴿子在執行操做後返回成功響應或錯誤響應。網絡

爲Golang服務建立文件夾結構

我假設您已經安裝了Go運行時。若是不這樣作,請按照其官方文檔中的步驟進行操做,網址爲 https://golang.org/doc/instal...

咱們還將使用dep做爲咱們項目的依賴管理工具。 Dep是用於管理golang項目中外部依賴關係的成熟解決方案。咱們使用dep是由於還沒有正式發佈Go模塊支持。

若是您是Windows用戶,則將dep安裝的路徑放在環境的PATH變量中。這使您更容易使用,而無需指定可執行文件的完整路徑便可使用它。

安裝Go運行時後,請執行如下步驟。

$GOPATH/src 中建立名爲 bitbucket-repository-management-service」的目錄。
而後在目錄中設置標準子包,整個項目架構具體以下:

g01.jpg

  • 導航到項目的根目錄並執行如下命令

    • 若是是windows系統, "dep.exe init"
    • 若是是linux系統, "dep init"
  • 上面的命令將建立一個名爲「vendor」的文件夾以及「Gopkg.lock」和「 Gopkg.toml」文件。這兩個文件對於管理咱們項目的不一樣依賴關係很重要。
  • 咱們的下一步是將原型文件放入「internal」文件夾,由於這些文件嚴格綁定到咱們的應用程序。之後,若是咱們想使用不一樣的編程語言將相同的文件用於不一樣的服務,則將爲此建立一個單獨的存儲庫。可是爲了簡單起見,咱們如今將它們放在同一目錄中。
  • 以下圖所示,在「內部」包中建立名爲「proto-files」的文件夾。
  • 在「proto-files」文件夾中,建立兩個子文件夾:

    • domain
    • service

所以最終項目的程序架構佈局將以下所示。

g02.png

接下來,咱們將如下代碼粘貼到名爲「repository.proto」的文件中。此代碼定義了用protobuf語法編寫的框架消息,該消息將在grpc客戶端和服務器之間交換。

syntax = "proto3";

package domain;

option go_package = "bitbucket-repository-management-service/internal/gRPC/domain";

message Repository {
    int64 id  = 1;
    string name = 2;
    int64 userId = 3;
    bool isPrivate = 4;
}

以後,咱們將下面的代碼粘貼到名爲「repository-service.proto」的文件中。該代碼定義了grpc服務定義。它定義了grpc服務器將支持的操做以及可能的輸入和返回類型。

syntax = "proto3";

package service;

option go_package = "bitbucket-repository-management-service/internal/gRPC/service";

import "bitbucket-repository-management-service/internal/proto-files/domain/repository.proto";

//RepositoryService Definition
service RepositoryService {
    rpc add (domain.Repository) returns (AddRepositoryResponse);
}
 
message AddRepositoryResponse {
    domain.Repository addedRepository = 1;
    Error error = 2;
}
message Error {
    string code = 1;
    string message = 2;
}

安裝gRPC編譯器

若是在咱們的系統中未安裝gRPC編譯器,咱們將沒法生成存根。

要安裝協議編譯器,

  • 導航到此連接
  • 選擇最新版本的標籤,請確保選擇一個穩定的版本。
  • 下載適合您的操做系統的二進制文件。
  • 下載後,將其解壓縮到操做系統的path變量正在掃描的位置。

安裝Go綁定並生成存根

沒有Go綁定,咱們的存根就沒有用了。 Go綁定提供了輔助結構,接口和函數,可用於註冊gRPC服務,封送和解封二進制消息等。

爲此,咱們首先須要將很是簡單的Go代碼添加到咱們的server.go文件中,由於默認狀況下,若是項目中沒有go代碼,則dep(咱們的依賴性管理工具)不會下載任何庫。

爲了知足dep的要求,咱們將一些很是基本的go代碼放入cmd/gRPC/main.go文件。

package main

import "fmt"

func main() {
    fmt.Println("gRPC In Action!")
}

g03.png

如今,咱們均可覺得原型緩衝區安裝go綁定了。咱們將執行如下命令進行安裝。

Linux

dep ensure --add google.golang.org/gRPC/github.com/golang/protobuf/protoc-gen-go

Windows

dep.exe ensure -add google.golang.org/gRPC github.com/golang/protobuf/protoc-gen-go

上面的命令會將go綁定下載到「vendor」文件夾中。

如今該生成存根了。
若是您在Windows上,請執行此命令。

protoc.exe -I $env:GOPATH\src --go_out=$env:GOPATH\src $env:GOPATH\src\bitbucket-repository-management-service\internal\proto-files\domain\repository.proto

protoc.exe -I $env:GOPATH\src --go_out=plugins=gRPC:$env:GOPATH\src $env:GOPATH\src\bitbucket-repository-management-service\internal\proto-files\service\repository-service.proto

若是您在Linux上,請執行此命令。

protoc -I $GOPATH/src --go_out=$GOPATH/src $GOPATH/src/bitbucket-repository-management-service/internal/proto-files/domain/repository.proto

protoc -I $GOPATH/src --go_out=plugins=gRPC:$GOPATH/src $GOPATH/src/bitbucket-repository-management-service/internal/proto-files/service/repository-service.proto

上面的命令將在如下標記的子目錄中生成存根。

g04.png

實現 gRPC Service Stub

接下來,編寫咱們本身的服務實現,

  • 咱們將在「internal」目錄中建立一個名爲「impl」的軟件包。
  • 咱們將建立一個名爲RepositoryServiceGrpcImpl的結構,
  • 確保咱們的結構實現了全部gRPC存根方法。

所以,咱們知道咱們的gRPC服務有一個稱爲add的方法。在此過程的早期,咱們將其定義寫入了原始文件中。

rpc add (domain.Repository) returns (AddRepositoryResponse);

爲了實現它的服務契約,咱們將首先聲明一個負責RepositoryService實現的結構。

package impl

import (
    "bitbucket-repository-management-service/internal/gRPC/domain"
    "bitbucket-repository-management-service/internal/gRPC/service"
    "context"
    "log"
)

//RepositoryServiceGrpcImpl is a implementation of RepositoryService Grpc Service.
type RepositoryServiceGrpcImpl struct {
}

//NewRepositoryServiceGrpcImpl returns the pointer to the implementation.
func NewRepositoryServiceGrpcImpl() *RepositoryServiceGrpcImpl {
    return &RepositoryServiceGrpcImpl{}
}

//Add function implementation of gRPC Service.
func (serviceImpl *RepositoryServiceGrpcImpl) Add(ctx context.Context, in *domain.Repository) (*service.AddRepositoryResponse, error) {
    log.Println("Received request for adding repository with id " + strconv.FormatInt(in.Id, 10))

    //Logic to persist to database or storage.
    log.Println("Repository persisted to the storage")

    return &service.AddRepositoryResponse{
        AddedRepository: in,
        Error:           nil,
    }, nil
}

如今是時候編寫服務器配置,端口配置和最小的測試客戶端了,咱們能夠執行這些操做來驗證整個流程。

讓咱們先從gRPC服務器開始。

配置 gRPC Server

咱們將建立一個RepositoryServiceGrpcImpl的實例。
repositoryServiceImpl:= impl.NewRepositoryServiceGrpcImpl()

咱們將建立net.Listener:

func getNetListener(port uint) net.Listener {
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
        panic(fmt.Sprintf("failed to listen: %v", err))
    }

    return lis
}

建立gRPC server:

gRPCServer := gRPC.NewServer()

咱們將服務實現註冊到gRPC服務器。

service.RegisterRepositoryServiceServer(gRPCServer, repositoryServiceImpl)

咱們將綁定net.Listener和gRPC服務器,以使其從指定端口進行通訊。

// start the server
    if err := gRPCServer.Serve(netListener); err != nil {
        log.Fatalf("failed to serve: %s", err)
    }

若是咱們把全部東西都鏈接起來,咱們將獲得如下內容:

package main

import (
    "bitbucket-repository-management-service/internal/gRPC/impl"
    "bitbucket-repository-management-service/internal/gRPC/service"
    "fmt"
    "log"
    "net"

    "google.golang.org/gRPC"
)

func main() {
    netListener := getNetListener(7000)
    gRPCServer := gRPC.NewServer()

    repositoryServiceImpl := impl.NewRepositoryServiceGrpcImpl()
    service.RegisterRepositoryServiceServer(gRPCServer, repositoryServiceImpl)

    // start the server
    if err := gRPCServer.Serve(netListener); err != nil {
        log.Fatalf("failed to serve: %s", err)
    }

}

func getNetListener(port uint) net.Listener {
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
        panic(fmt.Sprintf("failed to listen: %v", err))
    }

    return lis
}

g05.png

配置gRPC Client
要配置客戶端:

咱們將建立與gRPC服務器的鏈接。

serverAddress := "localhost:7000"
conn, e := gRPC.Dial(serverAddress, gRPC.WithInsecure())

咱們將把該鏈接傳遞給gRPC客戶端。

client := service.NewRepositoryServiceClient(conn)

調用gRPC方法:

client.Add(context.Background(), &repositoryModel);

若是咱們在這裏也鏈接起來,它將像:

package main

import (
    "bitbucket-repository-management-service/internal/gRPC/domain"
    "bitbucket-repository-management-service/internal/gRPC/service"
    "context"
    "fmt"

    "google.golang.org/gRPC"
)

func main() {
    serverAddress := "localhost:7000"

    conn, e := gRPC.Dial(serverAddress, gRPC.WithInsecure())

    if e != nil {
        panic(e)
    }
    defer conn.Close()

    client := service.NewRepositoryServiceClient(conn)

    for i := range [10]int{} {
        repositoryModel := domain.Repository{
            Id:        int64(i),
            IsPrivate: true,
            Name:      string("Grpc-Demo"),
            UserId:    1245,
        }

        if responseMessage, e := client.Add(context.Background(), &repositoryModel); e != nil {
            panic(fmt.Sprintf("Was not able to insert Record %v", e))
        } else {
            fmt.Println("Record Inserted..")
            fmt.Println(responseMessage)
            fmt.Println("=============================")
        }
    }
}

g06.png

測試

要運行gRPC服務器,請從項目的根目錄執行如下命令。

go run .\cmd\gRPC\server\main.go

運行客戶端:

go run .\cmd\gRPC\client\main.go

您應該在客戶端的標準輸出流上看到相似的內容。

g07.png

在服務端應該能夠看到以下的內容:

g08.png

總結

咱們建立了一個最小程序,並考慮了gRPC請求響應的最佳實踐。一方面,咱們的gRPC服務器正在偵聽和處理請求,另外一方面,客戶端正在向服務器發送請求。咱們正在使用自定義消息來往/從gRPC服務器/客戶端傳遞消息。

咱們上面的實現是同步的。咱們還沒有解決服務器的異步響應和流式處理。

相關文章
相關標籤/搜索