iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
0
DevOps

欸你這週GO了嘛系列 第 21

[DAY21]小朋友才要選擇~gRPC與http我全都要之gRPC Gateway

  • 分享至 

  • xImage
  •  

有沒有種moment是原本都用gRPC用的好好的,但是臨時要給其他服務打,又只能用http,
只能捏著____再開一個port來做http服務嗎
小朋友才要選擇呢~我全都要
https://ithelp.ithome.com.tw/upload/images/20200928/20129515onX8NDgQbM.jpg

gRPC Gateway

gRPC Gateway是套威力加強版的gRPC套件,
可以把gRPC轉換成http服務,提供給外部服務使用
下圖為官方的架構圖
https://ithelp.ithome.com.tw/upload/images/20200928/20129515baZPlfAMqE.png
簡單來說,原本protoc定義的gRPC服務可以透過gRPC Gateway啟動的http服務,讓外部以RESTful API溝通,
內部再轉回gRPC,全部都吃protoc的定義,所以使用者可以不用再寫二套服務的hanlder

安裝環境

因為要編輯gRPC Gateway加料過的protco檔,所以步驟會麻煩一些..
第一個步驟會花蠻多時間的

mkdir tmp
cd tmp
git clone https://github.com/google/protobuf
cd protobuf
./autogen.sh
./configure
make
make check
sudo make install
```:
步驟2:安裝grpc-ecosystem的套件,第三個如果有安裝過就可以PASS
```bash
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

定義proto檔

原料原味的proto檔

syntax = "proto3";

package mdfk2020;

service SaySomethingService{  
  rpc MDFK2020 (MDFKRequest) returns (MDFKResponse) {}
}

message MDFKRequest {
  string name = 1;  
}

message MDFKResponse {
  string data = 1;
}

如果要用gRPC gateway的話,proto檔要加一些語法進去

syntax = "proto3";
//額外import套件
import "google/api/annotations.proto";
package mdfk2020;

service SaySomethingService{  
   rpc GetMDFK2020 (MDFKRequest) returns (MDFKResponse) {
     //重點就是這個定義,可以定義get,post,put,delete等method,主要是給外部呼叫使用
    option (google.api.http) = {
      get: "/v1/mdfk2020"     
    };
  }
  rpc PostMDFK2020 (MDFKRequest) returns (MDFKResponse) {
    option (google.api.http) = {
      post: "/v1/mdfk2020"  
      body: "*"
    };
  }
}

message MDFKRequest {
  string name = 1;  
}

message MDFKResponse {
  string data = 1;
}

產生pb檔

gRPC Gateway的pb檔跟原本生的cli語法不同,要改用以下語法進行產生pb檔,可以把二段語法整合成shell檔,後續使用會方便很多

//產生proto檔
$protoc -I/usr/local/include -I. -I$GOPATH/src -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis 
mdfk2020.proto --go_out=plugins=grpc:.
//產生gw檔
$protoc -I/usr/local/include -I. -I$GOPATH/src -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis mdfk2020.proto --grpc-gateway_out=logtostderr=true:.

實作gRPC server

package main

import (
	"context"
	"net"
	"net/http"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	gw "internal/grpcdemo/pb"
)


type saySomethingServiceServer struct{}

func newSaySomethingServiceServer() gw.SaySomethingServiceServer {
	return &saySomethingServiceServer{}
}

func httpServer() error {
	ctxr := context.Background()
	ctx, cancel := context.WithCancel(ctxr)
	defer cancel()
	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}
    //與原生gRPC不同點在這邊,需要做http與grpc的對應
	err := gw.RegisterSaySomethingServiceHandlerFromEndpoint(ctx, mux, ":8787", opts)
	if err != nil {
		return err
	}
	return http.ListenAndServe(":7878", mux)
}
func grpcServer() {

	apiListener, err := net.Listen("tcp", ":8787")
	if err != nil {
		panic(err)
	}

	grpc := grpc.NewServer()

	gw.RegisterSaySomethingServiceServer(grpc, newSaySomethingServiceServer())

	reflection.Register(grpc)

	if err = grpc.Serve(apiListener); err != nil {
		panic(err)
	}
}

func main() {
    //啟動http與grpc server
	go httpServer()
	grpcServer()
}

func (s saySomethingServiceServer) GetMDFK2020(ctx context.Context, req *gw.MDFKRequest) (*gw.MDFKResponse, error) {
	return &gw.MDFKResponse{Data: "TEST"}, nil
}
func (s saySomethingServiceServer) PostMDFK2020(ctx context.Context, req *gw.MDFKRequest) (*gw.MDFKResponse, error) {
	return &gw.MDFKResponse{Data: "TEST"}, nil
}

使用RESTful API呼叫

$curl --location --request GET '127.0.0.1:7878/v1/Getmdfk2020'
//{"data":"TEST"}

如果需要header怎麼辦

要如何取得RESTful API打過來request的header呢
其實不難,只要多了幾個步驟
1.http request的header要使用前綴詞「Grpc-Metadata-」 ,ex:Grpc-Metadata-token
2.import google.golang.org/grpc/metadata
3.從context裡面取得資料
以下為實作


import google.golang.org/grpc/metadata

func (s saySomethingServiceServer) GetMDFK2020(ctx context.Context, req *gw.MDFKRequest) (*gw.MDFKResponse, error) {
    //取得context裡面的資料
	hd, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("get token err")
	}
    //http request header為Grpc-Metadata-token,在metadata中為token
	token, exist := hd["token"]
	if !exist {
		return nil, errors.New("miss token")
	}
	return &gw.MDFKResponse{Data: token[0]}, nil
}

使用middleware

原生gRPC也是有Middleware可以用,但是它叫「interceptor」,

//單向
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error
//串流
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

原生只能使用一組interceptor,
如果需要多組interceptor的話,可以使用grpc-ecosystem的go-grpc-middleware,可以使到鏈式interceptors,以達成多組interceptor的需求

import "github.com/grpc-ecosystem/go-grpc-middleware"

myServer := grpc.NewServer(
    grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
        grpc_ctxtags.StreamServerInterceptor(),
        grpc_opentracing.StreamServerInterceptor(),
        grpc_prometheus.StreamServerInterceptor,
        grpc_zap.StreamServerInterceptor(zapLogger),
        grpc_auth.StreamServerInterceptor(myAuthFunction),
        grpc_recovery.StreamServerInterceptor(),
    )),
    grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
        grpc_ctxtags.UnaryServerInterceptor(),
        grpc_opentracing.UnaryServerInterceptor(),
        grpc_prometheus.UnaryServerInterceptor,
        grpc_zap.UnaryServerInterceptor(zapLogger),
        grpc_auth.UnaryServerInterceptor(myAuthFunction),
        grpc_recovery.UnaryServerInterceptor(),
    )),
)

一般來說grpc_recovery/grpc_auth與logging會是最常使用到的middleware

以使用grpc_recovery為例

//定義一個RecoveryInterceptor
func RecoveryInterceptor() grpc_recovery.Option {
	return grpc_recovery.WithRecoveryHandler(func(p interface{}) (err error) {		
		return err
	})
}
//使用grpc_middleware
grpc := grpc.NewServer(
		grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
			grpc_recovery.UnaryServerInterceptor(),
		)),
	)
//讓func會panic掉,如果沒加recovery的話,遇到panic會服務死掉/pod重啟
func (s saySomethingServiceServer) GetMDFK2020(ctx context.Context, req *gw.MDFKRequest) (*gw.MDFKResponse, error) {
	hd, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, errors.New("get token err")
	}

	token, exist := hd["token"]
	if !exist {
		return nil, errors.New("miss token")
	}
	panic("GG")
	return &gw.MDFKResponse{Data: token[0]}, nil
}

執行結果

{
    "error": "GG",
    "code": 13,
    "message": "GG"
}

總結

gRPC Gateway一個不錯的解決方案,有沒有缺點嘛,當然還是有,因為多轉了一層,效能多少會有損耗,
是否能接受就看使用情境囉,如果http服務request跟回應速度有要求,還是直接啟動http服務會好一些


上一篇
[DAY20]GO gRPC初體驗
下一篇
[DAY22]Golang玩MySQL
系列文
欸你這週GO了嘛30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言