【grpc】11.grpc-gw将gRPC 转 RESTful api
# 应用场景
gRPC is great – it generates API clients and server stubs in many programming languages, it is fast, easy-to-use, bandwidth-efficient and its design is combat-proven by Google. However, you might still want to provide a traditional RESTful API as well. Reasons can range from maintaining backwards-compatibility, supporting languages or clients not well supported by gRPC to simply maintaining the aesthetics and tooling involved with a RESTful architecture.
gRPC非常棒--它可以用许多编程语言生成API客户端和服务器存根,它速度快、易于使用、带宽效率高,而且它的设计经过了Google的实战验证。然而,你可能仍然想提供一个传统的RESTful API。原因可能包括保持向后兼容,支持gRPC不支持的语言或客户端,以及简单地保持RESTful架构所涉及的美学和工具。
# 版本信息
- go:1.18.0
- grpc:1.54.0
- protoc:3.19.4
- protoc-gen-go: 1.28.0
- protoc-gen-grpc-gateway:
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2
# 定义api
https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L46
项目目录
grpc_2_http └── proto └── helloworld └── hello_world.proto
1
2
3
4googleapi仓库
/Users/liusaisai/third_party |__google/ └── googleapis/ // git clone https://github.com/googleapis/googleapis.git
1
2
3
4
5hello.proto
syntax = "proto3"; package proto; option go_package = ".;proto"; import "google/api/annotations.proto"; // 必须引入 service Messaging { rpc GetMessage(GetMessageRequest) returns (Message) { option (google.api.http) = { get: "/v1/{name=messages/*}" }; } } message GetMessageRequest { string name = 1; // Mapped to URL path. } message Message { string text = 1; // The resource content. }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 生成stub
生成grpc和gw stub 命令
protoc -I=proto -I=/Users/liusaisai/third_party \ --go_out=proto --go_opt=paths=source_relative \ --go-grpc_out=proto --go-grpc_opt=paths=source_relative \ --grpc-gateway_out=proto --grpc-gateway_opt=paths=source_relative \ helloworld/hello_world.proto
1
2
3
4
5
# 服务启动
# 1. http和grpc服务
package main
import (
"context"
"log"
"net"
"net/http"
helloworldpb "bigox-rpc/proto/helloworld"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" // 注意v2版本
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type Server struct {
helloworldpb.UnimplementedMessagingServer
}
func NewServer() *Server {
return &Server{}
}
func (s *Server) GetMessage(ctx context.Context, in *helloworldpb.GetMessageRequest) (*helloworldpb.Message, error) {
return &helloworldpb.Message{Text: in.Name + " world"}, nil
}
func main() {
// Create a listener on TCP port
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalln("Failed to listen:", err)
}
// 创建gRPC server
s := grpc.NewServer()
// 注册Message service到server
helloworldpb.RegisterMessagingServer(s, &Server{})
// 8080端口启动gRPC Server
log.Println("Serving gRPC on 0.0.0.0:8080")
go func() {
log.Fatalln(s.Serve(lis))
}()
// 创建一个连接到我们刚刚启动的 gRPC 服务器的客户端连接
// gRPC-Gateway 就是通过它来代理请求(将HTTP请求转为RPC请求)
conn, err := grpc.DialContext(
context.Background(),
"0.0.0.0:8080",
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalln("Failed to dial server:", err)
}
gwMux := runtime.NewServeMux()
// 注册Greeter
err = helloworldpb.RegisterMessagingHandler(context.Background(), gwMux, conn)
if err != nil {
log.Fatalln("Failed to register gateway:", err)
}
gwServer := &http.Server{
Addr: ":8090",
Handler: gwMux,
}
// 8090端口提供gRPC-Gateway服务
log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
log.Fatalln(gwServer.ListenAndServe())
}
// curl http://localhost:8090/v1/messages/133
// res {"text":"messages/133 world"}%
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 2. http和grpc共用一个端口
package main
import (
hellopb "bigox-rpc/proto/helloworld"
"context"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
"log"
"net/http"
"strings"
)
const addr = "0.0.0.0:8080"
type HelloServer struct {
hellopb.UnimplementedMessagingServer
}
func (h *HelloServer) GetMessage(ctx context.Context, req *hellopb.GetMessageRequest) (*hellopb.Message, error) {
return &hellopb.Message{Text: req.Name + "++++++"}, nil
}
func main() {
// http server
httpMux := http.NewServeMux()
// grpc server
grpcSvc := grpc.NewServer()
hellopb.RegisterMessagingServer(grpcSvc, &HelloServer{})
reflection.Register(grpcSvc)
// gateway
gwMux := runtime.NewServeMux()
options := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
err := hellopb.RegisterMessagingHandlerFromEndpoint(context.Background(), gwMux, addr, options)
if err != nil {
log.Fatal(err)
}
httpMux.Handle("/", gwMux)
err = http.ListenAndServe(addr, grpcHandlerFunc(grpcSvc, httpMux))
if err != nil {
log.Fatal(err)
}
}
func grpcHandlerFunc(grpcServer *grpc.Server, httpHandler http.Handler) http.Handler {
return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
grpcServer.ServeHTTP(w, r)
} else {
httpHandler.ServeHTTP(w, r)
}
}), &http2.Server{})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 生成swagger文档
cmd
# swagger doc protoc -I=./proto -I=/Users/liusaisai/third_party \ --swagger_out ./proto/ \ --swagger_out=logtostderr=true:. \ helloworld/hello_world.proto
1
2
3
4
5
# 常见问题
# 1. 自定义返回body
grpc-gw 出错时默认返回格式
{ "code":5, "message":"Not Found", "details":[] }
1
2
3
4
5
# 1.1 方案一
将返回的内容定义为每条消息中回复的一部分
message HelloWorldWrapper { int code = 1; HelloWorldResponse data = 2; string error = 3; } message HelloWorldResponse { string key = 1; }
1
2
3
4
5
6
7
8
9缺点是大量的proto api需要冗余定义
# 1.2 方案二
核心函数
runtime.WithForwardResponseOption
只会在正常响应的时候调用runtime.WithErrorHandler
该函数只要请求就会调用,且在正常请求runtime.WithForwardResponseOption
之后调用
代码示例
- custom res
type StandardResp struct { Code int `json:"code"` Data interface{} `json:"data"` Error string `json:"error"` } const ( proxyFlag = "__success__" ) func HttpSuccessHandler(ctx context.Context, w http.ResponseWriter, p proto.Message) error { fmt.Print("111111111111111") resp := StandardResp{ Code: 0, Data: p, Error: "", } bs, _ := json.Marshal(&resp) return errors.New(proxyFlag + string(bs)) } func HttpErrorHandler(ctx context.Context, mux *runtime.ServeMux, m runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) { fmt.Print("00000000000000") w.Header().Set("Content-Type", "application/json") // success proxy raw := err.Error() if strings.HasPrefix(raw, proxyFlag) { raw = raw[len(proxyFlag):] w.Write([]byte(raw)) return } // normal error s, ok := status.FromError(err) if !ok { s = status.New(codes.Unknown, err.Error()) } resp := StandardResp{ Code: 1, Data: nil, Error: s.Message(), } bs, _ := json.Marshal(&resp) w.Write(bs) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48mux
// gateway gwMux := runtime.NewServeMux( runtime.WithForwardResponseOption(HttpSuccessHandler), runtime.WithErrorHandler(HttpErrorHandler), )
1
2
3
4
5
# 2. 跨域问题解决
func grpcHandlerFunc(grpcServer *grpc.Server, httpHandler http.Handler) http.Handler {
return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") {
grpcServer.ServeHTTP(w, r)
} else {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Request-Method", "GET, POST, PUT, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
httpHandler.ServeHTTP(w, r)
}
}), &http2.Server{})
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3. 空字段不返回问题
问题原因:protobuf 生成的 pb.go 中 struct 字段都是用
json:",omitempty"
修饰,这会导致在 gateway 转发返回时 json marshal 空的字段(初始值,0,空 slice 等)不返回。解决办法:是使用 jsonpb (opens new window) marshal,jsonpb 提供了
EmitDefaults
选项来控制是否解析omitempty
字段。https://stackoverflow.com/questions/34716238/golang-protobuf-remove-omitempty-tag-from-generated-json-tagsgrpc-gateway
runtime v1
gwMux := runtime.NewServeMux( runtime.WithMarshalerOption( runtime.MIMEWildcard, &runtime.JSONPb{OrigName: true, EmitDefaults: true}, ) )
1
2
3
4
5
6runtime v2
import ( "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/protobuf/encoding/protojson" ) gwMux := runtime.NewServeMux( runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{ MarshalOptions: protojson.MarshalOptions{ EmitUnpopulated: true, }, }), )
1
2
3
4
5
6
7
8
9
10
11
12
非grpc gateway
"github.com/golang/protobuf/jsonpb"
func sendProtoMessage(resp proto.Message, w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json; charset=utf-8") m := protojson.Marshaler{EmitDefaults: true} m.Marshal(w, resp) // You should check for errors here }
1
2
3
4
5google.golang.org/protobuf/jsonpb
func sendProtoMessage(resp proto.Message, w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json; charset=utf-8") m := protojson.MarshalOptions{EmitUnpopulated: true} b, err := m.Marshal(resp) if err != nil { // Handle error appropriately } w.Write(b) }
1
2
3
4
5
6
7
8
9