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的界面。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM