为什么选择gRPC和gRPC-gateway
人应该积极,但牛马应当消极,在能偷懒的地方决不含糊。
gRPC
protoc | 编译.proto | github.com/protocolbuf... |
protoc-gen-go | 转译Go数据结构 | github.com/protocolbuf... |
protoc-gen-go-grpc | 生成Go服务代码 | github.com/grpc/grpc-g... |
gRPC-gateway
protoc-gen-grpc-gateway | 生成http反向代理 | github.com/grpc-ecosys... |
protoc-gen-openapiv2 | 生成接口文档 | github.com/grpc-ecosys... |
buf | 编译.proto | github.com/bufbuild/bu... |
protoc-gen-buf-lint | linter | github.com/bufbuild/bu... |
protoc-gen-buf-breaking | 兼容性检查 | github.com/bufbuild/bu... |
-
每个仓库下的 Releases 标签内含有其 binary 文件,下载即可
-
按照仓库各自的 Readme 编译安装亦可
从零开始的工程
按照 project-layout 指引建立适合的简化项目结构,后续在需要的时候逐步补上即可
bash
├── api # 服务接口, 例如 http, grpc, api文档
├── build # 部署脚本, 例如编译脚本
├── cmd # app启动文件
├── configs # 配置文件
├── internal # 工程相关编码文件, 业务代码
└── pkg # 通用代码, 例如公用的库
什么是gRPC-gateway?
按照官方的说法,gateway可以同时暴露 gRPC 和 RESTful HTTP 的 API 提供服务,先上图
什么是RPC?
假设在代码中有一个打印 Hello World! 的 echo 函数,本地调用的代码是长这样的:
go
package main
import "fmt"
func main() {
fmt.Println(echo("Hello, World!"))
}
func echo(request string) (response string) {
return request
}
现在假设本地的机器的IP是 192.168.1.100 ,另一台IP是 192.168.1.101 的机器充当服务器,如果有方式能把echo函数放到服务器中运行,然后能以类似本地调用的方式去使用:
go
func main() {
// 调用在服务器上的echo函数
// 格式: IP + Port + 函数名 + 实参
response := 192.168.1.100:8080["echo"]["Hello, World!"]
fmt.Println(response)
}
对于使用方来说,把远端的代码称为服务,实参称为请求参数,返回值称为响应,现在使用格式:(服务器地址+服务名+请求参数)进行调用获取返回响应,这个过程就是RPC
什么是proto?
从上面知道rpc的调用格式(服务名+请求参数)-> 响应,proto就是用来描述这种格式的,如果把调用echo服务的过程翻译成proto,代码是长这样的:
protobuf
syntax = "proto3";
message EchoRequest {
string value = 1;
}
message EchoResponse {
string value = 1;
}
service EchoService {
rpc Echo(EchoRequest) returns (EchoResponse) {}
}
-
EchoRequest:请求参数,相当于Echo函数的实参
-
EchoResponse:服务响应,相当于Echo函数的返回值
-
EchoService:Echo函数本体
go
// 调用的伪代码
func main() {
// 格式:IP + Port + 服务名 + API + 请求参数
response := 192.168.1.101:8080["EchoService"]["Echo"]["Hello, World!"]
}
对于用proto描述的API,我们期望以这样的形式去使用:(服务器地址+服务名+API+请求参数)-> 服务响应
回到gRPC-gateway
假设你是调用者,现在需要通过HTTP的方式请求Echo服务,从发出请求到返回响应,大致是这样的:
请求 => 过滤器链(流量控制,鉴权,日志,异常处理...)=> url路由表 => 请求参数验证 => 具体服务 => 返回响应
对于gRPC的服务也是同理,流程还需要同样来一遍,意味着要维护流程一致,只是协议不同的两个服务
gRPC-gateway做了什么?
通过protobuf的自定义option实现了一个网关,服务端同时开启gRPC 和HTTP 1.1服务,HTTP服务接收客户端请求后转换为grpc请求数据,获取响应后转为json数据返回给客户端。
什么意思?
-
proto描述了gRPC请求参数,api服务,返回参数,定义了使用格式
-
proto还能配合一些额外自定义配置描述HTTP的url
-
gateway能把HTTP的url映射到gRPC的具体服务一一对应起来
gRPC-gateway就是通过代码生成的方式把proto描述的服务翻译成HTTP服务的接口层和gRPC的接口层。
还是以echo服务为例,请求url是:POST /v1/example/echo,请求body是:"Hello, World!",到达HTTP服务时,HTTP处理过程中会把自己伪装成gRPC客户端,把HTTP的请求参数转换成gRPC请求格式,查询url对应到gRPC服务签名是rpc Echo(EchoRequest) returns (EchoResponse),转发到gRPC的具体服务处理返回响应,把从gRPC返回的响应包装成HTTP需要的json数据,最后返回给调用者。
这样做有什么用?
一般对于现在的系统架构来说,服务器与服务器之间是通过RPC交互,对外提供服务通过HTTP-JSON交互,基于这个前提,同时维护HTTP和GRPC是件很痛苦的事情。
-
基础痛苦:HTTP服务和gRPC的过滤器链大多数都是重复的,同样的逻辑代码要码两次。
-
双倍痛苦:API需要修改请求和响应参数的时候,同样要修改两次,同理还有API文档。
-
三倍痛苦:假设系统是微服务架构时,多个服务由多语言编写,接口层的工作量巨大。
当不巧所有工作都是由你负责,包括还有前端的时候,面具得戴上。
如何使用?代码怎么写?
-
编写.proto文件,描述请求参数,api服务,返回参数
-
在.proto文件中引入http.proto,描述HTTP的url如何映射到gRPC服务
-
在使用http.proto时,同时指定HTTP的请求参数如何转换到gRPC的请求参数
-
通过protoc或者buf配合protoc-gen-go,protoc-gen-go-grpc,protoc-gen-grpc-gateway编译.proto生成HTTP和gRPC接口层代码
-
根据.proto定义的api服务签名,实现业务逻辑
-
实例化gRPC服务绑定ip+port
-
实例化HTTP服务绑定ip+port,并伪装成gRPC客户端