gRPC转换HTTP
gRPC转换HTTP
一、前言
我们通常把RPC
用作内部通信,而使用Restful Api
进行外部通信。为了避免写两套应用,我们使用grpc-gateway把gRPC
转成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响应给客户端。结构如图:
下面我们就直接实战吧。基于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`,俩个文件都需要下载。
- 创建
google/api/
文件目录; - 下载
annotations.proto
文件,并保存到google/api
目录下面 annotations.proto下载地址; - 下载http.proto文件,并保存到
google/api
目录下面,http.proto下载地址
五、编译引用外部包问题
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.
5.2 解决方式1
- 到官方下载descriptor.proto文件,保存到本地,下载google/protobuf地址;
- 在创建proto目录下面创建
google/protobuf
目录并将将下载的descriptor.proto文件移动到该目录下面;
- 修改外部引用,修改为自己下载的文件的目录
//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"; // 修改自己的目录
- 修改
simple.proto
文件
//import "google/api/annotations.proto";
import "10grpc-gateway/proto/google/api/annotations.proto";
进入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
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
,此时的完整路径应该是:
$GOPATH/src/google/api/annotations.proto
这才完成了第一步,如果这时候你去直接执行protoc编译命令,依旧会得到上面的报错信息,protoc并没有成功的获取到外部proto文件。
为了解决问题,首先了解下protoc中import的两条规则:
- import 不允许使用相对路径;
- 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测试
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)
}
}
七、总结
本篇介绍了如何使用grpc-gateway
让gRPC
同时支持HTTP,最终转成的Restful Api
支持bearer token
验证、数据验证。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律