1. 程式人生 > 實用技巧 >Golang gRPC筆記04 同時提供 gRPC 服務和 HTTP 介面

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 介面?

  1. 檢測請求協議是否為 HTTP/2
  2. 判斷 Content-Type 是否為 application/grpc(gRPC 的預設標識位)
  3. 根據協議的不同轉發到不同的服務處理
    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檔案:

  1. 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";
...
...
...
  1. 重新編寫 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

  1. 編譯:

在編寫 .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/2
  • ClientAuth: 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"}

測試成功