go-grpc实践指南-05Http网关-grpc-gateway
http网关
etcd3 API全面升级为gRPC后,同时要提供REST API服务,维护两个版本的服务显然不太合理,所以grpc-gateway诞生了。通过protobuf的自定义option实现了一个网关,服务端同时开启grpc和http服务,http服务接收到客户请求后转换为grpc请求数据,获取响应后转换为json数据返回给客户端。
结构如图:
安装grpc-gateway
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2
会在GOPAHT/bin目录下生成两个可执行文件:
protoc-gen-grpc-gateway.exe
protoc-gen-openapiv2.exe
目录结构
|—— hello_http/
|—— client/
|—— main.go // 客户端
|—— server/
|—— main.go // GRPC服务端
|—— server_http/
|—— main.go // HTTP服务端
|—— proto/
|—— google // googleApi http-proto定义
|—— api
|—— annotations.proto
|—— annotations.pb.go
|—— http.proto
|—— http.pb.go
|—— 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转换提供支持。
下载路径
示例代码
Step 1. 编写proto描述文件:proto/hello_http.proto
syntax = "proto3";
package hello_http;
option go_package = "hello_http";
import "google/api/annotations.proto";
// 定义hello服务
service Hello {
rpc SayHello(HelloHTTPRequest) returns(HelloHTTPResponse){
// http option
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
# 编译google.api
protoc --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative .\proto\google\api\*.proto -I proto/
# 编译hello_http.proto
protoc --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative .\proto\hello_http\hello_http.proto -I proto/
# 编译hello_http.proto gateway
protoc -I proto/ --grpc-gateway_out=logtostderr=true:./proto/ .\proto\hello_http\hello_http.proto
注意这里需要编译google/api中的两个proto文件,用grpc-gateway编译生成hello_http_pb.gw.go文件,这个文件就是用来做协议转换的,查看文件可以看到里面生成的http handler,处理proto文件中定义的路由”example/echo”接收POST参数,调用HelloHTTP服务的客户端请求grpc服务并响应结果。
Step 3: 实现服务端和客户端
server/main.go:
package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"log"
"my_grpc/proto/hello_http"
"net"
)
const (
Address = ":9000"
)
type helloService struct {
hello_http.UnimplementedHelloServer
}
var HelloService = new(helloService)
func (*helloService) SayHello(ctx context.Context, in *hello_http.HelloHTTPRequest) (*hello_http.HelloHTTPResponse, error) {
rsp := new(hello_http.HelloHTTPResponse)
rsp.Message = "hello " + in.Name
return rsp, nil
}
func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
log.Fatalln("net.Listen err:", err)
}
gServer := grpc.NewServer()
// 注册服务
hello_http.RegisterHelloServer(gServer, HelloService)
grpclog.Errorln("Listen on ", Address)
err = gServer.Serve(listen)
if err != nil {
log.Fatalln("gServer.Serve err:", err)
}
}
client/main.go:
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"my_grpc/proto/hello_http"
)
func main() {
clientConn, err := grpc.Dial(":9000", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalln("grpc.Dial err:", err)
}
helloClient := hello_http.NewHelloClient(clientConn)
reply, err := helloClient.SayHello(context.Background(), &hello_http.HelloHTTPRequest{Name: "gRPC"})
if err != nil {
log.Fatalln("helloClient.SayHello err:", err)
}
fmt.Println(reply)
}
server_http/main.go:
package main
import (
"context"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/grpclog"
"log"
gw "my_grpc/proto/hello_http"
"net/http"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// grpc服务地址
endpoint := ":9000"
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
// http转grpc
err := gw.RegisterHelloHandlerFromEndpoint(ctx, mux, endpoint, opts)
if err != nil {
log.Fatalln("gw.RegisterHelloHandlerFromEndpoint err:", err)
}
grpclog.Errorln("HTTP listen on 8000")
err = http.ListenAndServe(":8000", mux)
if err != nil {
log.Fatalln("http.ListenAndServe err:", err)
}
}
就是这么简单。开启了一个http server,收到请求后根据路由转发请求到对应的RPC接口获得结果。grpc-gateway做的事情就是帮我们自动生成了转换过程的实现。
运行结果
依次开启gRPC服务和HTTP服务端:
go run hello_http/server/main.go
go run hello_http/server_http/main.go
调用grpc客户端:
go run .\hello_http\client\main.go
利用postman或者insomnia发送http的post请求:
http://127.0.0.1:8000/example/echo
携带json参数:
{
"name": "mayanan"
}
# 返回结果:
{
"message": "hello mayanan"
}
升级服务端
上面的使用方式已经实现了我们最初的需求,grpc-gateway项目中提供的示例也是这种使用方式,这样后台需要开启两个服务两个端口。其实我们也可以只开启一个服务,同时提供http和gRPC调用方式。
新建一个项目hello_http_2, 基于hello_tls项目改造。客户端只要修改调用的proto包地址就可以了,这里我们看服务端的实现:
点击查看代码
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"golang.org/x/net/http2"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
"io/ioutil"
"log"
"my_grpc/proto/hello_http"
"my_grpc/utils"
"net"
"net/http"
"strings"
)
const (
Address = ":9000"
)
type helloService struct {
hello_http.UnimplementedHelloServer
}
var HelloService = new(helloService)
func (*helloService) SayHello(ctx context.Context, in *hello_http.HelloHTTPRequest) (*hello_http.HelloHTTPResponse, error) {
rsp := new(hello_http.HelloHTTPResponse)
rsp.Message = "hello " + in.Name
grpclog.Errorln(in.Name)
return rsp, nil
}
func main() {
// 监听
listen, err := net.Listen("tcp", Address)
if err != nil {
log.Fatalln("net.Listen err:", err)
}
// grpc TLS Server
serverOpts := utils.GetTlsServerOpts()
gServer := grpc.NewServer(serverOpts...)
hello_http.RegisterHelloServer(gServer, HelloService)
// gw server
ctx := context.Background()
dialOpts := utils.GetTlsDialOpts()
gwMux := runtime.NewServeMux()
err = hello_http.RegisterHelloHandlerFromEndpoint(ctx, gwMux, Address, dialOpts)
if err != nil {
log.Fatalln("hello_http.RegisterHelloHandlerFromEndpoint err:", err)
}
// http服务
mux := http.NewServeMux()
mux.Handle("/", gwMux)
srv := http.Server{
Addr: Address,
Handler: grpcHandlerFunc(gServer, mux),
TLSConfig: getTlsConfig(),
}
grpclog.Errorf("gRPC and http listen on %s", Address)
if err = srv.Serve(tls.NewListener(listen, srv.TLSConfig)); err != nil {
log.Fatalln("srv.Serve err:", err)
}
}
func getTlsConfig() *tls.Config {
cert, err := tls.LoadX509KeyPair("keys/server/server.pem", "keys/server/server.key")
if err != nil {
log.Fatalln("tls.LoadX509KeyPair err:", err)
}
demoCert := &cert
return &tls.Config{
Certificates: []tls.Certificate{*demoCert},
NextProtos: []string{http2.NextProtoTLS}, // http2 TLS支持
}
}
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)
}
})
}
utils工具包中的代码
utils/tls.go:
package utils
import (
"crypto/tls"
"crypto/x509"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"log"
)
func GetTlsServerOpts() (opts []grpc.ServerOption) {
certifacate, err := tls.LoadX509KeyPair("keys/server/server.pem", "keys/server/server.key")
if err != nil {
log.Fatalln("tls加载X509失败,err:",err)
}
// 创建证书池
certPool := x509.NewCertPool()
// 向证书池中加入证书
certBytes, err := ioutil.ReadFile("keys/ca/ca.crt")
if err != nil {
log.Fatalln("读取ca.crt证书失败,err:", err)
}
// 加载证书从pem文件里面
certPool.AppendCertsFromPEM(certBytes)
// 创建credentials对象
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certifacate}, // 服务端证书
ClientAuth: tls.RequireAndVerifyClientCert, // 需要并且验证客户端证书
ClientCAs: certPool, // 客户端证书池
})
opts = append(opts, grpc.Creds(creds))
return opts
}
func GetTlsDialOpts() (opts []grpc.DialOption) {
cert, err := tls.LoadX509KeyPair("keys/client/client.pem", "keys/client/client.key")
if err != nil {
log.Fatalln("tls.LoadX509 err:", err)
}
certPool := x509.NewCertPool()
certBytes, err := ioutil.ReadFile("keys/ca/ca.crt")
if err != nil {
log.Fatalln("读取ca证书失败:", err)
}
certPool.AppendCertsFromPEM(certBytes)
tcreds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},// 放入客户端证书
ServerName: "localhost", //证书里面的 commonName
RootCAs: certPool, // 证书池
})
creds := grpc.WithTransportCredentials(tcreds)
opts = append(opts, creds)
return opts
}
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
运行结果
# go run .\hello_http_2\server\main.go
2022/09/27 11:36:55 ERROR: gRPC and http listen on :9000
2022/09/27 11:37:08 ERROR: gRPC 你好
# grpc请求
# go run .\hello_http_2\client\main.go
message:"hello gRPC 你好"
# postman发送post请求:https://127.0.0.1:9000/example/echo
携带Json参数:{"name": "马艳娜"}
{
message: "hello 马艳娜"
}