1、gRPC Gateway
etcd3 API全面升级为gRPC后,同时要提供REST API服务,维护两个版本的服务显然不太合理,所以
grpc-gateway
诞生了。通过protobuf的自定义option实现了一个网关,服务端同时开启gRPC和HTTP服务,
HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。结构如图:
grpc-gateway地址:https://github.com/grpc-ecosystem/grpc-gateway
1.1 安装grpc-gateway
$ go get github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@v1.16.0
$ go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@v1.16.0
1.2 proto编写
这里用到了google官方Api中的两个proto描述文件,直接拷贝不要做修改,里面定义了protocol buffer扩展的
HTTP option,为grpc的http转换提供支持。
annotations.proto
文件的内容:
// ./proto/google/api/annotations.proto
syntax = "proto3";package google.api;option go_package = "google/api;google_api";import "google/api/http.proto";
import "google/protobuf/descriptor.proto";option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";extend google.protobuf.MethodOptions {HttpRule http = 72295728;}
http.proto
文件的内容:
// ./proto/google/api/http.proto
syntax = "proto3";package google.api;option go_package = "google/api;google_api";option cc_enable_arenas = true;
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";message Http {repeated HttpRule rules = 1;
}message HttpRule {string selector = 1;oneof pattern {string get = 2;string put = 3;string post = 4;string delete = 5;string patch = 6;CustomHttpPattern custom = 8;}string body = 7;repeated HttpRule additional_bindings = 11;
}message CustomHttpPattern {string kind = 1;string path = 2;
}
编写自定义的proto
描述文件hello_http.proto
:
// ./proto/hello_http/hello_http.proto
syntax = "proto3";
package hello_http;
option go_package = "./hello_http;hello_http";import "google/api/annotations.proto";// 定义Hello服务
service HelloHTTP {// 定义SayHello方法rpc SayHello(HelloHTTPRequest) returns (HelloHTTPResponse) {// http optionoption (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
。
1.3 编译proto
$ cd proto# 编译google.api
$ protoc -I . --go_out=plugins=grpc:. google/api/*.proto# 编译hello_http.proto
$ protoc -I . --go_out=plugins=grpc:. hello_http/*.proto# 编译hello_http.proto gateway
$ protoc --grpc-gateway_out=logtostderr=true:. hello_http/hello_http.proto
注意这里需要编译google/api中的两个proto文件,最后使用grpc-gateway编译生成hello_http_pb.gw.go
文
件,这个文件就是用来做协议转换的,查看文件可以看到里面生成的http handler,处理proto文件中定义的路由
example/echo
接收POST参数,调用HelloHTTP服务的客户端请求grpc服务并响应结果。
1.4 实现服务端
server.go
的内容:
package mainimport ("context""fmt""net"// 引入编译生成的包pb "demo/proto/hello_http""google.golang.org/grpc""log"
)const (// Address gRPC服务地址Address = "127.0.0.1:50053"
)// 定义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 Servers := grpc.NewServer()// 注册HelloServicepb.RegisterHelloHTTPServer(s, HelloService)log.Println("Listen on " + Address)s.Serve(listen)
}
1.5 客户端实现
client.go
的内容:
package mainimport ("context"pb "demo/proto/hello_http""google.golang.org/grpc""log"
)const (// Address gRPC服务地址Address = "127.0.0.1:50053"
)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)
}
1.6 http server
server_http.go
的内容:
package mainimport ("fmt""github.com/grpc-ecosystem/grpc-gateway/runtime"gw "demo/proto/hello_http""golang.org/x/net/context""google.golang.org/grpc""log""net/http"
)func main() {ctx := context.Background()ctx, cancel := context.WithCancel(ctx)defer cancel()// grpc服务地址endpoint := "127.0.0.1:50053"mux := runtime.NewServeMux()opts := []grpc.DialOption{grpc.WithInsecure()}// HTTP转grpcerr := 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做的事情就是帮我们自动生成了转换过程的实现。
1.7 测试
依次开启gRPC服务和HTTP服务端:
[root@zsx demo]# go run server.go
2023/02/12 09:38:52 Listen on 127.0.0.1:50053
[root@zsx demo]# go run server_http.go
2023/02/12 09:39:07 HTTP Listen on 8080
调用grpc客户端:
[root@zsx demo]# go run client.go
2023/02/12 09:39:37 Hello gRPC.
# 发送 HTTP 请求
[root@zsx demo]# curl -X POST -k http://localhost:8080/example/echo -d "{\"name\":\"gRPC-HTTP\"}"
{"message":"Hello gRPC-HTTP."}
# 项目结构
$ tree demo
demo
├── client.go
├── go.mod
├── go.sum
├── proto
│ ├── google # googleApi http-proto定义
│ │ └── api
│ │ ├── annotations.pb.go
│ │ ├── annotations.proto
│ │ ├── http.pb.go
│ │ └── http.proto
│ └── hello_http
│ ├── hello_http.pb.go
│ ├── hello_http.pb.gw.go # gateway编译后文件
│ └── hello_http.proto
├── server.go # gRPC服务端
└── server_http.go # HTTP服务端4 directories, 12 files
1.8 升级版服务端(gRPC转换HTTP)
上面的使用方式已经实现了我们最初的需求,grpc-gateway
项目中提供的示例也是这种使用方式,这样后台需
要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供http和gRPC调用方式。
新建一个项目,基于上面的项目改造,客户端只要修改调用的proto包地址就可以了。
1.8.1 服务端实现
server.go
的内容:
package mainimport ("crypto/tls""github.com/grpc-ecosystem/grpc-gateway/runtime"pb "demo/proto/hello_http""golang.org/x/net/context""golang.org/x/net/http2""google.golang.org/grpc""google.golang.org/grpc/credentials""log""io/ioutil""net""net/http""strings"
)// 定义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 servercreds, err := credentials.NewServerTLSFromFile("./cert/server/server.pem", "./cert/server/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 serverctx := context.Background()dcreds, err := credentials.NewClientTLSFromFile("./cert/server/server.pem", "test.example.com")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("./cert/server/server.pem")key, _ := ioutil.ReadFile("./cert/server/server.key")var demoKeyPair *tls.Certificatepair, err := tls.X509KeyPair(cert, key)if err != nil {log.Fatalf("TLS KeyPair err: %v\n", err)}demoKeyPair = &pairreturn &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)}})
}
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.8.2 客户端实现
client.go
的内容:
package mainimport ("context"// 引入proto包pb "demo/proto/hello_http""google.golang.org/grpc"// 引入grpc认证包"google.golang.org/grpc/credentials""log"
)const (// Address gRPC服务地址Address = "127.0.0.1:50052"
)func main() {log.Println("客户端连接!")// TLS连接creds, err := credentials.NewClientTLSFromFile("./cert/server/server.pem", "test.example.com")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:", 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)
}
1.8.3 测试
[root@zsx demo]# go run server.go
2023/02/12 09:57:44 gRPC and https listen on: 127.0.0.1:50052[root@zsx demo]# go run client.go
2023/02/12 09:59:46 客户端连接!
2023/02/12 09:59:46 Hello gRPC.
# 发送 HTTP 请求
[root@zsx demo]# curl -X POST -k https://localhost:50052/example/echo -d "{\"name\":\"gRPC-HTTP\"}"
{"message":"Hello gRPC-HTTP."}
# 项目结构
$ tree demo/
demo/
├── cert
│ ├── ca.crt
│ ├── ca.csr
│ ├── ca.key
│ ├── ca.srl
│ ├── client
│ │ ├── client.csr
│ │ ├── client.key
│ │ └── client.pem
│ ├── openssl.cnf
│ └── server
│ ├── server.csr
│ ├── server.key
│ └── server.pem
├── client.go
├── go.mod
├── go.sum
├── proto
│ ├── google
│ │ └── api
│ │ ├── annotations.pb.go
│ │ ├── annotations.proto
│ │ ├── http.pb.go
│ │ └── http.proto
│ └── hello_http
│ ├── hello_http.pb.go
│ ├── hello_http.pb.gw.go
│ └── hello_http.proto
└── server.go