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数据返回给客户端。

结构如图:

img

安装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";
// 定义Hello服务
service HelloHTTP {
// 定义SayHello方法
rpc SayHello(HelloHTTPRequest) returns (HelloHTTPResponse) {
// http option
option (google.api.http) = {
post: "/example/echo"
body: "*"
};
}
}
// HelloRequest 请求结构
message HelloHTTPRequest {
string name = 1;
}
// HelloResponse 响应结构
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 main
import (
"fmt"
"net"
pb "hellogrpc/proto/hello_http" // 引入编译生成的包
"context"
"google.golang.org/grpc"
"log"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
// 定义helloService并实现约定的接口
type helloService struct{}
// HelloService Hello服务
var HelloService = helloService{}
// SayHello 实现Hello服务接口
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)
}
// 实例化grpc Server
s := grpc.NewServer()
// 注册HelloService
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 main
import (
pb "hellogrpc/proto/hello_http" // 引入编译生成的包
"golang.org/x/net/context"
"google.golang.org/grpc"
"log"
)
const (
// Address gRPC服务地址
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 main

import (
"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()
// grpc服务地址
endpoint := "127.0.0.1:50052"
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
// HTTP转grpc
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.go
Listen on 127.0.0.1:50052
1
2
$ cd hello_http/server_http && go run main.go
HTTP Listen on 8080

调用grpc客户端:

1
2
3
4
$ cd hello_http/client && go run main.go
Hello 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 main

import (
"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"
)
// 定义helloHTTPService并实现约定的接口
type helloHTTPService struct{}
// HelloHTTPService Hello HTTP服务
var HelloHTTPService = helloHTTPService{}
// SayHello 实现Hello服务接口
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)
}
// grpc tls server
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)
// gw server
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)
}
// http服务
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}, // HTTP2 TLS支持
}
}
// grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC
// connections or otherHandler otherwise. Copied from cockroachdb.
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 main

import (
pb "hellogrpc/proto/hello_http" // 引入proto包
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"log"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
func main() {
// TLS连接 serverNameOverride值为制作证书时Common Name的值
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.go
gRPC and https listen on: 127.0.0.1:50052
1
2
3
4
$ cd hello_http_2/client && go run main.go
Hello gRPC.
# HTTP 请求
$ curl -X POST -k https://localhost:50052/example/echo -d '{"name": "gRPC-HTTP is working!"}'{"message":"Hello gRPC-HTTP is working!."}