Golang gRPC筆記04 同時提供 gRPC 服務和 HTTP 介面
一、 gRPC 和 HTTP
我們通常把 RPC 用作內部通訊,而使用 Restful Api 進行外部通訊。在某些時候,我們需要同時提供 RPC 服務和 HTTP 介面,
這種情況下為了避免寫兩套應用,可以使用 grpc-gateway 把gRPC轉成HTTP。服務接收到HTTP請求後,grpc-gateway 把它轉成gRPC進行處理,然後以JSON形式返回資料。
為什麼可以同時提供 HTTP 介面?
關鍵一點,gRPC 的協議是基於 HTTP/2 的,因此應用程式能夠在單個 TCP 埠上提供 HTTP/1.1 和 gRPC 介面服務(兩種不同的流量)
怎麼同時提供 HTTP 介面?
- 檢測請求協議是否為 HTTP/2
- 判斷 Content-Type 是否為 application/grpc(gRPC 的預設標識位)
- 根據協議的不同轉發到不同的服務處理
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { server.ServeHTTP(w, r) } else { mux.ServeHTTP(w, r) }
安裝 grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
二、 完成專案編碼
專案目錄:
grpc_demo/ |—— demo04/ |—— client/ |—— client.go // 客戶端 |—— conf/ // 證書檔案,與上一節相同 |—— pkg/ |—— util/ |—— tls_support.go |—— proto/ |—— google/ |—— api/ |—— annotations.proto |—— annotations.pb.go |—— http.proto |—— http.pb.go |—— prod/ |—— prod.proto |—— prod.pb.go |—— prod.pb.gw.go |—— server/ |—— server.go // 服務端
.proto
檔案:
- google.api
proto目錄中有google/api目錄,它用到了google官方提供的兩個api描述檔案,直接複製自 $GOPATH/grpc-gateway/third_party/googleapis/google/api,以便於import引用
這些檔案主要是針對grpc-gateway的http轉換提供支援,定義了Protocol Buffer所擴充套件的HTTP Option
同時,為了直接將編譯得到的.go
檔案生成在 proto/google/api 檔案下,修改 option go_package = "google/api;annotations"
annotations.proto檔案:
// Copyright (c) 2015, Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
import "google/api/http.proto";
import "google/protobuf/descriptor.proto";
//option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option go_package = "google/api;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
extend google.protobuf.MethodOptions {
// See `HttpRule`.
HttpRule http = 72295728;
}
http.proto檔案:
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
option cc_enable_arenas = true;
//option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option go_package = "google/api;annotations";
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
...
...
...
- 重新編寫
prod.proto
檔案:
syntax = "proto3";
package prodpb;
option go_package="./prod;prodpb";
import "google/api/annotations.proto";
message ProdRequest {
int64 prod_id = 1;
}
message ProdResponse {
int64 prod_stock = 1;
}
service ProdService {
// 定義一個方法
rpc GetProdStock (ProdRequest) returns (ProdResponse) {
// http option
option (google.api.http) = {
get: "/v1/prod/{prod_id}"
};
}
}
在檔案中,引用了google/api/annotations.proto,達到支援HTTP Option的效果
在 service ProdService 服務內部定義了一個HTTP Option的GET方法,HTTP響應路徑為 /v1/prod/{prod_id},其中的 {prod_id} 匹配 message ProdRequest 中的 prod_id
- 編譯:
在編寫 .proto
檔案中,import 路徑和 go_package 路徑,我全部以 proto 目錄為相對路徑,因此,編譯 .proto
檔案需切換到 proto 目錄進行
cd grpc_demo/demo04/proto
# 編譯google.api
protoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto
# 編譯 prod.proto為 prod.pb.go
protoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=grpc_demo/demo04/proto/google/api:. ./prod/prod.proto
# 編譯 prod.proto 為 prod.pb.gw.go,以完成對grpc-gateway的功能支援
protoc --grpc-gateway_out=logtostderr=true:. ./prod/prod.proto
util: tls_support.go
在 tls_support.go檔案中,新增 GetCATLSConfig 方法
// 封裝基於 CA 的 TLS 認證服務端配置項
func GetCATLSConfig(serverCertFile, serverKeyFile, caFile string) (*tls.Config, error) {
// 從證書相關檔案中讀取和解析資訊,得到證書公鑰、金鑰對
cert, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile)
if err != nil {
return nil, fmt.Errorf("tls.LoadX509KeyPair err: %v", err)
}
// 建立一個新的、空的 CertPool,並嘗試解析 PEM 編碼的證書,解析成功會將其加到 CertPool 中
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(caFile)
if err != nil {
return nil, fmt.Errorf("ioutil.ReadFile err: %v", err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
return nil, fmt.Errorf("certPool.AppendCertsFromPEM err")
}
return &tls.Config{
// 設定證書鏈,允許包含一個或多個
Certificates: []tls.Certificate{cert},
// 要求客戶端提供證書,但是如果客戶端沒有提供證書,服務端還是會繼續處理請求
ClientAuth: tls.RequestClientCert,
// 設定根證書的集合,校驗方式使用 ClientAuth 中設定的模式
ClientCAs: certPool,
// NextProtoTLS是談判期間的NPN/ALPN協議,用於HTTP/2的TLS設定,如需支援HTTP/2需配置此項
NextProtos: []string{http2.NextProtoTLS},
}, nil
}
GetCATLSConfig函式用於獲取TLS配置,處理HTTP服務的TLS認證相關問題
NextProtos: []string{http2.NextProtoTLS}
: 該配置用於支援 HTTP/2ClientAuth: tls.RequestClientCert
: 不檢測 HTTP 請求的客戶端證書,gRPC請求正常檢測,此處檢測證書會提示無效證書,暫不知如何處理,因此先關閉 HTTP 請求證書檢測
server端:server.go
package main
import (
"crypto/tls"
"grpc_demo/demo04/pkg/util"
prodpb "grpc_demo/demo04/proto/prod"
"log"
"net"
"net/http"
"strings"
"golang.org/x/net/context"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
)
const (
endpoint = "127.0.0.1:8899"
serverCertFile = "demo03/conf/keys/server/server.pem"
serverKeyFile = "demo03/conf/keys/server/server.key"
clientCertFile = "demo03/conf/keys/client/client.pem"
clientKeyFile = "demo03/conf/keys/client/client.key"
caFile = "demo03/conf/keys/ca.pem"
certServerName = "localhost"
)
func main() {
// 開啟 HTTP 服務
tlsConfig, err := util.GetCATLSConfig(serverCertFile, serverKeyFile, caFile)
if err != nil {
log.Fatalf("載入服務端 TLS 憑證失敗,err=%v", err)
}
srv := &http.Server{
Addr: endpoint,
Handler: grpcHandlerFunc(newGRPCServer(), newHTTPHandler()),
TLSConfig: tlsConfig,
}
conn, err := net.Listen("tcp", srv.Addr)
if err != nil {
log.Fatal("監聽連線失敗:", err)
}
log.Printf("grpc and https on port: %s", srv.Addr)
log.Fatal(srv.Serve(tls.NewListener(conn, srv.TLSConfig)))
}
// grpcHandlerFunc 檢查請求協議並返回對應的 http handler
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
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)
}
})
}
// newGRPCServer 例項化標準grpc server
func newGRPCServer() *grpc.Server {
serverOpt, err := util.GetCATLSServerOption(serverCertFile, serverKeyFile, caFile)
if err != nil {
log.Fatalf("載入服務端 TLS 憑證失敗,err=%v", err)
}
rpcServer := grpc.NewServer(serverOpt)
prodpb.RegisterProdServiceServer(rpcServer, CreateProdService())
return rpcServer
}
// newHTTPHandler 初始化 http-grpc gateway
func newHTTPHandler() http.Handler {
dialOpt, err := util.GetCATLSDialOption(clientCertFile, clientKeyFile, caFile, certServerName)
if err != nil {
log.Fatalf("載入客戶端 TLS 憑證失敗,err=%v", err)
}
ctx := context.Background()
// 新建gwmux,它是grpc-gateway的請求複用器。它將http請求與模式匹配,並呼叫相應的處理程式
gwMux := runtime.NewServeMux()
// 新增 dialOption
dOpts := []grpc.DialOption{dialOpt}
// 將服務的http處理程式註冊到gwmux。處理程式通過endpoint轉發請求到grpc端點
err = prodpb.RegisterProdServiceHandlerFromEndpoint(ctx, gwMux, endpoint, dOpts)
if err != nil {
log.Fatalf("Failed to register gw server: %v", err)
}
return gwMux
}
newGRPCServer:
該方法 例項化標準grpc server,用來處理 gRPC 請求
newHTTPHandler:
runtime.NewServeMux 建立一個新的 ServerMux,它的內部對映是空的;ServeMux是grpc-gateway的一個請求多路複用器。它將http請求與模式匹配,並呼叫相應的處理程式
函式 RegisterProdServiceHandlerFromEndpoint 註冊ProdService服務的HTTP Handle到 serverMux,
serverMux 實現了 ServeHTTP 方法,因此可以作為 http.Handler 傳給 grpcHandlerFunc 函式
grpcHandlerFunc:
grpcHandlerFunc函式是用於判斷請求是來源於Rpc客戶端還是Restful Api的請求,根據不同的請求註冊不同的ServeHTTP服務;r.ProtoMajor == 2也代表著請求必須基於HTTP/2
client端:client.go
package main
import (
"context"
"grpc_demo/demo04/pkg/util"
prodpb "grpc_demo/demo04/proto/prod"
"log"
"google.golang.org/grpc"
)
const Address = "127.0.0.1:8899"
func main() {
dialOpt, err := util.GetCATLSDialOption("demo03/conf/keys/client/client.pem", "demo03/conf/keys/client/client.key", "demo03/conf/keys/ca.pem", "localhost")
if err != nil {
log.Fatalf("載入客戶端 TLS 憑證失敗,err=%v", err)
}
// 連線 rpc 伺服器
conn, err := grpc.Dial(Address, dialOpt)
if err != nil {
panic("grpc.Dial err: " + err.Error())
}
defer conn.Close()
// 初始化客戶端
client := prodpb.NewProdServiceClient(conn)
resp, err := client.GetProdStock(context.Background(), &prodpb.ProdRequest{ProdId: 4444})
if err != nil {
log.Print("呼叫失敗,err=", err)
return
}
log.Printf("%+v \n", resp)
}
驗證:
啟動 Server:
cd demo04/server
go run server.go
grpc and https on port: 127.0.0.1:8899
啟動 Client:
cd demo04/client
go run client.go
prod_stock:4008
測試Restful Api
curl -k https://localhost:8899/v1/prod/123
{"prod_stock":"4008"}
測試成功