1. HTTP网关

源自coreos的一篇博客 Take a REST with HTTP/2, Protobufs, and Swagger

etcd3 API全面升级为gRPC后,同时要提供REST API服务,维护两个版本的服务显然不太合理,所以grpc-gateway诞生了。通过protobuf的自定义option实现了一个网关,服务端同时开启gRPC和HTTP服务,HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。

结构如图:

HTTP网关 - 图1

1.1. 安装grpc-gateway

  1. $ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

1.2. 目录结构

  1. |—— hello_http/
  2. |—— client/
  3. |—— main.go // 客户端
  4. |—— server/
  5. |—— main.go // GRPC服务端
  6. |—— server_http/
  7. |—— main.go // HTTP服务端
  8. |—— proto/
  9. |—— google // googleApi http-proto定义
  10. |—— api
  11. |—— annotations.proto
  12. |—— annotations.pb.go
  13. |—— http.proto
  14. |—— http.pb.go
  15. |—— hello_http/
  16. |—— hello_http.proto // proto描述文件
  17. |—— hello_http.pb.go // proto编译后文件
  18. |—— hello_http_pb.gw.go // gateway编译后文件

这里用到了google官方Api中的两个proto描述文件,直接拷贝不要做修改,里面定义了protocol buffer扩展的HTTP option,为grpc的http转换提供支持。

1.3. 示例代码

Step 1. 编写proto描述文件:proto/hello_http.proto

  1. syntax = "proto3";
  2. package hello_http;
  3. option go_package = "hello_http";
  4. import "google/api/annotations.proto";
  5. // 定义Hello服务
  6. service HelloHTTP {
  7. // 定义SayHello方法
  8. rpc SayHello(HelloHTTPRequest) returns (HelloHTTPResponse) {
  9. // http option
  10. option (google.api.http) = {
  11. post: "/example/echo"
  12. body: "*"
  13. };
  14. }
  15. }
  16. // HelloRequest 请求结构
  17. message HelloHTTPRequest {
  18. string name = 1;
  19. }
  20. // HelloResponse 响应结构
  21. message HelloHTTPResponse {
  22. string message = 1;
  23. }

这里在原来的SayHello方法定义中增加了http option, POST方式,路由为"/example/echo"。

Step 2. 编译proto

  1. $ cd proto
  2. # 编译google.api
  3. $ protoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto
  4. # 编译hello_http.proto
  5. $ protoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=github.com/jergoo/go-grpc-example/proto/google/api:. hello_http/*.proto
  6. # 编译hello_http.proto gateway
  7. $ protoc --grpc-gateway_out=logtostderr=true:. hello_http/hello_http.proto

注意这里需要编译google/api中的两个proto文件,同时在编译hello_http.proto时使用M参数指定引入包名,最后使用grpc-gateway编译生成hello_http_pb.gw.go文件,这个文件就是用来做协议转换的,查看文件可以看到里面生成的http handler,处理proto文件中定义的路由"example/echo"接收POST参数,调用HelloHTTP服务的客户端请求grpc服务并响应结果。

Step 3: 实现服务端和客户端

server/main.go和client/main.go的实现与hello项目一致,这里不再说明。

server_http/main.go

  1. package main
  2. import (
  3. "net/http"
  4. "github.com/grpc-ecosystem/grpc-gateway/runtime"
  5. "golang.org/x/net/context"
  6. "google.golang.org/grpc"
  7. "google.golang.org/grpc/grpclog"
  8. gw "github.com/jergoo/go-grpc-example/proto/hello_http"
  9. )
  10. func main() {
  11. ctx := context.Background()
  12. ctx, cancel := context.WithCancel(ctx)
  13. defer cancel()
  14. // grpc服务地址
  15. endpoint := "127.0.0.1:50052"
  16. mux := runtime.NewServeMux()
  17. opts := []grpc.DialOption{grpc.WithInsecure()}
  18. // HTTP转grpc
  19. err := gw.RegisterHelloHTTPHandlerFromEndpoint(ctx, mux, endpoint, opts)
  20. if err != nil {
  21. grpclog.Fatalf("Register handler err:%v\n", err)
  22. }
  23. grpclog.Println("HTTP Listen on 8080")
  24. http.ListenAndServe(":8080", mux)
  25. }

就是这么简单。开启了一个http server,收到请求后根据路由转发请求到对应的RPC接口获得结果。grpc-gateway做的事情就是帮我们自动生成了转换过程的实现。

1.4. 运行结果

依次开启gRPC服务和HTTP服务端:

  1. $ cd hello_http/server && go run main.go
  2. Listen on 127.0.0.1:50052
  1. $ cd hello_http/server_http && go run main.go
  2. HTTP Listen on 8080

调用grpc客户端:

  1. $ cd hello_http/client && go run main.go
  2. Hello gRPC.
  3. # HTTP 请求
  4. $ curl -X POST -k http://localhost:8080/example/echo -d '{"name": "gRPC-HTTP is working!"}'
  5. {"message":"Hello gRPC-HTTP is working!."}

1.5. 升级版服务端

上面的使用方式已经实现了我们最初的需求,grpc-gateway项目中提供的示例也是这种使用方式,这样后台需要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供http和gRPC调用方式。

新建一个项目hello_http_2, 基于hello_tls项目改造。客户端只要修改调用的proto包地址就可以了,这里我们看服务端的实现:

hello_http_2/server/main.go

  1. package main
  2. import (
  3. "crypto/tls"
  4. "io/ioutil"
  5. "net"
  6. "net/http"
  7. "strings"
  8. "github.com/grpc-ecosystem/grpc-gateway/runtime"
  9. pb "github.com/jergoo/go-grpc-example/proto/hello_http"
  10. "golang.org/x/net/context"
  11. "golang.org/x/net/http2"
  12. "google.golang.org/grpc"
  13. "google.golang.org/grpc/credentials"
  14. "google.golang.org/grpc/grpclog"
  15. )
  16. // 定义helloHTTPService并实现约定的接口
  17. type helloHTTPService struct{}
  18. // HelloHTTPService Hello HTTP服务
  19. var HelloHTTPService = helloHTTPService{}
  20. // SayHello 实现Hello服务接口
  21. func (h helloHTTPService) SayHello(ctx context.Context, in *pb.HelloHTTPRequest) (*pb.HelloHTTPResponse, error) {
  22. resp := new(pb.HelloHTTPResponse)
  23. resp.Message = "Hello " + in.Name + "."
  24. return resp, nil
  25. }
  26. func main() {
  27. endpoint := "127.0.0.1:50052"
  28. conn, err := net.Listen("tcp", endpoint)
  29. if err != nil {
  30. grpclog.Fatalf("TCP Listen err:%v\n", err)
  31. }
  32. // grpc tls server
  33. creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem", "../../keys/server.key")
  34. if err != nil {
  35. grpclog.Fatalf("Failed to create server TLS credentials %v", err)
  36. }
  37. grpcServer := grpc.NewServer(grpc.Creds(creds))
  38. pb.RegisterHelloHTTPServer(grpcServer, HelloHTTPService)
  39. // gw server
  40. ctx := context.Background()
  41. dcreds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
  42. if err != nil {
  43. grpclog.Fatalf("Failed to create client TLS credentials %v", err)
  44. }
  45. dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
  46. gwmux := runtime.NewServeMux()
  47. if err = pb.RegisterHelloHTTPHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
  48. grpclog.Fatalf("Failed to register gw server: %v\n", err)
  49. }
  50. // http服务
  51. mux := http.NewServeMux()
  52. mux.Handle("/", gwmux)
  53. srv := &http.Server{
  54. Addr: endpoint,
  55. Handler: grpcHandlerFunc(grpcServer, mux),
  56. TLSConfig: getTLSConfig(),
  57. }
  58. grpclog.Infof("gRPC and https listen on: %s\n", endpoint)
  59. if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
  60. grpclog.Fatal("ListenAndServe: ", err)
  61. }
  62. return
  63. }
  64. func getTLSConfig() *tls.Config {
  65. cert, _ := ioutil.ReadFile("../../keys/server.pem")
  66. key, _ := ioutil.ReadFile("../../keys/server.key")
  67. var demoKeyPair *tls.Certificate
  68. pair, err := tls.X509KeyPair(cert, key)
  69. if err != nil {
  70. grpclog.Fatalf("TLS KeyPair err: %v\n", err)
  71. }
  72. demoKeyPair = &pair
  73. return &tls.Config{
  74. Certificates: []tls.Certificate{*demoKeyPair},
  75. NextProtos: []string{http2.NextProtoTLS}, // HTTP2 TLS支持
  76. }
  77. }
  78. // grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC
  79. // connections or otherHandler otherwise. Copied from cockroachdb.
  80. func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
  81. if otherHandler == nil {
  82. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  83. grpcServer.ServeHTTP(w, r)
  84. })
  85. }
  86. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  87. if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
  88. grpcServer.ServeHTTP(w, r)
  89. } else {
  90. otherHandler.ServeHTTP(w, r)
  91. }
  92. })
  93. }

gRPC服务端接口的实现没有区别,重点在于HTTP服务的实现。gRPC是基于http2实现的,net/http包也实现了http2,所以我们可以开启一个HTTP服务同时服务两个版本的协议,在注册http handler的时候,在方法grpcHandlerFunc中检测请求头信息,决定是直接调用gRPC服务,还是使用gateway的HTTP服务。net/http中对http2的支持要求开启https,所以这里要求使用https服务。

步骤

  • 注册开启TLS的grpc服务
  • 注册开启TLS的gateway服务,地址指向grpc服务
  • 开启HTTP server

1.5.1. 运行结果

  1. $ cd hello_http_2/server && go run main.go
  2. gRPC and https listen on: 127.0.0.1:50052
  1. $ cd hello_http_2/client && go run main.go
  2. Hello gRPC.
  3. # HTTP 请求
  4. $ curl -X POST -k https://localhost:50052/example/echo -d '{"name": "gRPC-HTTP is working!"}'
  5. {"message":"Hello gRPC-HTTP is working!."}