1. 程式人生 > 其它 >gRPC詳細入門介紹

gRPC詳細入門介紹

一、gRPC是什麼?

gRPC的官方文件:https://grpc.io/docs/

gRPC可以使用協議緩衝區作為其介面定義語言(IDL)和底層訊息交換格式,是一個高效能、開源和通用的RPC框架,面向服務端和移動端,基於HTTP/2設計。它使客戶端和伺服器應用程式能夠透明地通訊,並使構建連線系統變得更加容易。

簡介

概述

在gRPC中,客戶端應用程式可以直接呼叫不同機器上的服務端應用程式上的方法,就想呼叫本地物件一樣,可以更輕鬆地建立分散式應用程式和服務。與許多RPC系統一樣,gRPC基於定義服務的思想,指定可以通過引數和返回型別遠端呼叫的方法。在伺服器端,伺服器實現了這個介面並執行一個gRPC伺服器來處理客戶端呼叫。在客戶端,客戶端有一個存根(在某些語言中簡稱為客戶端),它提供與伺服器相同的方法。

gPRC客戶端和伺服器可以在各種環境中執行和相互通訊 - 從 Google 內部的伺服器到您自己的桌面 - 並且可以用任何 gRPC 支援的語言編寫。因此,例如,您可以輕鬆地使用 Java、Go、Python 或 Ruby 中的客戶端建立一個 gRPC 伺服器。此外,最新的 Google API 將具有其介面的 gRPC 版本,讓您可以輕鬆地將 Google 功能構建到您的應用程式中。

Protocol Buffers 協議緩衝區

gRPC使用Protocol Buffers(基於Google開源的結構資料序列化工具)。使用Protocol Buffers的第一步就是要在proto檔案中序列化資料結構:這是一個帶有.proto副檔名的普通文字檔案。協議緩衝區資料被構造為message,其中每條訊息都是一個小的資訊邏輯記錄,包含一系列稱為欄位的name-value

message Person{
    string name = 1;
    int32 id = 2;
    bool has_ponycopter = 3;
}

一旦您指定了資料結構,您就可以使用協議緩衝區編譯器protoc根據您的 proto 定義以您的首選語言(例如:go、java、python、csharp等)生成資料訪問類。這些為每個欄位提供了簡單的訪問器,如name()set_name(),以及將整個結構序列化/解析為原始位元組/從原始位元組序列化/解析整個結構的方法。

在普通的 proto 檔案中定義 gRPC 服務,並將 RPC 方法引數和返回型別指定為協議緩衝區訊息:

// The greeter service definition
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The Request message containing the user's name
message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

Protocol Buffer 比 XML、JSON快很多,因為是基於二進位制流,比字串更省頻寬,傳輸速度快。

Protocol Buffers 版本

一般來說,我們都是使用proto3,這樣可以使用所有gRPC支援的語言,並且避免與proto2客戶端通訊的相容性問題。

syntax = "proto3";

核心概念

服務定義

與許多 RPC 系統一樣,gRPC 基於定義服務的思想,指定可以通過引數和返回型別遠端呼叫的方法。預設情況下,gRPC 使用協議緩衝區作為介面定義語言 (IDL),用於描述服務介面和有效載荷訊息的結構。

gRPC允許定義四種服務方法:

  • 一元RPC,客戶端向服務端傳送單個請求並返回單個響應,就像普通的函式呼叫一樣。

    rpc SayHello(HelloRequest) returns (HelloResponse);
  • 服務端stream RPC,客戶端向伺服器傳送請求並獲取stream以讀取一系列訊息。客戶端從返回的stream中讀取,直到沒有更多訊息。gRPC能保證單個RPC呼叫中的訊息排序。

    rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
  • 客戶端stream RPC,客戶端寫入一系列訊息並將它們傳送到伺服器,再次使用提供的stream。一旦客戶端完成寫入訊息,它等待伺服器讀取它們並返回其響應。gRPC能保證單個RPC呼叫中的訊息排序。

    rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  • 雙向stream PRC,其中雙方使用讀寫stream傳送一系列訊息。這兩個stream獨立執行,因此客戶端和服務端可以按照它們喜歡的任何順序進行讀寫。

    rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

使用API

.proto檔案中的服務定義開始,gRPC 提供了協議緩衝區編譯器外掛,用於生成客戶端和伺服器端程式碼。gRPC 使用者通常在客戶端呼叫這些 API,並在伺服器端實現相應的 API。

  • 在伺服器端,伺服器實現服務宣告的方法並執行 gRPC 伺服器來處理客戶端呼叫。gRPC 基礎設施解碼傳入請求、執行服務方法並編碼服務響應。

  • 在客戶端,客戶端有一個稱為stub(對於某些語言,首選術語是client)的本地物件,它實現與服務相同的方法。然後客戶端可以在本地物件上呼叫這些方法,將呼叫的引數包裝在適當的協議緩衝區訊息型別中 - gRPC 負責將請求傳送到伺服器並返回伺服器的協議緩衝區響應。

同步與非同步

在伺服器響應到達之前阻塞的同步 RPC 呼叫最接近於 RPC 所追求的過程呼叫的抽象。另一方面,網路本質上是非同步的,在許多情況下,能夠在不阻塞當前執行緒的情況下啟動 RPC 很有用。

大多數語言中的 gRPC 程式設計 API 都有同步和非同步兩種風格。

RPC生命週期

瞭解當 gRPC 客戶端呼叫 gRPC 伺服器方法時會發生什麼

一元RPC

首先考慮客戶端傳送單個請求並返回單個響應的最簡單的 RPC 型別。

  1. 一旦客戶端呼叫了存根方法,伺服器就會收到通知:RPC 已經被呼叫,其中包含客戶端的元資料 、方法名稱和指定的截止日期(如果適用)。

  2. 然後伺服器可以立即發回它自己的初始元資料(必須在任何響應之前傳送),或者等待客戶端的請求訊息。首先發生的是特定於應用程式的。

  3. 一旦伺服器收到客戶端的請求訊息,它就會執行建立和填充響應所需的任何工作。然後將響應(如果成功)連同狀態詳細資訊(狀態程式碼和可選狀態訊息)和可選的尾隨元資料一起返回給客戶端。

  4. 如果響應狀態為OK,則客戶端得到響應,在客戶端完成呼叫。

伺服器流式RPC

伺服器流式 RPC 類似於一元 RPC,不同之處在於伺服器返回訊息流以響應客戶端的請求。傳送完所有訊息後,伺服器的狀態詳細資訊(狀態程式碼和可選狀態訊息)和可選的尾隨元資料將傳送到客戶端。這樣就完成了伺服器端的處理。一旦客戶端擁有伺服器的所有訊息,它就完成了。

客戶端流式 RPC

客戶端流式 RPC 類似於一元 RPC,不同之處在於客戶端向伺服器傳送訊息流而不是單個訊息。伺服器用一條訊息(連同它的狀態詳細資訊和可選的尾隨元資料)進行響應,通常但不一定是在它收到所有客戶端的訊息之後。

雙向流式RPC

在雙向流式 RPC 中,呼叫由呼叫方法的客戶端和接收客戶端元資料、方法名稱和截止日期的伺服器發起。伺服器可以選擇發回其初始元資料或等待客戶端開始流式傳輸訊息。

客戶端和伺服器端流處理是特定於應用程式的。由於兩個流是獨立的,客戶端和伺服器可以按任意順序讀寫訊息。例如,伺服器可以等到收到所有客戶端的訊息後再寫入訊息,或者伺服器和客戶端可以玩“乒乓”——伺服器收到請求,然後發回響應,然後客戶端傳送基於響應的另一個請求,依此類推。

二、總結

gRPC的特性

  • 使用Protocol Buffers結構資料序列化工具

  • 可以跨語言使用

  • 安裝簡單、擴充套件方便(每秒可達到百萬個RPC)

  • 基於HTTP/2協議

gRPC使用流程

  • 定義標準的proto檔案

  • 通過proto工具生成標準程式碼

  • 服務端使用生成的程式碼提供服務

  • 客戶端使用生成的程式碼呼叫服務

為什麼要使用gRPC?

主要使用場景:

  • 低延遲、高度可擴充套件的分散式系統

  • 開發與雲伺服器通訊的移動客戶端

  • 設計一個需要準確、高效且獨立於語言的新協議

  • 分層設計以啟用擴充套件,例如:身份驗證、負載平衡、日誌記錄和監控等。

三、實踐

關於protoc和protoc-gen-go的安裝見上一篇文章。

新建一個go專案,專案目錄如下:

goGrpcDemo
│  client.go
│  go.mod
│  go.sum
│  server.go
│  
├─.idea
│      .gitignore
│      encodings.xml
│      goGrpcDemo.iml
│      misc.xml
│      modules.xml
│      runConfigurations.xml
│      workspace.xml
│      
├─cmd
│      gen-golang.sh
│      
└─proto
        hello.pb.go
        hello.proto

proto檔案

hello.proto程式碼如下:

syntax = "proto3";

package proto;

option go_package = "../proto";

service HelloWorld {
  rpc SayHelloWorld(HelloWorldRequest) returns (HelloWorldResponse) {}
  rpc SayGoodNight(HelloWorldRequest) returns (HelloWorldResponse) {}
}

message HelloWorldRequest {
  string name = 1;
}

message HelloWorldResponse {
  string message = 1;
}

在go1.15+以上,是需要go_package的,否則生成會報錯;

而hello.pb.go是通過hello.proto使用命令生成的,命令我寫在了gen-golang.sh中:

#!/usr/bin/env bash

protoDir="../proto"
outDir="../proto"

protoc -I ${protoDir}/ ${protoDir}/*proto --go_out=plugins=grpc:${outDir}

protoc工具引數介紹:

  • -I:指定要引入proto檔案的路徑,可以指定多個 -I 引數,編譯時按順序查詢,不指定預設當前目錄

  • --go_out:指定go語言的訪問類

  • plugins:指定依賴的外掛

由於是在window上,我們可以使用git bash客戶端執行.sh檔案,如下:

定義服務端

server.go程式碼如下:

package main

import (
    "context"
    "fmt"
    "goGrpcDemo/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
    "log"
    "net"
)

const port = ":50000"

type server struct {
    proto.UnimplementedHelloWorldServer
}

// SayHelloWorld 服務端必須實現的介面
func (s *server) SayHelloWorld(ctx context.Context, request *proto.HelloWorldRequest) (*proto.HelloWorldResponse, error) {
    fmt.Printf("%s 說:Hello World", request.Name)
    return &proto.HelloWorldResponse{Message: "success"}, nil
}
func (s *server) SayGoodNight(ctx context.Context, request *proto.HelloWorldRequest) (*proto.HelloWorldResponse, error) {
    fmt.Printf("%s 說:GoodNight", request.Name)
    return &proto.HelloWorldResponse{Message: "success"}, nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen:%v", err)
    }

    s := grpc.NewServer()
    reflection.Register(s)  // 為了後續使用grpcurl測試
    proto.RegisterHelloWorldServer(s, &server{})

    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve:%v", err)
    }
}

定義客戶端

client.go程式碼如下:

package main

import (
    "context"
    "fmt"
    "goGrpcDemo/proto"
    "google.golang.org/grpc"
    "log"
    "time"
)

const address = "localhost:50000"

func main() {
    conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("not connect:%v", err)
    }
    defer conn.Close()

    // 客戶端
    client := proto.NewHelloWorldClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    // 呼叫SayHelloWorld方法
    r, err := client.SayHelloWorld(ctx, &proto.HelloWorldRequest{
        Name: "cxt",
    })

    if err != nil {
        log.Fatalf("error:%v", err)
    }
    fmt.Printf("接收服務端返回訊息: %s", r.Message)
}

先執行服務端程式碼(server.go)再通過客戶端像服務端發起請求,結果如下:

四、grpcurl命令工具使用

安裝

使用go get命令進行安裝

go get -u github.com/fullstorydev/grpcurl
# go get 獲取包成功後,進入github.com/fullstorydev/grpcurl/cmd/grpcurl目錄下,執行
go install
# 這樣在gopath下的bin目錄就會生成grpcurl.exe

使用

由於grpcurl是基於反射的,可以看到我們在server.go中加入了這樣一行程式碼

reflection.Register(s)

常用命令如下:

# 1、查詢服務列表
grpcurl -plaintext 127.0.0.1:50000 list
# 2、查詢服務提供的方法
grpcurl -plaintext 127.0.0.1:50000 list proto.HelloWorld
# 3、服務提供的方法更詳細的描述
grpcurl -plaintext 127.0.0.1:50000 describe proto.HelloWorld
# 4、獲取服務方法的請求型別資訊
grpcurl -plaintext 127.0.0.1:50000 describe proto.HelloWorldRequest
# 5、呼叫服務的方法
grpcurl -plaintext -d '{"name":"cxt"}' 127.0.0.1:50000 proto.HelloWorld/SayHelloWorld

五、grpcui介面工具使用

安裝

使用go get命令安裝

go get -u github.com/fullstorydev/grpcui
# go get 獲取包成功後,進入github.com/fullstorydev/grpcui/cmd/grpcui,執行
go install
# 這樣在gopath下的bin目錄就會生成grpcui.exe

使用

常用命令如下:

# 執行web介面,然後使用提示的連結在瀏覽器開啟
grpcui -plaintext 127.0.0.1:50000

頁面很簡單,就跟我們常見的postman類似的操作:

撒花,就不一一截圖了,自個研究下grpcui的介面。