咱們都知道 gRPC
並非萬能的工具。 在某些狀況下,咱們仍然想提供傳統的 HTTP/JSON API
。緣由可能從保持向後兼容性到支持編程語言或 gRPC
沒法很好地支持的客戶端。可是僅僅爲了公開 HTTP/JSON API
而編寫另外一個服務是一項很是耗時且乏味的任務。git
那麼,有什麼方法能夠只編寫一次代碼,卻能夠同時在 gRPC
和 HTTP/JSON
中提供 API
?github
答案是 Yes
。golang
gRPC-Gateway
是 Google protocol buffers compiler protoc
的插件。 它讀取 protobuf service
定義並生成反向代理服務器( reverse-proxy server
) ,該服務器將 RESTful HTTP API
轉換爲 gRPC
。 該服務器是根據服務定義中的 google.api.http
批註(annotations
)生成的。編程
這有助於你同時提供 gRPC
和 HTTP/JSON
格式的 API
。json
在開始編碼以前,咱們必須安裝一些工具。api
在示例中,咱們將使用 Go gRPC Server
,所以請首先從 https://golang.org/dl/
安裝 Go
。服務器
安裝 Go
以後,請使用 go get
下載如下軟件包:微信
$ go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway $ go get google.golang.org/protobuf/cmd/protoc-gen-go $ go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
這將安裝咱們生成存根所需的協議生成器插件。確保將 $GOPATH/bin
添加到 $PATH
中,以便經過 go get
安裝的可執行文件在 $PATH
中可用。curl
咱們將在本教程的新模塊中進行工做,所以,請當即在您選擇的文件夾中建立該模塊:tcp
建立 go.mod 文件
使用 go mod init
命令啓動你的 module
以建立 go.mod
文件。
運行 go mod init
命令,給它代碼所在 module
的路徑。在這裏,使用 github.com/myuser/myrepo
做爲 module
路徑—在生產代碼中,這將是能夠從其中下載 module
的 URL
。
$ go mod init github.com/myuser/myrepo go: creating new go.mod: module github.com/myuser/myrepo
go mod init
命令建立一個 go.mod
文件,該文件將您的代碼標識爲能夠從其餘代碼中使用的 module
。 您剛建立的文件僅包含模塊名稱和代碼支持的 Go
版本。 可是,當您添加依賴項(即其餘模塊的軟件包)時,go.mod
文件將列出要使用的特定 module
版本。 這樣能夠使構建具備可複製性,並使您能夠直接控制要使用的 module
版本。
爲了瞭解 gRPC-Gateway
,咱們首先要製做一個 hello world gRPC
服務。
在建立 gRPC
服務以前,咱們應該建立一個 proto
文件來定義咱們須要的東西,這裏咱們在 proto/helloworld/
目錄下建立了一個名爲 hello_world.proto
的文件。
gRPC service
使用 Google Protocol Buffers 定義的。這裏定義以下:
syntax = "proto3"; package helloworld; // The greeting 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; }
Buf
是一個工具,它提供了各類 protobuf
實用程序,如 linting
, breaking change detection
和 generation
。請在 https://docs.buf.build/installation/
上找到安裝說明。
它是經過 buf.yaml
文件配置的,應將其檢入你存儲庫的根目錄中。 若是存在,Buf
將自動讀取此文件。 也能夠經過命令行標誌 --config
提供配置,該標誌接受 .json
或 .yaml
文件的路徑,或是直接 JSON
或 YAML
數據。
全部使用本地 .proto
文件做爲輸入的 Buf
操做都依賴於有效的構建配置。這個配置告訴 Buf
在哪裏搜索 .proto
文件,以及如何處理導入。與 protoc
(全部 .proto
文件都是在命令行上手動指定的)不一樣,buf
的操做方式是遞歸地發現配置下的全部 .proto
文件並構建它們。
下面是一個有效配置的示例,假設您的 .proto
文件根位於相對於存儲庫根的 proto
文件夾中。
version: v1beta1 name: buf.build/myuser/myrepo build: roots: - proto
要爲 Go
生成 type
和 gRPC stubs
,請在存儲庫的根目錄下建立文件 buf.gen.yaml
:
version: v1beta1 plugins: - name: go out: proto opt: paths=source_relative - name: go-grpc out: proto opt: paths=source_relative
咱們使用 go
和 go-grpc
插件生成 Go types
和 gRPC service
定義。咱們正在輸出相對於 proto
文件夾的生成文件,並使用 path=source_relative
選項,這意味着生成的文件將與源 .proto
文件顯示在同一目錄中。
而後運行:
$ buf generate
這將爲咱們的 proto
文件層次結構中的每一個 protobuf
軟件包生成一個 *.pb.go
和 *_grpc.pb.go
文件。
這是一個 protoc
命令可能會生成 Go stubs
的示例,假設您位於存儲庫的根目錄,而且您的 proto
文件位於一個名爲 proto
的目錄中:
$ protoc -I ./proto \ --go_out ./proto --go_opt paths=source_relative \ --go-grpc_out ./proto --go-grpc_opt paths=source_relative \ ./proto/helloworld/hello_world.proto
咱們使用 go
和 go-grpc
插件生成 Go types
和 gRPC service
定義。咱們正在輸出相對於 proto
文件夾的生成文件,並使用 path=source_relative
選項,這意味着生成的文件將與源 .proto
文件顯示在同一目錄中。
這將爲 proto/helloworld/hello_world.proto
生成一個 *.pb.go
和 *_grpc.pb.go
文件。
在建立 main.go
文件以前,咱們假設用戶已經建立了一個名爲 github.com/myuser/myrepo
的 go.mod
。此處的 import
使用的是相對於存儲庫根目錄的 proto/helloworld
中生成的文件的路徑。
package main import ( "context" "log" "net" "google.golang.org/grpc" helloworldpb "github.com/myuser/myrepo/proto/helloworld" ) type server struct{} func NewServer() *server { return &server{} } func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) { return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil } func main() { // Create a listener on TCP port lis, err := net.Listen("tcp", ":8080") if err != nil { log.Fatalln("Failed to listen:", err) } // Create a gRPC server object s := grpc.NewServer() // Attach the Greeter service to the server helloworldpb.RegisterGreeterServer(s, &server{}) // Serve gRPC Server log.Println("Serving gRPC on 0.0.0.0:8080") log.Fatal(s.Serve(lis)) }
如今,咱們已經能夠使用 Go gRPC
服務器,咱們須要添加 gRPC-Gateway
批註。
批註定義了 gRPC
服務如何映射到 JSON
請求和響應。 使用 protocol buffers
時,每一個 RPC
必須使用 google.api.http
批註定義 HTTP
方法和路徑。
所以,咱們須要將 google/api/http.proto
導入添加到 proto
文件中。咱們還須要添加所需的 HTTP->gRPC
映射。在這種狀況下,咱們會將 POST /v1/example/echo
映射到咱們的 SayHello RPC
。
syntax = "proto3"; package helloworld; import "google/api/annotations.proto"; // Here is the overall greeting service definition where we define all our endpoints service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) { option (google.api.http) = { post: "/v1/example/echo" body: "*" }; } } // The request message containing the user's name message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
如今咱們已經將 gRPC-Gateway
批註添加到了 proto
文件中,咱們須要使用 gRPC-Gateway
生成器來生成存根(stubs
)。
使用 buf
咱們須要將 gRPC-Gateway
生成器添加到生成配置中:
version: v1beta1 plugins: - name: go out: proto opt: paths=source_relative - name: go-grpc out: proto opt: paths=source_relative,require_unimplemented_servers=false - name: grpc-gateway out: proto opt: paths=source_relative
咱們還須要將 googleapis
依賴項添加到咱們的 buf.yaml
文件中:
version: v1beta1 name: buf.build/myuser/myrepo deps: - buf.build/beta/googleapis build: roots: - proto
而後,咱們須要運行 buf beta mod update
以選擇要使用的依賴項版本。
就是這樣!如今,若是您運行:
$ buf generate
它應該產生一個 *.gw.pb.go
文件。
使用 protoc
在使用 protoc
生成 stubs
以前,咱們須要將一些依賴項複製到咱們的 proto
文件結構中。將一部分 googleapis
從官方存儲庫複製到您本地的原始文件結構中。以後看起來應該像這樣:
proto ├── google │ └── api │ ├── annotations.proto │ └── http.proto └── helloworld └── hello_world.proto
如今咱們須要將 gRPC-Gateway
生成器添加到 protoc
調用中:
$ protoc -I ./proto \ --go_out ./proto --go_opt paths=source_relative \ --go-grpc_out ./proto --go-grpc_opt paths=source_relative \ --grpc-gateway_out ./proto --grpc-gateway_opt paths=source_relative \ ./proto/helloworld/hello_world.proto
這將生成一個 *.gw.pb.go
文件。
咱們還須要在 main.go
文件中添加 gRPC-Gateway
多路複用器(mux
)併爲其提供服務。
package main import ( "context" "log" "net" "net/http" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" helloworldpb "github.com/myuser/myrepo/proto/helloworld" ) type server struct{ helloworldpb.UnimplementedGreeterServer } func NewServer() *server { return &server{} } func (s *server) SayHello(ctx context.Context, in *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) { return &helloworldpb.HelloReply{Message: in.Name + " world"}, nil } func main() { // Create a listener on TCP port lis, err := net.Listen("tcp", ":8080") if err != nil { log.Fatalln("Failed to listen:", err) } // Create a gRPC server object s := grpc.NewServer() // Attach the Greeter service to the server helloworldpb.RegisterGreeterServer(s, &server{}) // Serve gRPC server log.Println("Serving gRPC on 0.0.0.0:8080") go func() { log.Fatalln(s.Serve(lis)) }() // Create a client connection to the gRPC server we just started // This is where the gRPC-Gateway proxies the requests conn, err := grpc.DialContext( context.Background(), "0.0.0.0:8080", grpc.WithBlock(), grpc.WithInsecure(), ) if err != nil { log.Fatalln("Failed to dial server:", err) } gwmux := runtime.NewServeMux() // Register Greeter err = helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn) if err != nil { log.Fatalln("Failed to register gateway:", err) } gwServer := &http.Server{ Addr: ":8090", Handler: gwmux, } log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090") log.Fatalln(gwServer.ListenAndServe()) }
如今咱們能夠啓動服務器了:
$ go run main.go
而後,咱們使用 cURL
發送 HTTP
請求:
$ curl -X POST -k http://localhost:8090/v1/example/echo -d '{"name": " hello"}'
{"message":"hello world"}
我是爲少 微信:uuhells123 公衆號:黑客下午茶 加我微信(互相學習交流),關注公衆號(獲取更多學習資料~)