gRPC转换HTTP

gRPC转换HTTP

一、前言

我们通常把RPC用作内部通信,而使用Restful Api进行外部通信。为了避免写两套应用,我们使用grpc-gatewaygRPC转成HTTP。服务接收到HTTP请求后,grpc-gateway把它转成gRPC进行处理,然后以JSON形式返回数据。本篇代码以上篇为基础,最终转成的Restful Api支持bearer token验证、数据验证,并添加swagger文档。

正当有这个需求的时候,就看到了这个实现姿势。源自coreos的一篇博客,转载到了grpc官方博客gRPC with REST and Open APIs

etcd3改用grpc后为了兼容原来的api,同时要提供http/json方式的API,为了满足这个需求,要么开发两套API,要么实现一种转换机制,他们选择了后者,而我们选择跟随他们的脚步。

他们实现了一个协议转换的网关,对应github上的项目grpc-gateway,这个网关负责接收客户端请求,然后决定直接转发给grpc服务还是转给http服务,当然,http服务也需要请求grpc服务获取响应,然后转为json响应给客户端。结构如图:

image-20220515175009451

下面我们就直接实战吧。基于hello-tls项目扩展,客户端改动不大,服务端和proto改动较大。

源码

二、安装grpc-gateway

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

三、gRPC转成HTTP

1.编写simple.proto

syntax = "proto3";

package proto;

import "github.com/mwitkow/go-proto-validators/validator.proto";
import "go-grpc-example/10-grpc-gateway/proto/google/api/annotations.proto";

message InnerMessage {
  // some_integer can only be in range (1, 100).
  int32 some_integer = 1 [(validator.field) = {int_gt: 0, int_lt: 100}];
  // some_float can only be in range (0;1).
  double some_float = 2 [(validator.field) = {float_gte: 0, float_lte: 1}];
}

message OuterMessage {
  // important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax).
  string important_string = 1 [(validator.field) = {regex: "^[a-z]{2,5}$"}];
  // proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage.
  InnerMessage inner = 2 [(validator.field) = {msg_exists : true}];
}

service Simple{
  rpc Route (InnerMessage) returns (OuterMessage){
      option (google.api.http) ={
          post:"/v1/example/route"
          body:"*"
      };
  }
}

可以看到,proto变化不大,只是添加了API的路由路径

      option (google.api.http) ={
          post:"/v1/example/route"
          body:"*"
      };

四、引入编译依赖项目

simple.proto文件引用了google/api/annotations.proto,我这里是把google/文件夹直接复制到项目中的proto/目录。发现annotations.proto引用了google/api/http.proto`,俩个文件都需要下载。

  1. 创建google/api/文件目录;
  2. 下载annotations.proto文件,并保存到google/api目录下面 annotations.proto下载地址;
  3. 下载http.proto文件,并保存到google/api目录下面,http.proto下载地址

image-20220428220514190

五、编译引用外部包问题

5.1 问题

google/api/http.proto: File not found.
google/protobuf/descriptor.proto: File not found.
10grpc-gateway/proto/google/api/annotations.proto:19:1: Import "google/api/http.proto" was not found or had errors.
10grpc-gateway/proto/google/api/annotations.proto:22:1: Import "google/protobuf/descriptor.proto" was not found or had errors.
10grpc-gateway/proto/google/api/annotations.proto:32:8: "google.protobuf.MethodOptions" seems to be defined in "10grpc-gateway/proto/google/protobuf/descriptor.proto", which is not imported by "10grpc-gateway/proto/google/api/annota
tions.proto".  To use it here, please add the necessary import.
10grpc-gateway/proto/simple.proto:6:1: Import "10grpc-gateway/proto/google/api/annotations.proto" was not found or had errors.

image-20220428220931431

5.2 解决方式1

  1. 到官方下载descriptor.proto文件,保存到本地,下载google/protobuf地址;

image-20220428210933076

  1. 在创建proto目录下面创建google/protobuf目录并将将下载的descriptor.proto文件移动到该目录下面;

image-20220428211051521

  1. 修改外部引用,修改为自己下载的文件的目录
//import "google/api/http.proto";
import "10grpc-gateway/proto/google/api/http.proto";  // 修改自己的目录

//import "google/protobuf/descriptor.proto";
import "10grpc-gateway/proto/google/protobuf/descriptor.proto";  // 修改自己的目录

image-20220428221408281

  1. 修改simple.proto文件
//import "google/api/annotations.proto";
import "10grpc-gateway/proto/google/api/annotations.proto";

image-20220428221943556

进入grpc所在目录,编译就不会出错:

// 生成simple.validator.pb.go和simple.pb.go
protoc -I ./  --govalidators_out=.\10grpc-gateway\proto\ --go_out=plugins=grpc:.\10grpc-gateway\proto\  .\10grpc-gateway\proto\simple.proto

// 生成simple.pb.gw.go
protoc -I ./ --grpc-gateway_out=logtostderr=true:.\10grpc-gateway\proto\ .\10grpc-gateway\proto\simple.proto

image-20220428221548790

5.3 方法2

test.proto文件中引用了一个外部包:

import "google/api/annotations.proto";

当使用命令编译的时候提示找不到包:

# protoc --go_out=plugins=grpc:. ./test.proto
google/api/annotations.proto: File not found.
test.proto:5:1: Import "google/api/annotations.proto" was not found or had errors.

解决:

去github上将对应的包下载下来放在$GOPATH/src下,例如这里缺失google/api

gooogleapis将项目下载下来,并将整个项目放到$GOPATH/src,此时的完整路径应该是:

image-20220428222059016

$GOPATH/src/google/api/annotations.proto

image-20220428222303739

这才完成了第一步,如果这时候你去直接执行protoc编译命令,依旧会得到上面的报错信息,protoc并没有成功的获取到外部proto文件。

为了解决问题,首先了解下protoc中import的两条规则:

  1. import 不允许使用相对路径;
  2. import 导入路径应该从根开始的绝对路径;

这个根开始的绝对路径指的是$GOPATH/src开始的路径,这个需要先了解。

假设此时的目录结构为:

src
-- google
  -- api
  	-- annotations.proto
-- test
  -- test.proto

test.proto中引用了google/api/annotations.proto,此时我们命令的执行位置为:

src/test

执行的命令为:

protoc --go_out=plugins=grpc:. ./test.proto

protoc有一个参数-I,表示引入文件的目录路径,这里有

-I参数简单来说,就是如果多个proto文件之间有互相依赖,生成某个proto文件时,需要import其他几个proto文件,这时候就要用-I来指定搜索目录。如果没有指定-I参数,则在当前目录进行搜索。

例如这里的import "google/api/annotations.proto";,这里的这个路径,其实是从$GOPATH/src开始的路径。

也就是说,首先要用-I参数将引入包的路径设置到$GOPATH/src目录下,即

protoc -I ../

完整命令:

# pwd
.../src/test
# protoc -I ../ -I ./ --go_out=plugins=grpc:. ./test.proto

每个-I参数都引入一个目录,proto文件中引入了几个外部proto文件理论来说就需要多少个-I(同一目录的可以一次性引入),再加上待编译的proto也需要引入,所以上面这里就用了两个-I来引入目录文件。

推荐使用$GOPATH/src的方式来引入,简单直观不容易出错b

protoc -I ./ \
	-I $GOPATH/src \
	-I $GOPATH/src/google/api \
	--go_out=plugins=grpc:. ./xxx.proto

等同于:

#  test.pb.go
protoc -I G:\goproject\src   -I ./ --go_out=plugins=grpc:. ./test.proto
# test.pb.gw.go
protoc -I G:\goproject\src   -I ./ --grpc-gateway_out=logtostderr=true:. .\proto\test.proto

六、服务端代码修改

1.pkg/文件夹下新建gateway/目录,然后在里面新建gateway.go文件

package gateway

import (
	"context"
	"crypto/tls"
	"io/ioutil"
	"log"
	"net/http"
	"strings"

	"google.golang.org/grpc/credentials/insecure"

	pb "go-grpc-example/11grpc-gateway/proto"

	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"
)

/*
@author RandySun
@create 2022-05-08-23:40
*/

// ProvideHTTP 把gRPC服务转成HTTP服务,让gRPC同时支持HTTP
func ProvideHTTP(endpoint string, grpcServer *grpc.Server) *http.Server {
	ctx := context.Background()
	//获取证书
	//creds, err := credentials.NewServerTLSFromFile("G:\\goproject\\go\\grpcGateway\\pkg\\tls\\server_cert.pem", "G:\\goproject\\go\\grpcGateway\\pkg\\tls\\server_key.pem")
	//if err != nil {
	//	log.Fatalf("Failed to create TLS credentials %v", err)
	//}
	//添加证书
	dopts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	//dopts := []grpc.DialOption{grpc.WithTransportCredentials(creds)}
	//新建gwmux,它是grpc-gateway的请求复用器。它将http请求与模式匹配,并调用相应的处理程序。
	gwmux := runtime.NewServeMux()
	//将服务的http处理程序注册到gwmux。处理程序通过endpoint转发请求到grpc端点
	err := pb.RegisterSimpleHandlerFromEndpoint(ctx, gwmux, endpoint, dopts)
	if err != nil {
		log.Fatalf("Register Endpoint err: %v", err)
	}
	//新建mux,它是http的请求复用器
	mux := http.NewServeMux()
	//注册gwmux
	mux.Handle("/", gwmux)

	log.Println(endpoint + " HTTP.Listing whth TLS and token...")
	return &http.Server{
		Addr:    endpoint,
		Handler: grpcHandlerFunc(grpcServer, mux),
		//TLSConfig: getTLSConfig(),
	}
}

// grpcHandlerFunc 根据不同的请求重定向到指定的Handler处理
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
	return h2c.NewHandler(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)
		}
	}), &http2.Server{})
}

// getTLSConfig获取TLS配置
func getTLSConfig() *tls.Config {
	cert, _ := ioutil.ReadFile("server_cert.pem")
	key, _ := ioutil.ReadFile("server_key.pem")
	var demoKeyPair *tls.Certificate
	pair, err := tls.X509KeyPair(cert, key)
	if err != nil {
		grpclog.Fatalf("TLS KeyPair err: %v\n", err)
	}
	demoKeyPair = &pair
	return &tls.Config{
		Certificates: []tls.Certificate{*demoKeyPair},
		NextProtos:   []string{http2.NextProtoTLS}, // HTTP2 TLS支持
	}
}

它主要作用是把不用的请求重定向到指定的服务处理,从而实现把HTTP请求转到gRPC服务。

2.gRPC支持HTTP

//使用gateway把grpcServer转成httpServer
httpServer := gateway.ProvideHTTP(Address, grpcServer)

// 证书认证 https
if err = httpServer.Serve(tls.NewListener(listener, httpServer.TLSConfig)); err != nil {
    log.Fatal("ListenAndServe: ", err)
}

// 无需证书认证 http
if err = httpServer.Serve(listener); err != nil {
			zap.L().Error("ListenAndServe: ", zap.Error(err))
}

使用postman测试

img

image-20220508235913789

grpc客户端

package main

import (
	"context"
	pb "go-grpc-example/11grpc-gateway/proto"
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

/*
@author RandySun
@create 2022-05-08-23:40
*/

// Address 连接地址
const Address string = ":8000"

var grpcClient pb.SimpleClient

func main() {
	//从输入的证书文件中为客户端构造TLS凭证

	conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("net.Connect err: %v", err)
	}
	defer conn.Close()

	// 建立gRPC连接
	grpcClient = pb.NewSimpleClient(conn)
	route()
}

// route 调用服务端Route方法
func route() {
	// 创建发送结构体
	req := pb.InnerMessage{
		SomeInteger: 99,
		SomeFloat:   1,
	}
	// 调用我们的服务(Route方法)
	// 同时传入了一个 context.Context ,在有需要时可以让我们改变RPC的行为,比如超时/取消一个正在运行的RPC
	res, err := grpcClient.Route(context.Background(), &req)
	if err != nil {
		log.Fatalf("Call Route err: %v", err)
	}
	// 打印返回值
	log.Println(res)
}

服务端:

package main

import (
	"context"
	"go-grpc-example/11grpc-gateway/pkg/gateway"
	pb "go-grpc-example/11grpc-gateway/proto"
	"log"
	"net"

	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"

	"google.golang.org/grpc"
)

/*
@author RandySun
@create 2022-05-08-23:40
*/

// SimpleService 定义我们的服务
type SimpleService struct{}

// Route 实现Route方法
func (s *SimpleService) Route(ctx context.Context, req *pb.InnerMessage) (*pb.OuterMessage, error) {
	res := pb.OuterMessage{
		ImportantString: "hello grpc gateway",
		Inner:           req,
	}
	return &res, nil
}

const (
	// Address 监听地址
	Address string = ":8000"
	// Network 网络通信协议
	Network string = "tcp"
)

func main() {
	// 监听本地端口
	listener, err := net.Listen(Network, Address)
	if err != nil {
		log.Fatalf("net.Listen err: %v", err)
	}

	// 新建gRPC服务器实例
	grpcServer := grpc.NewServer(
		grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
			grpc_validator.StreamServerInterceptor(), // 校验器

		)),
		grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
			grpc_validator.UnaryServerInterceptor(), // 校验器

		)),
	)
	// 在gRPC服务器注册我们的服务
	pb.RegisterSimpleServer(grpcServer, &SimpleService{})
	log.Println(Address + " net.Listing whth TLS and token...")
	// grpc-->http
	httpServer := gateway.ProvideHTTP(Address, grpcServer)

	// 无需证书认证
	if err = httpServer.Serve(listener); err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
	//用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatalf("grpcServer.Serve err: %v", err)
	}
}

image-20220509000218019

七、总结

本篇介绍了如何使用grpc-gatewaygRPC同时支持HTTP,最终转成的Restful Api支持bearer token验证、数据验证。

posted @ 2022-05-15 18:27  RandySun  阅读(935)  评论(0编辑  收藏  举报