go語言gRPC系列(三) - 使用grpc-gateway同時提供HTTP和gRPC服務

寶樹吶發表於2020-08-15

1. gRPC提供HTTP服務

1.1 存在的意義

在某些場景下單純的RPC服務不能滿足提供的服務需求的話,還是需要提供HTTP服務作為補充,gRPC一樣可以提供HTTP服務。

  • 注意:gRPC提供的HTTP介面是基於HTTP 2.0

1.2 程式碼示例

package main

import (
	"fmt"
	"gomicro-quickstart/grpc_server/service"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"log"
	"net/http"
)

func main() {
	// 1. 引用證書
	tls, err := credentials.NewServerTLSFromFile("grpc_server/keys/server.crt", "grpc_server/keys/server_no_password.key")
	if err != nil {
		log.Fatal("服務端獲取證書失敗: ", err)
	}

	// 2. new一個grpc的server,並且加入證書
	rpcServer := grpc.NewServer(grpc.Creds(tls))

	// 3. 將剛剛我們新建的ProdService註冊進去
	service.RegisterProdServiceServer(rpcServer, new(service.ProdService))

    // 4. 新建一個路由,並傳入rpcServer
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		fmt.Println(request)
		rpcServer.ServeHTTP(writer, request)
	})
	
	// 5. 定義httpServer,監聽8082
	httpServer := http.Server{
		Addr:    ":8082",
		Handler: mux,
	}

    // 6. 以https形式監聽httpServer
	httpServer.ListenAndServeTLS("grpc_server/keys/server.crt", "grpc_server/keys/server_no_password.key")
}

1.3 使用postman嘗試呼叫

執行上述的程式碼,然後postman訪問8082埠,提示訪問這個介面需要http/2協議

1.4 gRPC客戶端程式碼呼叫

針對上一節的客戶端呼叫的程式碼,我們不需要修改即可以直接訪問

即直接呼叫protoc產生的go檔案中的方法

我們服務端程式碼因為列印出了,http request的內容

所以我們檢視一下通過客戶端呼叫,會列印出什麼,可以看到

  • 請求的路徑是/service.ProdService/GetProductStock,是{服務名}/{方法名}的格式
  • 協議是:http/2

2. 使用grpc-gateway同時提供HTTP和gRPC服務

2.1 前言

某些場景下需要同時要提供REST API服務gRPC服務,維護兩個版本的服務顯然不太合理,所以grpc-gateway誕生了。

原理:通過protobuf的自定義option實現了一個閘道器,服務端同時開啟gRPCHTTP 1.1服務,HTTP服務接收客戶端請求後轉換為grpc請求資料,獲取響應後轉為json資料返回給客戶端。

按照官方的結構說明如圖

2.2 安裝

執行安裝以下三個

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

2.3 目錄結構

這裡用到了google官方Api中的兩個proto描述檔案,直接拷貝不要做修改,裡面定義了protocol buffer擴充套件的HTTP option,為grpc的http轉換提供支援。

|—— hello_http/
    |—— client/
        |—— main.go   // 客戶端
    |—— server/
        |—— main.go   // GRPC服務端
    |—— server_http/
        |—— main.go   // HTTP服務端
|—— proto/
    |—— google       // googleApi http-proto定義
        |—— api
            |—— annotations.proto
            |—— annotations.pb.go
            |—— http.proto
            |—— http.pb.go
    |—— hello_http/
        |—— hello_http.proto   // proto描述檔案
        |—— hello_http.pb.go   // proto編譯後檔案
        |—— hello_http_pb.gw.go // gateway編譯後檔案

2.4 示例程式碼

2.4.1 編寫proto描述檔案:proto/hello_http.proto

SayHello方法定義中增加了http option, POST方式,路由為/example/echo

syntax = "proto3";

package hello_http;
option go_package = "hello_http";

import "google/api/annotations.proto";

// 定義Hello服務
service HelloHTTP {
    // 定義SayHello方法
    rpc SayHello(HelloHTTPRequest) returns (HelloHTTPResponse) {
        // http option
        option (google.api.http) = {
            post: "/example/echo"
            body: "*"
        };
    }
}

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

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

2.4.2 編譯proto

$ cd proto

# 編譯google.api
$ protoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto

# 編譯hello_http.proto
$ protoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=github.com/jergoo/go-grpc-example/proto/google/api:. hello_http/*.proto

# 編譯hello_http.proto gateway
$ protoc --grpc-gateway_out=logtostderr=true:. hello_http/hello_http.proto

2.4.3 實現HTTP服務端

package main

import (
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/grpclog"

    gw "github.com/jergoo/go-grpc-example/proto/hello_http"
)

func main() {
    // 1. 定義一個context
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // grpc服務地址
    endpoint := "127.0.0.1:50052"
    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}

    // HTTP轉grpc
    err := gw.RegisterHelloHTTPHandlerFromEndpoint(ctx, mux, endpoint, opts)
    if err != nil {
        grpclog.Fatalf("Register handler err:%v\n", err)
    }

    grpclog.Println("HTTP Listen on 8080")
    http.ListenAndServe(":8080", mux)
}

2.4.4 實現gRPC服務端

package main

import (
    "fmt"
    "net"
    "net/http"

    pb "github.com/jergoo/go-grpc-example/proto/hello" // 引入編譯生成的包

    "golang.org/x/net/context"
    "golang.org/x/net/trace"
    "google.golang.org/grpc"
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC服務地址
    Address = "127.0.0.1:50052"
)

// 定義helloService並實現約定的介面
type helloService struct{}

// HelloService Hello服務
var HelloService = helloService{}

// SayHello 實現Hello服務介面
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
    resp := new(pb.HelloResponse)
    resp.Message = fmt.Sprintf("Hello %s.", in.Name)

    return resp, nil
}

func main() {
    listen, err := net.Listen("tcp", Address)
    if err != nil {
        grpclog.Fatalf("failed to listen: %v", err)
    }

    // 例項化grpc Server
    s := grpc.NewServer()

    // 註冊HelloService
    pb.RegisterHelloServer(s, HelloService)

    grpclog.Println("Listen on " + Address)
    s.Serve(listen)
}

2.4.5 實現客戶端

package main

import (
    pb "github.com/jergoo/go-grpc-example/proto/hello" // 引入proto包
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/grpclog"
)

const (
    // Address gRPC服務地址
    Address = "127.0.0.1:50052"
)

func main() {
    // 連線
    conn, err := grpc.Dial(Address, grpc.WithInsecure())
    if err != nil {
        grpclog.Fatalln(err)
    }
    defer conn.Close()

    // 初始化客戶端
    c := pb.NewHelloClient(conn)

    // 呼叫方法
    req := &pb.HelloRequest{Name: "gRPC"}
    res, err := c.SayHello(context.Background(), req)

    if err != nil {
        grpclog.Fatalln(err)
    }

    grpclog.Println(res.Message)
}

2.5 執行並呼叫

依次開啟gRPC服務端和HTTP服務端

$ cd hello_http/server && go run main.go
Listen on 127.0.0.1:50052

$ cd hello_http/server_http && go run main.go
HTTP Listen on 8080

然後呼叫gRPC的客戶端

$ cd hello_http/client && go run main.go
Hello gRPC.

# HTTP 請求
$ curl -X POST -k http://localhost:8080/example/echo -d '{"name": "gRPC-HTTP is working!"}'
{"message":"Hello gRPC-HTTP is working!."}

相關文章