关于 gRPC
gRPC 是一个高性能、通用的开源 RPC 框架,其由 Google 主要面向移动应用开发并基于 HTTP/2 协议标准而设计,基于 ProtoBuf (Protocol Buffers) 序列化协议开发,且支持众多开发语言()。
gRPC 提供了一种简单的方法来精确地定义服务和为iOS、Android 和 后台支持服务自动生成可靠性很强的客户端功能库。客户端充分利用高级流和链接功能,从而有助于节省带宽、降低的 TCP 链接次数、节省 CPU 使用、和电池寿命。下图为 gRPC 结构图:
gRPC 具有如下一些重要的特性:
- gRPC 默认通过 Protocol Buffers 来定义接口,可以制定更加严格规范的接口约束;
- 而基于 ProtoBuf 可以将数据序列化为二进制格式,从而大幅度减少数据量,进而大幅度的提升性能;
- 支持流式通信(Streaming),基于 HTTP/2 协议传输可以实现 Streaming 功能模式,可提供更快的响应和更高的性能;
- 支持多种语言,包括:Android Java、C++、C#/.NET、Dart、Go、Python、Web 等等;
关于 grpc-web
gRPC-web 是针对浏览器端的 gRPC 的 Javascript 实现,是需要配合代理来一起使用的。
在官网上的示例都是使用 Envoy 作为代理,本文主要采用 Nginx 作为代理,从 nginx-1.13.10 引入了对 grpc 的支持(grpc_pass),可以代理 gRPC TCP 连接,还可以终止、检查和跟踪 gRPC 的方法调用,可以如下:
- 发布 gRPC 服务,然后使用 NGINX 应用 HTTP/2 TLS 加密、限速、基于 IP 的访问控制列表和日志记录;也可以使用未加密的 HTTP/2(h2c cleartext)或者在服务之上封装 TLS 加密和认证信息;
- 通过单个端点发布多个 gRPC 服务,使用 NGINX 检查并跟踪每个内部服务的调用;
- 使用 Round Robin, Least Connections 或其他方法在集群分配调用,对 gRPC 服务集群进行负载均衡;
下图为一种简单的使用 Nginx 暴露 gRPC 服务的方案:
在客户端和服务端之间的 Nginx 可以为服务端的应用提供一个稳定可靠的网关。
下面通过一个简单完整的 todo 示例来演示 gRPC-web + Nginx + Go 服务端的应用。
创建 todo.proto 协议文件
定义接口和数据类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| syntax = "proto3"; package todo;
message getTodoParams{}
message addTodoParams { string task = 1; }
message deleteTodoParams { string id = 1; }
message todoObject { string id = 1; string task = 2; }
message todoResponse { repeated todoObject todos = 1; }
message deleteResponse { string message = 1; }
service todoService { rpc addTodo(addTodoParams) returns (todoObject) {} rpc deleteTodo(deleteTodoParams) returns (deleteResponse) {} rpc getTodos(getTodoParams) returns (todoResponse) {} }
|
这里使用的是 protobuf 的 v3 版本规范。通过 service 定义了一个 todoService,其中包含了 addTodo、deleteTodo、getTodos 等是三个 RPC 方法,以及通过 message 定义这些方法中请求参数和返回类型。
详细的 protobuf 规范参考
服务端代码生成
本示例的服务端采用 Go,下面使用 protoc 编译生成工具来生成服务端代码,执行如下命令行:
1 2 3 4 5 6 7
| ➜ todo protoc -I ./todo/ --go_out=plugins=grpc:./todo/ ./todo/todo.proto ➜ todo tree . ├── todo │ ├── todo.pb.go │ └── todo.proto └── todo-client
|
在上面的 protoc 命令中,指定 proto 文件的路径在 ./todo/ 下面,且指定了生成的源码文件 todo.pb.go 输出路径也为 ./todo/。
下面我们创建一个 handler.go 来处理这些 RPC 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package todo
import ( "log" "golang.org/x/net/context" "github.com/satori/go.uuid" )
type Server struct { Todos []*TodoObject }
func (s *Server) AddTodo(ctx context.Context, newTodo *AddTodoParams) (*TodoObject, error) { log.Printf("Received new task %s", newTodo.Task) todoObject := &TodoObject{ Id: uuid.NewV1().String(), Task: newTodo.Task, } s.Todos = append(s.Todos, todoObject) return todoObject, nil }
func (s *Server) GetTodos(ctx context.Context, _ *GetTodoParams) (*TodoResponse, error) { log.Printf("get tasks") return &TodoResponse{Todos: s.Todos}, nil }
func (s *Server) DeleteTodo(ctx context.Context, delTodo *DeleteTodoParams) (*DeleteResponse, error) { var updatedTodos []*TodoObject for index, todo := range s.Todos { if(todo.Id == delTodo.Id) { updatedTodos = append(s.Todos[:index], s.Todos[index + 1:]...) break; } } s.Todos = updatedTodos return &DeleteResponse{Message: "success"}, nil }
|
如上示例,在服务运行时的 Todos 数组中存储所有 todo 记录,所有的 RPC 方法都 “绑定” 在 Server 结构体下,其中:
- AddTodo:其中使用了 uuid 包来为每一条 todo 记录生成唯一的标识,并被 append 到 Todos 数组后;
- DeleteTodo:根据 todo.id 找到对应的 todo 记录并从 Todos 列表中移除和更新;
- GetTodo:简单的返回整个 Todo 列表;
Go support for Protocol Buffers
服务端入口程序
我们还需要启动 RPC 服务器的入口程序 server.go,位于 todo 根目录下,此时目录结构如下:
1 2 3 4 5 6 7 8
| ➜ todo tree . ├── server.go ├── todo │ ├── handler.go │ ├── todo.pb.go │ └── todo.proto └── todo-client
|
server.go 的源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package main
import ( "fmt" "log" "net"
"github.com/thearavind/grpc-todo/todo" "google.golang.org/grpc" )
func main() { lis, err: = net.Listen("tcp", fmt.Sprintf(":%d", 50096)) if err != nil { log.Fatalf("failed to listen: %v", err) }
s: = todo.Server {} grpcServer: = grpc.NewServer() todo.RegisterTodoServiceServer(grpcServer, & s)
if err: = grpcServer.Serve(lis); err != nil { log.Fatalf("failed to serve: %s", err) } else { log.Printf("Server started successfully") } }
|
在 main() 主函数入口中,首先启动 TCP 监听,端口为 50096,之后创建 Server 和 grpcServer 实例,并通过 RegisterTodoServiceServer 将我们的 todo 服务注册到这个新建的 grpcServer 上,最后启动 grpc server。
我们可以通过 go run server.go 来启动服务端:
Nginx 代理配置
Nginx 配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| server { listen 9080; server_name localhost;
location / { grpc_set_header Content-Type application/grpc; grpc_pass grpc://localhost:50098;
if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' "*"; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, DELETE'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Transfer-Encoding,Custom-Header-1,X-Accept-Content-Transfer-Encoding,X-Accept-Response-Streaming,X-User-Agent,X-Grpc-Web'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain charset=UTF-8'; add_header 'Content-Length' 0; return 204; }
if ($request_method = 'POST') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Transfer-Encoding,Custom-Header-1,X-Accept-Content-Transfer-Encoding,X-Accept-Response-Streaming,X-User-Agent,X-Grpc-Web'; add_header 'Access-Control-Expose-Headers' 'Content-Transfer-Encoding,Grpc-Message,Grpc-Status'; } } }
|
通过 grpc_pass 来实现 grpc 协议的代理,重定向至服务端 50098 端口。
注意:这里需要通过 grpc_set_header 来设置 Content-Type 为 application/grpc,默认为 application/grpc-web。
使用 vuejs 实现的前端应用
我们通过 vue 来创建一个简单的 todo-client:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ➜ todo vue create todo-client ➜ todo tree -L 2 . ├── go.mod ├── go.sum ├── server.go ├── todo │ ├── handler.go │ ├── todo.pb.go │ └── todo.proto └── todo-client ├── babel.config.js ├── node_modules ├── package.json ├── package-lock.json ├── public ├── README.md └── src
|
下面通过 grpc-web 插件生成 Javascript 的接口代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| ➜ todo protoc -I=./todo todo.proto --js_out=import_style=commonjs:./todo-client/src --grpc-web_out=import_style=commonjs,mode=grpcweb:./todo-client/src ➜ todo tree -L 2 . ├── go.mod ├── go.sum ├── server.go ├── todo │ ├── handler.go │ ├── todo.pb.go │ └── todo.proto └── todo-client ├── babel.config.js ├── node_modules ├── package.json ├── package-lock.json ├── public ├── README.md └── src ├── App.vue ├── assets ├── components ├── main.js ├── todo_grpc_web_pb.js └── todo_pb.js
|
在 todo-client/src 目录下生成 todo_grpc_web_pb.js 和 todo_pb.js 文件,下面我们在 App.vue 中引入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <script> import { addTodoParams, getTodoParams, deleteTodoParams } from "./todo_pb"; import { todoServiceClient } from "./todo_grpc_web_pb";
export default { name: 'App', components: { }, data: function() { return { inputField: "", todos: [] }; }, created: function() { this.client = new todoServiceClient("http://192.168.1.188:9080", null, null); this.getTodos(); }, methods: { getTodos: function() { let getRequest = new getTodoParams(); this.client.getTodos(getRequest, {}, (err, response) => { this.todos = response.toObject().todosList; console.log(this.todos); }); }, addTodo: function() { let request = new addTodoParams(); request.setTask(this.inputField); this.client.addTodo(request, {}, () => { this.inputField = ""; this.getTodos(); }); }, deleteTodo: function(todo) { let deleteRequest = new deleteTodoParams(); deleteRequest.setId(todo.id); this.client.deleteTodo(deleteRequest, {}, (err, response) => { if (response.getMessage() === "success") { this.getTodos(); } }); console.log("todo -> ", todo.id); } } } </script>
|
在 created 阶段创建 todoServiceClient 实例,其中第一个参数为代理的主机地址端口,这里为 Nginx 的(HTTP)服务地址和端口。在 getTodos, addTodo, deleteTodo 等方法中可以创建 RPC 访问的请求参数实例,并且在异步调用后,通过 response 来获取相应信息。如下为示例图:
References