個人網站: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
檔案可以直接複製程式碼!!
- 如果你生成Go程式碼的時候出現
- 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
- 生成ca私鑰,得到ca.key
openssl genrsa -out ca.key 4096
- 生成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]:
- 生成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
- 生成私鑰,得到server.key
openssl genrsa -out server.key 2048
- 生成證照籤發請求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf
這裡也一直回車就好。
- 用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 appname
和cobra add cmdname
- 提供智慧提示
- 自動生成commands和flags的幫助資訊
- 自動生成詳細的
help
資訊,如 app -help。 - 自動識別幫助
flag、 -h,--help
。 - 自動生成應用程式在 bash 下命令自動完成功能。
- 自動生成應用程式的 man 手冊。
- 命令列別名。
- 自定義
help
和usage
資訊。 - 可選的與
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 認證憑證。
- 建立 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
}
- grpc ServerOption
grpc.Creds() 其原型為func Creds(c credentials.TransportCredentials) ServerOption
,返回一個為伺服器連線設定憑據的ServerOption。
- 建立 grpc 服務端
grpc.NewServer() 建立一個沒有註冊服務的grpc服務端,可以配置ServerOption
- 註冊grpc服務
// 註冊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
邏輯
// 建立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)
- 註冊具體服務
// 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 協議》,轉載必須註明作者和本文連結