Go gRPC进阶教程gRPC转换HTTP


Posted in Golang onJune 16, 2022

前言

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

gRPC转成HTTP

编写和编译proto

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:"*"
      };

2.编译simple.proto

simple.proto文件引用了google/api/annotations.proto,先要把它编译了。我这里是把google/文件夹直接复制到项目中的proto/目录中进行编译。发现annotations.proto引用了google/api/http.proto,那把它也编译了。

进入annotations.proto所在目录,编译:

protoc --go_out=plugins=grpc:./ ./http.proto
protoc --go_out=plugins=grpc:./ ./annotations.proto

进入simple.proto所在目录,编译:

#生成simple.validator.pb.go和simple.pb.go
protoc --govalidators_out=. --go_out=plugins=grpc:./ ./simple.proto
#生成simple.pb.gw.go
protoc --grpc-gateway_out=logtostderr=true:./ ./simple.proto

以上完成proto编译,接着修改服务端代码。

服务端代码修改

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

package gateway
import (
	"context"
	"crypto/tls"
	"io/ioutil"
	"log"
	"net/http"
	"strings"
	pb "go-grpc-example/10-grpc-gateway/proto"
	"go-grpc-example/10-grpc-gateway/server/swagger"
	"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/credentials"
	"google.golang.org/grpc/grpclog"
)
// ProvideHTTP 把gRPC服务转成HTTP服务,让gRPC同时支持HTTP
func ProvideHTTP(endpoint string, grpcServer *grpc.Server) *http.Server {
	ctx := context.Background()
	//获取证书
	creds, err := credentials.NewClientTLSFromFile("../tls/server.pem", "go-grpc-example")
	if err != nil {
		log.Fatalf("Failed to create TLS credentials %v", err)
	}
	//添加证书
	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("../tls/server.pem")
	key, _ := ioutil.ReadFile("../tls/server.key")
	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)
	if err = httpServer.Serve(tls.NewListener(listener, httpServer.TLSConfig)); err != nil {
		log.Fatal("ListenAndServe: ", err)
	}

使用postman测试

Go gRPC进阶教程gRPC转换HTTP

在动图中可以看到,我们的gRPC服务已经同时支持RPC和HTTP请求了,而且API接口支持bearer token验证和数据验证。为了方便对接,我们把API接口生成swagger文档。

生成swagger文档

simple.swagger.json

1.安装protoc-gen-swagger

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

2.编译生成simple.swagger.json

到simple.proto文件目录下,编译:

protoc --swagger_out=logtostderr=true:./ ./simple.proto

再次提一下,本人在VSCode中使用VSCode-proto3插件,第一篇有介绍,只要保存,就会自动编译,很方便,无需记忆指令。完整配置如下:

编译生成后把需要的文件留下,不需要的删掉。

把swagger-ui转成Go代码,备用

1.下载swagger-ui

下载地址,把dist目录下的所有文件拷贝我们项目的server/swagger/swagger-ui/目录下。

2.把Swagger UI转换为Go代码

安装go-bindata:

go get -u github.com/jteeuwen/go-bindata/...

回到server/所在目录,运行指令把Swagger UI转成Go代码。

go-bindata --nocompress -pkg swagger -o swagger/datafile.go swagger/swagger-ui/...

这步有坑,必须要回到main函数所在的目录运行指令,因为生成的Go代码中的_bindata 映射了swagger-ui的路径,程序是根据这些路径来找页面的。如果没有在main函数所在的目录运行指令,则生成的路径不对,会报404,无法找到页面。本项目server/端的main函数在server.go中,所以在server/所在目录下运行指令。

var _bindata = map[string]func() (*asset, error){
	"swagger/swagger-ui/favicon-16x16.png": swaggerSwaggerUiFavicon16x16Png,
	"swagger/swagger-ui/favicon-32x32.png": swaggerSwaggerUiFavicon32x32Png,
	"swagger/swagger-ui/index.html": swaggerSwaggerUiIndexHtml,
	"swagger/swagger-ui/oauth2-redirect.html": swaggerSwaggerUiOauth2RedirectHtml,
	"swagger/swagger-ui/swagger-ui-bundle.js": swaggerSwaggerUiSwaggerUiBundleJs,
	"swagger/swagger-ui/swagger-ui-bundle.js.map": swaggerSwaggerUiSwaggerUiBundleJsMap,
	"swagger/swagger-ui/swagger-ui-standalone-preset.js": swaggerSwaggerUiSwaggerUiStandalonePresetJs,
	"swagger/swagger-ui/swagger-ui-standalone-preset.js.map": swaggerSwaggerUiSwaggerUiStandalonePresetJsMap,
	"swagger/swagger-ui/swagger-ui.css": swaggerSwaggerUiSwaggerUiCss,
	"swagger/swagger-ui/swagger-ui.css.map": swaggerSwaggerUiSwaggerUiCssMap,
	"swagger/swagger-ui/swagger-ui.js": swaggerSwaggerUiSwaggerUiJs,
	"swagger/swagger-ui/swagger-ui.js.map": swaggerSwaggerUiSwaggerUiJsMap,
}

对外提供swagger-ui

1.在swagger/目录下新建swagger.go文件

package swagger
import (
	"log"
	"net/http"
	"path"
	"strings"
	assetfs "github.com/elazarl/go-bindata-assetfs"
)
//ServeSwaggerFile 把proto文件夹中的swagger.json文件暴露出去
func ServeSwaggerFile(w http.ResponseWriter, r *http.Request) {
	if !strings.HasSuffix(r.URL.Path, "swagger.json") {
		log.Printf("Not Found: %s", r.URL.Path)
		http.NotFound(w, r)
		return
	}
	p := strings.TrimPrefix(r.URL.Path, "/swagger/")
	// "../proto/"为.swagger.json所在目录
	p = path.Join("../proto/", p)
	log.Printf("Serving swagger-file: %s", p)
	http.ServeFile(w, r, p)
}
//ServeSwaggerUI 对外提供swagger-ui
func ServeSwaggerUI(mux *http.ServeMux) {
	fileServer := http.FileServer(&assetfs.AssetFS{
		Asset:    Asset,
		AssetDir: AssetDir,
		Prefix:   "swagger/swagger-ui", //swagger-ui文件夹所在目录
	})
	prefix := "/swagger-ui/"
	mux.Handle(prefix, http.StripPrefix(prefix, fileServer))
}

2.注册swagger

在gateway.go中添加如下代码

//注册swagger
	mux.HandleFunc("/swagger/", swagger.ServeSwaggerFile)
	swagger.ServeSwaggerUI(mux)

到这里我们已经完成了swagger文档的添加工作了,由于谷歌浏览器不能使用自己制作的TLS证书,所以我们用火狐浏览器进行测试。

用火狐浏览器打开:https://127.0.0.1:8000/swagger-ui/

在最上面地址栏输入:https://127.0.0.1:8000/swagger/simple.swagger.json

然后就可以看到swagger生成的API文档了。

Go gRPC进阶教程gRPC转换HTTP

还有个问题,我们使用了bearer token进行接口验证的,怎么把bearer token也添加到swagger中呢?
最后我在grpc-gatewayGitHub上的这个Issues找到解决办法。

在swagger中配置bearer token

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";
import "go-grpc-example/10-grpc-gateway/proto/google/options/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}];
}
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
  security_definitions: {
    security: {
      key: "bearer"
      value: {
        type: TYPE_API_KEY
        in: IN_HEADER
        name: "Authorization"
        description: "Authentication token, prefixed by Bearer: Bearer <token>"
      }
    }
  }
  security: {
    security_requirement: {
      key: "bearer"
    }
  }
  info: {
		title: "grpc gateway sample";
		version: "1.0";	
		license: {
			name: "MIT";			
		};
  }
  schemes: HTTPS
};
service Simple{
  rpc Route (InnerMessage) returns (OuterMessage){
      option (google.api.http) ={
          post:"/v1/example/route"
          body:"*"
      };
      // //禁用bearer token
      // option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {
      //   security: { } // Disable security key
      // };
  }
}

2.重新编译生成simple.swagger.json

大功告成!

验证测试

1.添加bearer token

Go gRPC进阶教程gRPC转换HTTP

2.调用接口,正确返回数据

Go gRPC进阶教程gRPC转换HTTP

3.传递不合规则的数据,返回违反数据验证逻辑错误

Go gRPC进阶教程gRPC转换HTTP

总结

本篇介绍了如何使用grpc-gateway让gRPC同时支持HTTP,最终转成的Restful Api支持bearer token验证、数据验证。同时生成swagger文档,方便API接口对接。

教程源码地址:https://github.com/Bingjian-Zhu/go-grpc-example

参考文档

https://www.3water.com/article/251828.htm

https://www.3water.com/article/251837.htm

以上就是Go gRPC进阶教程gRPC转换HTTP的详细内容,更多关于Go gRPC转换HTTP的资料请关注三水点靠木其它相关文章!


Tags in this post...

Golang 相关文章推荐
Go缓冲channel和非缓冲channel的区别说明
Apr 25 Golang
Go语言中的UTF-8实现
Apr 26 Golang
解决golang在import自己的包报错的问题
Apr 29 Golang
浅谈golang 中time.After释放的问题
May 05 Golang
goland 设置project gopath的操作
May 06 Golang
golang 实现并发求和
May 08 Golang
go语言中http超时引发的事故解决
Jun 02 Golang
K8s部署发布Golang应用程序的实现方法
Jul 16 Golang
Go语言应该什么情况使用指针
Jul 25 Golang
Go语言基础map用法及示例详解
Nov 17 Golang
golang操作rocketmq的示例代码
Apr 06 Golang
Go本地测试解耦任务拆解及沟通详解Go本地测试的思路沟通的重要性总结
Jun 21 Golang
GoFrame gredis缓存DoVar Conn连接对象 自动序列化GoFrame gredisDo/DoVar方法Conn连接对象自动序列化/反序列化总结
Jun 14 #Golang
Go调用Rust方法及外部函数接口前置
详解Go语言中配置文件使用与日志配置
Jun 01 #Golang
详解Go语言中Get/Post请求测试
Golang实现可重入锁的示例代码
May 25 #Golang
Go web入门Go pongo2模板引擎
May 20 #Golang
Go语言入门exec的基本使用
May 20 #Golang
You might like
《五等分的花嫁》漫画完结!2020年10月第2期TV动画制作组换血!
2020/03/06 日漫
php 静态变量与自定义常量的使用方法
2010/01/26 PHP
php实现设计模式中的单例模式详解
2014/10/11 PHP
php绘图之在图片上写中文和英文的方法
2015/01/24 PHP
CI框架中redis缓存相关操作文件示例代码
2016/05/17 PHP
解决PHP程序运行时:Fatal error: Maximum execution time of 30 seconds exceeded in的错误提示
2016/11/25 PHP
JavaScript中的History历史对象
2008/01/16 Javascript
Jquery获取元素的父容器对象示例代码
2014/02/10 Javascript
javascript学习笔记(二)数组和对象部分
2014/09/30 Javascript
js实现在网页上简单显示时间的方法
2015/03/02 Javascript
this,this,再次讨论javascript中的this,超全面(经典)
2016/01/05 Javascript
jquery 全选、全不选、反选效果的实现代码【推荐】
2016/05/05 Javascript
JSON字符串转换JSONObject和JSONArray的方法
2016/06/03 Javascript
JS判断鼠标进入容器的方向与window.open新窗口被拦截的问题
2016/12/23 Javascript
详解Html a标签中href和onclick用法、区别、优先级别
2017/01/16 Javascript
easyui 中的datagrid跨页勾选问题的实现方法
2017/01/18 Javascript
Vue.js学习之计算属性
2017/01/22 Javascript
jQuery动态添加.active 实现导航效果代码思路详解
2017/08/29 jQuery
JavaScript正则表达式和级联效果
2017/09/14 Javascript
vue里input根据value改变背景色的实例
2018/09/29 Javascript
js的对象与函数详解
2019/01/21 Javascript
微信小程序官方动态自定义底部tabBar的例子
2019/09/04 Javascript
[06:35]2014DOTA2国际邀请赛 老男孩梦圆西雅图中国军团世界最强
2014/07/22 DOTA
[55:44]完美世界DOTA2联赛决赛 FTD vs Phoenix 第二场 11.08
2020/11/11 DOTA
Python 文件和输入输出小结
2013/10/09 Python
python实现图片处理和特征提取详解
2017/11/13 Python
一文秒懂python读写csv xml json文件各种骚操作
2019/07/04 Python
python2使用bs4爬取腾讯社招过程解析
2019/08/14 Python
numpy按列连接两个维数不同的数组方式
2019/12/06 Python
Python 利用flask搭建一个共享服务器的步骤
2020/12/05 Python
CSS3弹性盒模型开发笔记(三)
2016/04/26 HTML / CSS
致400米运动员广播稿
2014/02/07 职场文书
百日安全活动总结
2014/05/04 职场文书
个人批评与自我批评发言稿
2014/09/28 职场文书
解决Mysql报错 Table 'mysql.user' doesn't exist
2022/05/06 MySQL
聊聊配置 Nginx 访问与错误日志的问题
2022/05/25 Servers