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数据返回给客户端。
结构如图:
安装grpc-gateway 1 2 $ git clone https://github.com/google/go-genproto.git $GOPATH /src/google.golang.org/genproto $ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
目录结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |—— hello_http/ |—— client/ |—— main.go // 客户端 |—— server/ |—— main.go // GRPC服务端 |—— server_http/ |—— main.go // HTTP服务端 |—— proto/ |—— google // googleApi http-proto定义 |—— api |—— annotations.proto |—— http.proto |—— hello_http/ |—— hello_http.proto // proto描述文件 |—— hello_http.pb.go // proto编译后文件 |—— hello_http_pb.gw.go // gateway编译后文件
这里用到了google官方Api中的两个proto描述文件,直接拷贝不要做修改,里面定义了protocol buffer扩展的HTTP option,为grpc的http转换提供支持。
https://github.com/googleapis/googleapis //没有编译
github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis //没有编译
github.com/google/go-genproto //已经编译
示例代码 Step 1. 编写proto描述文件:proto/hello_http.proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 syntax = "proto3" ; package hello_http;option go_package = "hello_http" ;import "google/api/annotations.proto" ;service HelloHTTP { rpc SayHello(HelloHTTPRequest) returns (HelloHTTPResponse) { option (google.api.http) = { post: "/example/echo" body: "*" }; } } message HelloHTTPRequest { string name = 1 ; } message HelloHTTPResponse { string message = 1 ; }
这里在原来的SayHello
方法定义中增加了http option, POST方式,路由为”/example/echo”。
Step 2. 编译proto
1 2 3 4 5 6 $ cd proto# 编译hello_http.proto $ protoc -I . -IF:/goproject/src/protoc-3.7.1-win64/include --go_out=plugins=grpc:. ./hello_http/*.proto # 编译hello_http.proto gateway $ protoc -I . -IF:/goproject/src/protoc-3.7.1-win64/include --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/main.go
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 package mainimport ( "fmt" "net" pb "hellogrpc/proto/hello_http" "context" "google.golang.org/grpc" "log" ) const ( Address = "127.0.0.1:50052" ) type helloService struct {}var HelloService = helloService{}func (h helloService) SayHello(ctx context.Context, in *pb.HelloHTTPRequest) (*pb.HelloHTTPResponse, error ) { resp := new (pb.HelloHTTPResponse) resp.Message = fmt.Sprintf("Hello %s." , in.Name) return resp, nil } func main () { listen, err := net.Listen("tcp" , Address) if err != nil { log.Fatalf("Failed to listen: %v" , err) } s := grpc.NewServer() pb.RegisterHelloHTTPServer(s, HelloService) log.Println("Listen on " + Address) s.Serve(listen) }
client/main.go
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 package mainimport ( pb "hellogrpc/proto/hello_http" "golang.org/x/net/context" "google.golang.org/grpc" "log" ) const ( Address = "127.0.0.1:50052" ) func main () { conn, err := grpc.Dial(Address, grpc.WithInsecure()) if err != nil { log.Fatalln(err) } defer conn.Close() c := pb.NewHelloHTTPClient(conn) req := &pb.HelloHTTPRequest{Name: "gRPC" } res, err := c.SayHello(context.Background(), req) if err != nil { log.Fatalln(err) } log.Println(res.Message) }
server_http/main.go
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 package mainimport ( "net/http" "github.com/grpc-ecosystem/grpc-gateway/runtime" "context" "google.golang.org/grpc" "log" gw "hellogrpc/proto/hello_http" ) func main () { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() endpoint := "127.0.0.1:50052" mux := runtime.NewServeMux() opts := []grpc.DialOption{grpc.WithInsecure()} err := gw.RegisterHelloHTTPHandlerFromEndpoint(ctx, mux, endpoint, opts) if err != nil { log.Fatalf("Register handler err:%v\n" , err) } log.Println("HTTP Listen on 8080" ) http.ListenAndServe(":8080" , mux) }
就是这么简单。开启了一个http server,收到请求后根据路由转发请求到对应的RPC接口获得结果。grpc-gateway做的事情就是帮我们自动生成了转换过程的实现。
运行结果 依次开启gRPC服务和HTTP服务端:
1 2 $ cd hello_http/server && go run main.goListen on 127.0.0.1:50052
1 2 $ cd hello_http/server_http && go run main.goHTTP Listen on 8080
调用grpc客户端:
1 2 3 4 $ cd hello_http/client && go run main.goHello gRPC. # HTTP 请求 $ curl -X POST -k http://localhost:8080/example/echo -d '{"name": "gRPC-HTTP is working!"}' {"message" :"Hello gRPC-HTTP is working!." }
升级版服务端 上面的使用方式已经实现了我们最初的需求,grpc-gateway 项目中提供的示例也是这种使用方式,这样后台需要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供http和gRPC调用方式。
新建一个项目hello_http_2
, 基于hello_tls 项目改造。客户端只要修改调用的proto包地址就可以了,这里我们看服务端的实现:
hello_http_2/server/main.go
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 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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 package mainimport ( "crypto/tls" "io/ioutil" "net" "net/http" "strings" "github.com/grpc-ecosystem/grpc-gateway/runtime" pb "hellogrpc/proto/hello_http" "context" "golang.org/x/net/http2" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "log" ) type helloHTTPService struct {}var HelloHTTPService = helloHTTPService{}func (h helloHTTPService) SayHello(ctx context.Context, in *pb.HelloHTTPRequest) (*pb.HelloHTTPResponse, error ) { resp := new (pb.HelloHTTPResponse) resp.Message = "Hello " + in.Name + "." return resp, nil } func main () { endpoint := "127.0.0.1:50052" conn, err := net.Listen("tcp" , endpoint) if err != nil { log.Fatalf("TCP Listen err:%v\n" , err) } creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem" , "../../keys/server.key" ) if err != nil { log.Fatalf("Failed to create server TLS credentials %v" , err) } grpcServer := grpc.NewServer(grpc.Creds(creds)) pb.RegisterHelloHTTPServer(grpcServer, HelloHTTPService) ctx := context.Background() dcreds, err := credentials.NewClientTLSFromFile("../../keys/server.pem" , "yyy" ) if err != nil { log.Fatalf("Failed to create client TLS credentials %v" , err) } dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)} gwmux := runtime.NewServeMux() if err = pb.RegisterHelloHTTPHandlerFromEndpoint(ctx, gwmux, endpoint, dopts); err != nil { log.Fatalf("Failed to register gw server: %v\n" , err) } mux := http.NewServeMux() mux.Handle("/" , gwmux) srv := &http.Server{ Addr: endpoint, Handler: grpcHandlerFunc(grpcServer, mux), TLSConfig: getTLSConfig(), } log.Printf("gRPC and https listen on: %s\n" , endpoint) if err = srv.Serve(tls.NewListener(conn, srv.TLSConfig)); err != nil { log.Fatal("ListenAndServe: " , err) } return } func getTLSConfig () *tls.Config { cert, _ := ioutil.ReadFile("../../keys/server.pem" ) key, _ := ioutil.ReadFile("../../keys/server.key" ) var demoKeyPair *tls.Certificate pair, err := tls.X509KeyPair(cert, key) if err != nil { log.Fatalf("TLS KeyPair err: %v\n" , err) } demoKeyPair = &pair return &tls.Config{ Certificates: []tls.Certificate{*demoKeyPair}, NextProtos: []string {http2.NextProtoTLS}, } } func grpcHandlerFunc (grpcServer *grpc.Server, otherHandler http.Handler) http.Handler { if otherHandler == nil { return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { grpcServer.ServeHTTP(w, r) }) } return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type" ), "application/grpc" ) { grpcServer.ServeHTTP(w, r) } else { otherHandler.ServeHTTP(w, r) } }) }
hello_http_2/client/main.go
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 package mainimport ( pb "hellogrpc/proto/hello_http" "context" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "log" ) const ( Address = "127.0.0.1:50052" ) func main () { creds, err := credentials.NewClientTLSFromFile("../../keys/server.pem" , "yyy" ) if err != nil { log.Fatalf("Failed to create TLS credentials %v" , err) } conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds)) if err != nil { log.Fatalln(err) } defer conn.Close() c := pb.NewHelloHTTPClient(conn) req := &pb.HelloHTTPRequest{Name: "gRPC" } res, err := c.SayHello(context.Background(), req) if err != nil { log.Fatalln(err) } log.Println(res.Message) }
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 2 $ cd hello_http_2/server && go run main.gogRPC and https listen on: 127.0.0.1:50052
1 2 3 4 $ cd hello_http_2/client && go run main.goHello gRPC. # HTTP 请求 $ curl -X POST -k https://localhost:50052/example/echo -d '{"name": "gRPC-HTTP is working!"}' {"message" :"Hello gRPC-HTTP is working!." }