有沒有種moment是原本都用gRPC用的好好的,但是臨時要給其他服務打,又只能用http,
只能捏著____再開一個port來做http服務嗎
小朋友才要選擇呢~我全都要
gRPC Gateway是套威力加強版的gRPC套件,
可以把gRPC轉換成http服務,提供給外部服務使用
下圖為官方的架構圖
簡單來說,原本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檔
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;
}
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:.
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
}
$curl --location --request GET '127.0.0.1:7878/v1/Getmdfk2020'
//{"data":"TEST"}
要如何取得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
}
原生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服務會好一些