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 马艳娜"
}

github源码地址

posted @ 2022-09-27 13:42  专职  阅读(513)  评论(0编辑  收藏  举报