gRPC(八)生態 grpc-gateway 應用:同一個服務端支援Rpc和Restful Api

lin鍾一發表於2022-11-18

個人網站:linzyblog.netlify.app/
示例程式碼已經上傳到github:點選跳轉
gRPC官方文件:點選跳轉
grpc-gateway官方文件:點選跳轉
源自 coreos 的一篇部落格,轉載到了 gRPC 官方部落格 gRPC with REST and Open APIs

1、簡述

取自官方概述:
grpc-gateway is a plugin of protoc. It reads gRPC service definition, and generates a reverse-proxy server which translates a RESTful JSON API into gRPC. This server is generated according to custom options in your gRPC definition.

gRPC-Gateway 是 protoc 的外掛。它讀取gRPC服務定義並生成反向代理伺服器,將 RESTful JSON API 轉換為 gRPC。該伺服器是根據服務定義中的 google.api.http 註釋生成的。

2、出現

etcd v3 改用 gRPC 後為了相容原來的 API,同時要提供 HTTP/JSON 方式的API,為了滿足這個需求,要麼開發兩套 API,要麼實現一種轉換機制,所以grpc-gateway誕生了。

  • 透過protobuf的自定義option實現了一個閘道器,服務端同時開啟gRPC和HTTP服務。
  • HTTP服務接收客戶端請求後轉換為grpc請求資料,獲取響應後轉為json資料返回給客戶端。
  • 當 HTTP 請求到達 gRPC-Gateway 時,它將 JSON 資料解析為 Protobuf 訊息。使用解析的 Protobuf 訊息發出正常的 Go gRPC 客戶端請求。
  • Go gRPC 客戶端將 Protobuf 結構編碼為 Protobuf 二進位制格式,然後將其傳送到 gRPC 伺服器。
  • gRPC 伺服器處理請求並以 Protobuf 二進位制格式返回響應。
  • Go gRPC 客戶端將其解析為 Protobuf 訊息,並將其返回到 gRPC-Gateway,後者將 Protobuf 訊息編碼為 JSON 並將其返回給原始客戶端。

架構如下

在這裡插入圖片描述

由於本實踐偏向 Grpc+Grpc Gateway的方面,我們的需求是同一個服務端支援Rpc和Restful Api,那麼就意味著http2、TLS等等的應用,功能方面就是一個服務端能夠接受來自grpc和Restful Api的請求並響應。

本文示例程式碼已經上傳到github:點選跳轉

1、目錄結構

新建grpc-gateway-example資料夾,我們專案的初始目錄目錄如下:

grpc-gateway-example/
├── certs
├── client
├── cmd
├── pkg
├── proto
│   ├── google
│   │   └── api
│   │   │   └── annotations.proto
│   │   │   └── http.proto
│   │   └── protobuf
│   │   │   └── descriptor.proto
├── server
└── Makefile
  • certs:存放證照憑證
  • client:客戶端
  • cmd:存放 cobra 命令模組
  • pkg:第三方公共模組
  • proto:protobuf的一些相關檔案(含.proto、pb.go、.pb.gw.go),google/api中用於存放annotations.proto、http.proto、google/protobuf中用於存放descriptor.proto
    • 如果你生成Go程式碼的時候出現File not found,那一定就是找不到下面的檔案。
    • annotations.proto和http.proto檔案需要手動從 github.com/googleapis/googleapis/t...地址複製到自己的專案中或者手動複製程式碼!!
    • descriptor.proto檔案可以直接複製程式碼!!
  • server:服務端
  • Makefile:用於存放編譯的程式碼。

2、環境準備

1)Protobuf

詳細的請移步到《gRPC(二)入門:Protobuf入門》

2)gRPC

詳細的請移步到《gRPC(三)基礎:gRPC快速入門》

3)gRPC-Gateway

gRPC-Gateway 只是一個外掛,只需要安裝一下就可以了。這裡建議科學上網:

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

3、編寫 IDL

1)google.api

proto 目錄中有 google/api 目錄,它用到了 google 官方提供的兩個 api 描述檔案,主要是針對 grpc-gateway 的 http 轉換提供支援,定義了 Protocol Buffer 所擴充套件的 HTTP Option。

2)hello.proto

編寫Demo的 .proto檔案,我們在 proto目錄下新建 hello.proto 檔案,寫入檔案內容:

syntax = "proto3";

package proto;
option go_package = "./proto/helloworld;helloworld";

import "proto/google/api/annotations.proto";

// 定義Hello服務
service Hello {
  // 定義SayHello方法
  rpc SayHello(HelloRequest) returns (HelloResponse) {
    // http option 閘道器
    option (google.api.http) = {
      post: "/hello_world"
      body: "*"
    };
  }
}

// HelloRequest 請求結構
message HelloRequest {
  string referer = 1;
}

// HelloResponse 響應結構
message HelloResponse {
  string message = 1;
}

在 hello.proto 檔案中,引用了 google/api/annotations.proto,達到支援HTTP Option的效果

  • 定義了一個 serviceRPC 服務 HelloWorld,在其內部定義了一個 HTTP Option 的POST方法,HTTP 響應路徑為/hello_world
  • 定義message型別HelloWorldRequest、HelloWorldResponse,用於響應請求和返回結果。

每個方法都必須新增 google.api.http 註解後 gRPC-Gateway 才能生成對應 http 方法。
其中post為 HTTP Method,即 POST 方法,/hello_world 則是請求路徑。

3)編譯proto

在Makefile檔案內輸入以下內容:

protoc:
        protoc  --go_out=.  --go-grpc_out=. --grpc-gateway_out=. ./proto/*.proto
  • Go Plugins 用於生成 .pb.go 檔案
  • gRPC Plugins 用於生成 _grpc.pb.go
  • gRPC-Gateway 則是 pb.gw.go

使用 make protoc 編譯proto:

make protoc
protoc  --go_out=.  --go-grpc_out=. --grpc-gateway_out=. ./proto/*.proto

在這裡插入圖片描述

4、製作證照

詳細的請移步到《gRPC(五)進階:透過TLS建立安全連線》

在服務端支援Rpc和Restful Api,需要用到TLS,因此我們要先製作證照

進入certs目錄,生成TLS所需的公鑰金鑰檔案

1)生成CA根證照

ca.conf 檔案並寫入內容如下:

[ req ]
default_bits       = 4096
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = GB
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = linzyblog.netlify.app
commonName_max              = 64
commonName_default          = linzyblog.netlify.app
  1. 生成ca私鑰,得到ca.key
openssl genrsa -out ca.key 4096
  1. 生成ca證照籤發請求,得到ca.csr
$ openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf
GB [CN]:
State or Province Name (full name) [ZheJiang]:
Locality Name (eg, city) [HuZhou]:
Organization Name (eg, company) [Step]:
linzyblog.netlify.app [linzyblog.netlify.app]:
  1. 生成ca根證照,得到ca.crt
openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt

在這裡插入圖片描述

2)生成終端使用者證照

server.conf 寫入以下內容:

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = linzyblog.netlify.app

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = grpc-gateway-example
IP      = 127.0.0.1
  1. 生成私鑰,得到server.key
openssl genrsa -out server.key 2048
  1. 生成證照籤發請求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf

這裡也一直回車就好。

  1. 用CA證照生成終端使用者證照,得到server.crt
openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extensions req_ext -extfile server.conf

在這裡插入圖片描述

這樣我們需要的證照憑證就足夠了

1、Cobra介紹

官方文件:點選跳轉

Cobra 是一個用於建立強大的現代 CLI 應用程式的庫。它提供了一個簡單的介面來建立強大的現代 CLI 介面,類似於 git 和 go 工具。

Cobra 提供:

  • 簡易的子命令列模式
  • 完全相容 POSIX 的命令列模式(包括短版和長版)
  • 巢狀的子命令
  • 全域性、本地和級聯flags
  • 使用Cobra很容易的生成應用程式和命令,使用 cobra create appnamecobra add cmdname
  • 提供智慧提示
  • 自動生成commands和flags的幫助資訊
  • 自動生成詳細的 help 資訊,如 app -help。
  • 自動識別幫助 flag、 -h,--help
  • 自動生成應用程式在 bash 下命令自動完成功能。
  • 自動生成應用程式的 man 手冊。
  • 命令列別名。
  • 自定義 helpusage 資訊。
  • 可選的與 viper 的緊密整合。

2、概念

Cobra 建立在命令(commands)、引數(arguments )、選項(flags)的結構之上。

  • commands:命令代表行為,一般表示 action,即執行的二進位制命令服務。同時可以擁有子命令(children commands)
  • arguments:引數代表命令列引數。
  • flags:選項代表對命令列為的改變,即命令列選項。二進位制命令的配置引數,可對應配置檔案。引數可分為全域性引數和子命令引數。

最好的命令列程式在實際使用時,就應該像在讀一段優美的語句,能夠更加直觀的知道如何與使用者進行互動。

執行命令列程式應該遵循一般的格式:

#appname command  arguments
docker pull alpine:latest

#appname command flag
docker ps -a

#appname command flag argument
git commit -m "linzy"

3、安裝

使用 Cobra 很容易。首先,用於go get安裝最新版本的庫。

go get -u github.com/spf13/cobra@latest

4、編寫 server

在編寫 cmd 時需要先用 server 進行測試關聯,因此這一步我們先寫 server.go 用於測試

在 server 模組下 新建 server.go 檔案,寫入測試內容:

package server

import (
    "log"
)

var (
    ServerPort  string
    CertName    string
    CertPemPath string
    CertKeyPath string
)

func Serve() (err error) {
    log.Println(ServerPort)

    log.Println(CertName)

    log.Println(CertPemPath)

    log.Println(CertKeyPath)

    return nil
}

5、編寫 cmd

在cmd模組下 新建 root.go 檔案,寫入內容:

package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

// rootCmd表示在沒有任何子命令的情況下的基本命令
var rootCmd = &cobra.Command{
    // Command的用法,Use是一個行用法訊息
    Use: "grpc",
    // Short是help命令輸出中顯示的簡短描述
    Short: "Run the gRPC hello-world server",
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(-1)
    }
}

當前 cmd 目錄下繼續 新建 server.go 檔案,寫入內容:

package cmd

import (
    "github.com/spf13/cobra"
    "grpc-gateway-example/server"
    "log"
)

// 建立附加命令
// 本地標籤:在本地分配一個標誌,該標誌僅適用於該特定命令。
var serverCmd = &cobra.Command{
    Use:   "server",
    Short: "Run the gRPC hello-world server",
    // 執行:典型的實際工作功能。大多數命令只會實現這一點;
    // 另外還有PreRun、PreRunE、PostRun、PostRunE等等不同時期的執行命令,但比較少用,具體使用時再檢視亦可
    Run: func(cmd *cobra.Command, args []string) {
        defer func() {
            if err := recover(); err != nil {
                log.Println("Recover error : %v", err)
            }
        }()

        server.Serve()
    },
}

// 在 init() 函式中定義flags和處理配置。
func init() {
    // 我們定義了一個flag,值儲存在&server.ServerPort中,長命令為--port,短命令為-p,,預設值為50052。
    // 命令的描述為server port。這一種呼叫方式成為Local Flags 本地標籤
    serverCmd.Flags().StringVarP(&server.ServerPort, "port", "p", "50052", "server port")
    serverCmd.Flags().StringVarP(&server.CertPemPath, "cert-pem", "", "./certs/server.pem", "cert pem path")
    serverCmd.Flags().StringVarP(&server.CertKeyPath, "cert-key", "", "./certs/server.key", "cert key path")
    serverCmd.Flags().StringVarP(&server.CertName, "cert-name", "", "grpc-gateway-example", "server's hostname")

    // AddCommand向這父命令(rootCmd)新增一個或多個命令
    rootCmd.AddCommand(serverCmd)
}

6、啟動 & 請求

我們在 grpc-gateway-example 目錄下,新建檔案main.go,寫入內容:

package main

import "grpc-gateway-example/cmd"

func main() {
    cmd.Execute()
}

當前目錄下執行·go run main.go server·,檢視輸出是否為(此時應為預設值):

$ go run main.go server
2022/11/10 12:08:22 50052
2022/11/10 12:08:22 grpc-gateway-example                       
2022/11/10 12:08:22 ./certs/server.pem                         
2022/11/10 12:08:22 ./certs/server.key                         

執行go run main.go server --port=8000 --cert-pem=test-pem --cert-key=test-key --cert-name=test-name,檢驗命令列引數是否正確:

$ go run main.go server --port=8000 --cert-pem=test-pem --cert-key=test-key --cert-name=test-name
2022/11/10 12:24:53 8000
2022/11/10 12:24:54 test-name
2022/11/10 12:24:54 test-pem 
2022/11/10 12:24:54 test-key

到這都無誤,我們的 cmd 模組編寫就正確了,下面開始我們的重點

7、目錄結構

完成以上操作之後我們的目錄是這樣的結構,看看是不是缺少了:

grpc-gateway-example/
├── certs
├── client
├── cmd                        // 命令列模組
│   ├── root.go
│   └── server.go
├── pkg
├── proto
│   ├── google
│   │   └── api
│   │   │   ├── annotations.proto
│   │   │   └── http.proto
│   │   └── protobuf
│   │   │   └── descriptor.proto
│   ├── helloworld
│   │   ├── hello.pb.go        // proto編譯後檔案
│   │   ├── hello.pb.gw.go    // gateway編譯後檔案
│   │   └── hello_grpc.pb.go// proto編譯後介面檔案
│   ├── hello.proto
├── server                    // GRPC服務端
│   └── server.go
└── Makefile

1、編寫 hello.proto

在server目錄下新建檔案 hello.go ,寫入檔案內容:

package server

import (
    "context"
    "grpc-gateway-example/proto/helloworld"
)

type helloService struct {
    helloworld.UnimplementedHelloServer
}

func NewHelloService() *helloService {
    return &helloService{}
}

// ctx context.Context用於接受上下文引數
// r *pb.HelloWorldRequest用於接受protobuf的Request引數
func (h helloService) SayHello(ctx context.Context, r *helloworld.HelloRequest) (*helloworld.HelloResponse, error) {
    return &helloworld.HelloResponse{
        Message: "hello grpc-gateway",
    }, nil
}

2、編寫 grpc.go

在 pkg 下新建 util 目錄,新建 grpc.go 檔案,寫入內容:

package util

import (
    "google.golang.org/grpc"
    "net/http"
    "strings"
)

// 將gRPC請求和HTTP請求分別呼叫不同的handler處理。
func GrpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    if otherHandler == nil {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            grpcServer.ServeHTTP(w, r)
        })
    }
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
            grpcServer.ServeHTTP(w, r)
        } else {
            otherHandler.ServeHTTP(w, r)
        }
    })
}

GrpcHandlerFunc 函式是用於判斷請求是來源於 Rpc 客戶端還是 Restful Api 的請求,根據不同的請求註冊不同的 ServeHTTP 服務;r.ProtoMajor == 2 也代表著請求必須基於HTTP/2
簡而言之函式將gRPC請求和HTTP請求分別呼叫不同的handler處理。

如果不需要 TLS 建立安全連結,則可以使用h2c

func GrpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
            grpcServer.ServeHTTP(w, r)
        } else {
            otherHandler.ServeHTTP(w, r)
        }
    }), &http2.Server{})
}

3、編寫 tls.go

在pkg下的 util 目錄下,新建 tls.go 檔案,寫入內容:

package util

import (
    "crypto/tls"
    "golang.org/x/net/http2"
    "io/ioutil"
    "log"
)

// 用於處理從證照憑證檔案(PEM),最終獲取tls.Config作為HTTP2的使用引數
func GetTLSConfig(certPemPath, certKeyPath string) *tls.Config {
    var certKeyPair *tls.Certificate
    cert, _ := ioutil.ReadFile(certPemPath)
    key, _ := ioutil.ReadFile(certKeyPath)

    // 從一對PEM編碼的資料中解析公鑰/私鑰對。成功則返回公鑰/私鑰對
    pair, err := tls.X509KeyPair(cert, key)
    if err != nil {
        log.Println("TLS KeyPair err: %v\n", err)
    }

    certKeyPair = &pair

    return &tls.Config{
        // tls.Certificate:返回一個或多個證照,實質我們解析PEM呼叫的X509KeyPair的函式宣告
        // 就是func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error),返回值就是Certificate
        Certificates: []tls.Certificate{*certKeyPair},
        // http2.NextProtoTLS:NextProtoTLS是談判期間的NPN/ALPN協議,用於HTTP/2的TLS設定
        NextProtos: []string{http2.NextProtoTLS},
    }
}

GetTLSConfig 函式是用於獲取TLS配置,在內部,我們讀取了 server.key 和 server.pem 這類證照憑證檔案。經過一系列處理獲取 tls.Config 作為 HTTP2 的使用引數。

4、重新編寫核心檔案 server/server.go

修改server目錄下的server.go檔案,該檔案是我們服務裡的核心檔案,寫入內容:

package server

import (
    "context"
    "crypto/tls"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "grpc-gateway-example/pkg/util"
    "grpc-gateway-example/proto/helloworld"
    "log"
    "net"
    "net/http"
)

var (
    ServerPort  string
    CertName    string
    CertPemPath string
    CertKeyPath string
    EndPoint    string
)

func Serve() (err error) {
    EndPoint = ":" + ServerPort
    // 用於監聽本地的網路地址通知
    // 它的函式原型func Listen(network, address string) (Listener, error)
    conn, err := net.Listen("tcp", EndPoint)
    if err != nil {
        log.Printf("TCP Listen err:%v\n", err)
    }

    // 透過util.GetTLSConfig解析得到tls.Config,傳達給http.Server服務的TLSConfig配置項使用
    tlsConfig := util.GetTLSConfig(CertPemPath, CertKeyPath)
    srv := createInternalServer(conn, tlsConfig)

    log.Printf("gRPC and https listen on: %s\n", ServerPort)

    // NewListener將會建立一個Listener
    // 它接受兩個引數,第一個是來自內部Listener的監聽器,第二個引數是tls.Config(必須包含至少一個證照)
    if err = srv.Serve(tls.NewListener(conn, tlsConfig)); err != nil {
        log.Printf("ListenAndServe: %v\n", err)
    }

    return err
}

// 將認證的中介軟體註冊進去, 前面所獲取的tlsConfig僅能給HTTP使用
func createInternalServer(conn net.Listener, tlsConfig *tls.Config) *http.Server {
    var opts []grpc.ServerOption

    // 輸入證照檔案和伺服器的金鑰檔案構造TLS證照憑證
    creds, err := credentials.NewServerTLSFromFile(CertPemPath, CertKeyPath)
    if err != nil {
        log.Printf("Failed to create server TLS credentials %v", err)
    }

    // grpc.Creds()其原型為func Creds(c credentials.TransportCredentials) ServerOption
    // 該函式返回 ServerOption,它為伺服器連線設定憑據
    opts = append(opts, grpc.Creds(creds))

    // 建立了一個沒有註冊服務的grpc服務端
    grpcServer := grpc.NewServer(opts...)

    // 註冊grpc服務
    helloworld.RegisterHelloServer(grpcServer, NewHelloService())

    // 建立 grpc-gateway 關聯元件
    // context.Background()返回一個非空的空上下文。
    // 它沒有被登出,沒有值,沒有過期時間。它通常由主函式、初始化和測試使用,並作為傳入請求的頂級上下文
    ctx := context.Background()
    // 從客戶端的輸入證照檔案構造TLS憑證
    dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)
    if err != nil {
        log.Printf("Failed to create client TLS credentials %v", err)
    }
    // grpc.WithTransportCredentials 配置一個連線級別的安全憑據(例:TLS、SSL),返回值為type DialOption
    // grpc.DialOption DialOption選項配置我們如何設定連線(其內部具體由多個的DialOption組成,決定其設定連線的內容)
    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}

    // 建立HTTP NewServeMux及註冊grpc-gateway邏輯
    // runtime.NewServeMux:返回一個新的ServeMux,它的內部對映是空的;
    // ServeMux是grpc-gateway的一個請求多路複用器。它將http請求與模式匹配,並呼叫相應的處理程式
    gwmux := runtime.NewServeMux()

    // RegisterHelloWorldHandlerFromEndpoint:註冊HelloWorld服務的HTTP Handle到grpc端點
    if err := helloworld.RegisterHelloHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
        log.Printf("Failed to register gw server: %v\n", err)
    }

    // http服務
    // 分配並返回一個新的ServeMux
    mux := http.NewServeMux()
    // 為給定模式註冊處理程式
    mux.Handle("/", gwmux)

    return &http.Server{
        Addr:      EndPoint,
        Handler:   util.GrpcHandlerFunc(grpcServer, mux),
        TLSConfig: tlsConfig,
    }
}

5、server流程刨析

1)啟動監聽

net.Listen("tcp", EndPoint) 函式用於監聽本地網路地址的監聽。其函式原型Listen(ctx context.Context, network, address string) (Listener, error)

引數:

  • network:必須是tcp, tcp4, tcp6, unix或unixpacket。
  • address:對於TCP網路,如果address引數中的host為空或未指定的IP地址,則會自動返回一個可用的埠或者IP地址。

net.Listen(“tcp”, EndPoint)函式返回值是Listener

type Listener interface {
    // 接受等待並將下一個連線返回給Listener
    Accept() (Conn, error)

    // 關閉Listener
    Close() error

    // 返回 Listener 的網路地址。
    Addr() Addr
}

net.Listen 會返回一個監聽器的結構體,返回接下來的動作,讓其執行下一步的操作,可用執行以下操作Accept、Close、Addr。

2)獲取TLSConfig

透過呼叫 util.GetTLSConfig 函式解析得到 tls.Config,透過傳達給 createInternalServer函式完成 http.Server 服務的 TLSConfig 配置項使用。

3)建立內部服務

程式採用HTTP2、HTTPS,需要支援TLS,在啟動 grpc.NewServer() 前需要將serverOptions(伺服器選項,類似於中介軟體,可用設定例如憑證、編解碼器和保持存活引數等選項。),而前面所獲取的 tlsConfig 僅能給HTTP使用,因此第一步我們要建立 grpc 的 TLS 認證憑證。

  1. 建立 grpc 的 TLS 認證憑證
    引用 google.golang.org/grpc/credentials 第三方包,credentials 包實現gRPC庫支援的各種憑據,這些憑據封裝了客戶機與伺服器進行身份驗證所需的所有狀態,並進行各種斷言,例如,關於客戶機的身份、角色或是否授權進行特定呼叫。

我們呼叫 NewServerTLSFromFile 它能夠從伺服器的輸入證照檔案和金鑰檔案構造TLS憑據。

func NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {
    // LoadX509KeyPair從一對檔案中讀取並解析一個公私鑰對。檔案中必須包含PEM編碼的資料。
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        return nil, err
    }
    // NewTLS使用tls.Config來構建基於TLS的TransportCredentials(傳輸憑證)
    return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil
}
  1. grpc ServerOption

grpc.Creds() 其原型為func Creds(c credentials.TransportCredentials) ServerOption,返回一個為伺服器連線設定憑據的ServerOption。

  1. 建立 grpc 服務端
    grpc.NewServer() 建立一個沒有註冊服務的grpc服務端,可以配置 ServerOption
  2. 註冊grpc服務
// 註冊grpc服務
helloworld.RegisterHelloServer(grpcServer, NewHelloService())
  1. 建立 grpc-gateway 關聯元件
// context.Background()返回一個非空的空上下文。
// 它沒有被登出,沒有值,沒有過期時間。它通常由主函式、初始化和測試使用,並作為傳入請求的頂級上下文
ctx := context.Background()
// 從客戶端的輸入證照檔案構造TLS憑證
dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)
if err != nil {
    log.Printf("Failed to create client TLS credentials %v", err)
}
// grpc.WithTransportCredentials 配置一個連線級別的安全憑據(例:TLS、SSL),返回值為type DialOption
// grpc.DialOption DialOption選項配置我們如何設定連線(其內部具體由多個的DialOption組成,決定其設定連線的內容)
dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
  1. 建立HTTP NewServeMux及註冊 grpc-gateway 邏輯
// 建立HTTP NewServeMux及註冊grpc-gateway邏輯
// runtime.NewServeMux:返回一個新的ServeMux,它的內部對映是空的;
// ServeMux是grpc-gateway的一個請求多路複用器。它將http請求與模式匹配,並呼叫相應的處理程式
gwmux := runtime.NewServeMux()

// RegisterHelloWorldHandlerFromEndpoint:註冊HelloWorld服務的HTTP Handle到grpc端點
if err := helloworld.RegisterHelloHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
    log.Printf("Failed to register gw server: %v\n", err)
}

// http服務
// 分配並返回一個新的ServeMux
mux := http.NewServeMux()
// 為給定模式註冊處理程式
mux.Handle("/", gwmux)
  1. 註冊具體服務
// RegisterHelloWorldHandlerFromEndpoint:註冊HelloWorld服務的HTTP Handle到grpc端點
if err := helloworld.RegisterHelloHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {
    log.Printf("Failed to register gw server: %v\n", err)
}
  • ctx:上下文
  • gwmux:grpc-gateway 的請求多路複用器
  • EndPoint:服務網路地址
  • dopts:配置好的安全憑據

4)建立Listener

// NewListener將會建立一個Listener
// 它接受兩個引數,第一個是來自內部Listener的監聽器,第二個引數是tls.Config(必須包含至少一個證照)
if err = srv.Serve(tls.NewListener(conn, tlsConfig)); err != nil {
    log.Printf("ListenAndServe: %v\n", err)
}

5)服務接受請求

我們呼叫 srv.Serve(tls.NewListener(conn, tlsConfig))它是http.Server的方法,並且需要一個Listener作為引數。

func (srv *Server) Serve(l net.Listener) error {
    ...
    defer l.Close()
    ...
    baseCtx := context.Background()
    ...
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()
        ...
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks) // before Serve can return
        go c.serve(ctx)
    }
}

它建立了一個 context.Background() 上下文物件,並呼叫 Listener 的 Accept 方法開始接受請求,在獲取到連線資料後使用 newConn 建立連線物件,在最後使用goroutine的方式處理連線請求,完成請求後自動關閉連線。

1、編寫 client

在目錄client下,建立 main.go 檔案,新增以下內容:

package main

import (
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "grpc-gateway-example/proto/helloworld"
    "log"
)

func main() {
    creds, err := credentials.NewClientTLSFromFile("./certs/server.pem", "grpc-gateway-example")
    if err != nil {
        log.Println("Failed to create TLS credentials %v", err)
        return
    }
    conn, err := grpc.Dial(":50052", grpc.WithTransportCredentials(creds))
    defer conn.Close()

    if err != nil {
        log.Println(err)
    }

    c := helloworld.NewHelloClient(conn)
    ct := context.Background()
    body := &helloworld.HelloRequest{
        Referer: "Grpc",
    }

    r, err := c.SayHello(ct, body)
    if err != nil {
        log.Println(err)
    }

    log.Println(r)
}

2、啟動 & 請求

# 啟動服務端
$ go run main.go server
2022/11/10 16:34:06 gRPC and https listen on: 50052

# 啟動客戶端
$ go run client/main.go
2022/11/10 16:34:43 message:"hello grpc-gateway"

執行測試Restful Api,用POST方式訪問localhost:50052/hello_world
在這裡插入圖片描述

grpc-gateway-example/
├── certs                    //證照憑證
│   ├── ca.conf
│   ├── ca.crt
│   ├── ca.csr
│   ├── ca.key
│   ├── server.conf
│   ├── server.csr
│   ├── server.key
│   └── server.pem
├── client                    // 客戶端
│   └── main.go
├── cmd                        // 命令列模組
│   ├── root.go
│   └── server.go
├── pkg                        // 第三方公共模組
│   └── util
│   │   │   ├── grpc.go
│   │   │   └── tls.go
├── proto
│   ├── google
│   │   └── api
│   │   │   ├── annotations.proto
│   │   │   └── http.proto
│   │   └── protobuf
│   │   │   └── descriptor.proto
│   ├── helloworld
│   │   ├── hello.pb.go        // proto編譯後檔案
│   │   ├── hello.pb.gw.go    // gateway編譯後檔案
│   │   └── hello_grpc.pb.go// proto編譯後介面檔案
│   ├── hello.proto
├── server                    // GRPC服務端
│   └── server.go
├── main.go
└── Makefile

參考:
developer.aliyun.com/article/87948...
eddycjy.com/posts/go/grpc-gateway/...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章