1. HTTP网关

    源自coreos的一篇博客 。

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

    结构如图:

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

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

    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. }

    Step 2. 编译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项目一致,这里不再说明。

    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. opts := []grpc.DialOption{grpc.WithInsecure()}
    17. // HTTP转grpc
    18. err := gw.RegisterHelloHTTPHandlerFromEndpoint(ctx, mux, endpoint, opts)
    19. if err != nil {
    20. grpclog.Fatalf("Register handler err:%v\n", err)
    21. }
    22. grpclog.Println("HTTP Listen on 8080")
    23. http.ListenAndServe(":8080", mux)
    24. }

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

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

    调用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!."}

    上面的使用方式已经实现了我们最初的需求,项目中提供的示例也是这种使用方式,这样后台需要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供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. ctx := context.Background()
    40. dcreds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
    41. if err != nil {
    42. grpclog.Fatalf("Failed to create client TLS credentials %v", err)
    43. }
    44. dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}
    45. gwmux := runtime.NewServeMux()
    46. if err = pb.RegisterHelloHTTPHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil {
    47. grpclog.Fatalf("Failed to register gw server: %v\n", err)
    48. }
    49. // http服务
    50. mux := http.NewServeMux()
    51. mux.Handle("/", gwmux)
    52. srv := &http.Server{
    53. Addr: endpoint,
    54. Handler: grpcHandlerFunc(grpcServer, mux),
    55. TLSConfig: getTLSConfig(),
    56. }
    57. grpclog.Infof("gRPC and https listen on: %s\n", endpoint)
    58. if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil {
    59. grpclog.Fatal("ListenAndServe: ", err)
    60. }
    61. return
    62. }
    63. func getTLSConfig() *tls.Config {
    64. cert, _ := ioutil.ReadFile("../../keys/server.pem")
    65. key, _ := ioutil.ReadFile("../../keys/server.key")
    66. var demoKeyPair *tls.Certificate
    67. pair, err := tls.X509KeyPair(cert, key)
    68. if err != nil {
    69. grpclog.Fatalf("TLS KeyPair err: %v\n", err)
    70. }
    71. demoKeyPair = &pair
    72. return &tls.Config{
    73. Certificates: []tls.Certificate{*demoKeyPair},
    74. NextProtos: []string{http2.NextProtoTLS}, // HTTP2 TLS支持
    75. }
    76. }
    77. // grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC
    78. // connections or otherHandler otherwise. Copied from cockroachdb.
    79. func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
    80. if otherHandler == nil {
    81. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    82. grpcServer.ServeHTTP(w, r)
    83. })
    84. }
    85. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    86. if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
    87. grpcServer.ServeHTTP(w, r)
    88. } else {
    89. otherHandler.ServeHTTP(w, r)
    90. }
    91. })
    92. }

    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. $ cd hello_http_2/client && go run main.go
    2. Hello gRPC.
    3. # HTTP 请求