Protocol Buffers: Google数据交换格式
gRPC定义一个服务,指定可以被远程实现的方法,有参数和返回类型
- 服务端实现了接口,运行gRPC服务处理客户端的调用
- 客户端有一个stud(不同语言)提供了统一和服务端方一致的方法
1. gRPC背景
- 函数调用 => RPC
- gRPC定义一个服务,指定可以被远程实现的方法,有参数和返回类型;服务端实现了接口,运行gRPC服务处理客户端的调用;客户端有一个stud(不同语言)提供了统一和服务端方一致的方法;
- 图解gRPC: https://www.grpc.io/img/landing-2.svg
- gRPC支持的语言
- 使用Protocol Buffers序列化结构化数据(类似JSON的数据格式),
.proto
文件;pb是作为消息的结构,包含一系列kv项 - 利用
protoc
,pb的编译器,生成指定语言的数据访问类,提供类似name()
,set_name()
访问器,方法被被序列化/解析为源字节 - gRPC使用protoc使用指定gRPC插件生成code:获取客户端和服务端代码,和标准的pb代码赋值、获取消息类型
- pb版本v3,支持新特性,golang/protobuf,推荐使用v3(默认)
1.1. RPC架构和生命周期
- gRPC关键概念,RPC架构和生命周期
- gRPC类似其他RPC,也是定义服务、指定可以被调用的方法,gRPC使用PB作为IDL(接口定义语言),描述服务接口和消息负载
- 定义四类服务方法:
- 简单调用方式(请求服务,获取简单响应)
- 流式RPC服务(请求服务,获取一个流)
- 流式RPC客户端(请求流,服务端读完,应答简单响应)
- 双向流式RPC服务(请求、服务响应都是流)
- 使用API,gRPC通过
protoc
编译机器编译.proto
文件后,生成了客户端和服务端的代码。gRPC用户在客户端调用这些APIs,在服务端定义服务APIs - 在服务端,服务实现了申明的服务,运行gRPC服务来处理调用,gRPC基础设施解析进入的请求,执行服务方法,编译服务的响应;
- 在客户端,客户端有一个叫做
stub
(桩子)作为客户端,实现了和服务端同样的方法;客户端只需要调用这些本地的对象方法,将调用的参数包装在PB消息类型,发送给服务端; - 同步和异步:同步阻塞RPC调用直至服务器响应回来为止;网络本质是异步的,许多情况能够启动RPC而不阻塞当前线程
- 大多数语言都支持同步和异步两者方式
1.2. RPC生命周期
- 简单调用方式:客户端调用方法,通知服务端元数据;服务端可以立马响应元数据,或者等客户端的消息数据;服务端收到客户端请求消息后,处理完后,响应、状态信息,尾随的元数据一起返回;若状态OK客户端完成响应
- 服务端和客户端流式RPC与简单调用流程类似;
- 双向流式RPC,客户端调用后,服务端接收客户端的元数据、方法名、超时限制,服务端可以选择发回响应元数据或者等待客户端开始发送请求;接下来进行全双工的读写;
- gRPC允许客户端指定超时时间:
DEADLINE_EXCEEDED
,服务端可以查询特定的RPC是否超时,或者还剩下多少时间来完成RPC - RPC终止,可能出现服务端响应完成,但客户端RPC超时的情况
- RPC取消,取消会立即终止RPC,无需进一步的处理,取消之前做的更改不会回滚
- 元数据,以键值对提供RPC的调用信息
- 通道,客户端能指定通道参数修改gRPC的默认行为,诸如关闭消息压缩,通道有连接和闲置状态
1.3. gRPC认证
- gRPC被设计与其他各种auth机制协同工作:
SSL/TLS
、Token
- 认证API:凭证类型:通道凭证,调用凭证
- Go不加密和加密
1.3.1. gRPC认证 - 不加密
1
2
3
4
5
6
7
8
9
| // 客户端
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
// error handling omitted
client := pb.NewGreeterClient(conn)
// 服务端
s := grpc.NewServer()
lis, _ := net.Listen("tcp", "localhost:50051")
// error handling omitted
s.Serve(lis)
|
1.3.2. gRPC认证 - 加密
1
2
3
4
5
6
7
8
9
10
11
12
| // 客户端
creds, _ := credentials.NewClientTLSFromFile(certFile, "")
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
// error handling omitted
client := pb.NewGreeterClient(conn)
// 服务端
creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile)
s := grpc.NewServer(grpc.Creds(creds))
lis, _ := net.Listen("tcp", "localhost:50051")
// error handling omitted
s.Serve(lis)
|
1.4. gRPC错误处理
- 标准错误模型:OK状态码,错误状态码
- 标准的错误类型:https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto (涵盖了最常见的错误,以额外错误信息在元数据中提供)
- grpc-web
- 问题:
- 错误扩展在不同语言库中有差异
- 错误无法用于监视
- 可能干扰头阻塞,降低HTTP/2的压缩效率
- 错误细节过大,可能导致协议限制
- 错误状态码: - https://www.grpc.io/docs/guides/error/
- 基准测试
2. PB文件编译
文档推荐:https://developers.google.com/protocol-buffers/
2.1. PB文件编译工具安装
2.1.1. protoc编译器安装,以及protoc-gen-go安装
- C++或Go编译器
- 安装
protoc
:编译器protocol compiler
- 安装Go编译器插件
protoc-gen-go
,用于编译.proto
文件为.go
文件:go get -u github.com/golang/protobuf/protoc-gen-go
,其被安装到$GOPATH/bin
1
2
3
| // Code generated by protoc-gen-go. DO NOT EDIT.
// source: user/addressbook.proto
...
|
- 源
.proto
文件和生成的.pb.go
文件之间存在一对一的关系,但是.pb.go同一Go包中可以包含任意数量的文件。
2.1.2. protoc查看
- 编译输出
.pb.go
后缀文件: protoc --go_out=. *.proto
- 在生成的Go代码中,每个源
.proto文件
都与一个Go包相关联。 - 编译过程中指定额外参数,见示例
- proto当前有proto3(推荐)和proto2两个版本
1
2
3
4
5
6
7
8
| protoc [OPTION] PROTO_FILES
--version 版本信息
-I PATH, --proto_path=PATH: 指定proto源文件(支持多次指定、默认当前路径)
-o FILE: 编译好的二进制文件输出目标
--encode=MESSAGE_TYPE 基于文本消息,写成二进制消息(标准输入和输出)
--decode=MESSAGE_TYPE 基于二进制消息,解码成文本消息(标准输入和输出)
|
2.2. 使用protoc编译.protoc文件
1
2
3
4
5
6
7
8
9
10
11
| // 基于子目录生成
cd /data/github.com/tkstorm/go-gRPC/api/protobuf/idl/search
protoc --go_out=paths=source_relative:. ./serach_result.proto
// 基于上层目录,编译生成
cd ../search
protoc --go_out=paths=source_relative:. ./search/serach_result.proto
// 任意目录生成
cd data/github.com/tkstorm/go-gRPC
protoc -I=./api/protobuf/idl --gofast_out=./api/protobuf/generate ./api/protobuf/idl/search/serach_result.proto
|
3. gRPC使用
- gRPC使用PB即作为IDL,又作为底层消息交换格式
- QuickStart: https://www.grpc.io/docs/quickstart
- gRPC可以用于实现一个异构、分布式服务实现,支持跨平台和编程语言,gRPC客户端对服务端调用类似本地服务一样;
- gRPC默认使用PB用于数据序列结构化数据;利用
.proto
后缀PB文件定义数据结构,同时提供了类似set_name()
和name()
类似功能 - gRPC虽然默认支持PB,但也可以换成其他数据格式如JSON
3.1. 使用PB定义IDL
1
2
3
4
5
| message Person {
string name = 1;
int32 id = 2;
bool has_ponycopter = 3;
}
|
用pb的编译器protoc
生成指定语言数据访问类,提供简单的字段读和写(如set_name()
和name()
)
3.2. Greeter服务定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // The greeter service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
|
gRPC使用protoc附带gRPC指定插件来生成代码
4. gRPC定义4类服务&生命周期
4.1. 普通一元调用
类似普通函数调用,客户端请求获取到相应
- 客户端调用客户端对象,会告知服务端使用该调用的元数据,指定服务名、服务数据、方法名称和超时时间
- 服务端首次会立即发送自己的初始化元数据或等待消息一并应答
- 服务端处理,响应成功或失败
1
2
| rpc SayHello(HelloRequest) returns (HelloResponse){
}
|
4.2. 服务器流式RPC
客户端请求服务后,获得一个流用于读取服务数据,直至流关闭;
类似下载服务
1
2
| rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
|
4.3. 客户端流式RPC
客户端流式RPC,用于发送一系列消息给到服务端;一旦全部发送完毕,等待服务读取消息并处理返回,同时保证了消息的循序
类似HTTP1.1中的管道功能。
1
2
| rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
|
4.4. 双向流式RPC
支持客户端和服务端全双工的独立读写流,互不影响,每个流中的顺序是保障的!
1
2
| rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}
|
5. gRPC其他信息
5.1. 使用API的壳
- 服务端申明方法,运行gRPC服务接收客户端调用
- 客户端具有成为stub的本地对象(标准术语是client)实现了和服务端一样的服务,客户端仅需要调用本地对象,后续gRPC在将请求发送到服务器并返回PB消息
5.2. 同步与异步
- 同步阻塞调用RPC服务是最常见的方式
- 网络本质是异步的,在许多情况下启动RPC不阻止当前线程很有用
5.3. 截止时间&超时限制
- 支持客户端设定截止时间,超时错:
DEADLINE_EXCEEDED
,服务端支持RPC剩余时间查询 - 基于语言不同,截止时间和超时时间都可能会在不同语言有用到
- 即使服务端可能正常响应,但客户端可能由于超时中断了
5.4. 取消请求
gRPC的元数据是以kv
形式存在的,比如认证、版本等信息
5.6. Channel通道
通道提供客户端本地和远程服务端的连接,通道支持消息压缩打开和关闭,同时通道有连接(connected)和闲置(idle)的状态(一些语言支持通道状态的查询)
5.7. gRPC认证
- SSL/TLS
- Token-based authentication with Google
5.8. gRPC错误处理和Debug
// todo
5.9. gRPC压力测试
// todo
5.10. gRPC Over HTTP2
// todo
6. Go gRPC 试验
使用一个简单的路由映射应用,让客户端获取服务相关信息,高效、IDL约定
6.1. 基于.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
| syntax = "proto3";
package helloworld;
// The greeting service definition.(服务定义,指定rpc请求和响应类型,以下都是简单RPC请求和响应,还有流式等RPC模式支持)
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
}
// Sends another greeting
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {
}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
|
6.2. 编译gRPC服务
使用protocol buffer编译器 - proto
生成服务和客户端的.pb.go
包代码
1
| $ protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld
|
编译后的.pb.go
文件实现了:
- 用于填充,序列化和检索我们的请求和响应消息类型的所有协议缓冲区代码
- 客户端使用服务中定义的方法调用的接口类型(或stub)
- 服务端要实现的接口类型,以及服务定义的方法
6.3. 初始化客户端和服务端
6.3.1. 服务端
- 实施根据我们的服务定义生成的服务接口:完成我们服务的实际“工作”。
- 运行gRPC服务器以侦听来自客户端的请求,并将其分派到正确的服务实现。
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
| //go:generate protoc -I ../helloworld --go_out=plugins=grpc:../helloworld ../helloworld/helloworld.proto
// Package main implements a server for Greeter service.
package main
import (
"context"
"log"
"net"
pb "go-grpc/examples/helloworld/helloworld"
"google.golang.org/grpc"
)
const (
port = ":50051"
)
// 实现一个GreeterServer服务
type server struct{}
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.Name)
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}
func main() {
// TCP网络监听
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// grpc服务创建
s := grpc.NewServer()
// Greeter服务注册
pb.RegisterGreeterServer(s, &server{})
// 服务绑定&提供服务,等待gRPC客户端的请求
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
|
6.3.2. 客户端
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
48
49
50
| // Package main implements a client for Greeter service.
package main
import (
"context"
"log"
"os"
"time"
"google.golang.org/grpc"
pb "go-grpc/examples/helloworld/helloworld"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func main() {
// 初始化一个gRPC通路
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 初始化一个Greeter客户端
c := pb.NewGreeterClient(conn)
// 请求参数
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
// 设定gRPC服务请求的上下设定(超时设定)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Greeter客户端发起SayHello RPC调用,同步阻塞得到响应
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
// 打印响应内容Message
log.Printf("Greeting: %s", r.Message)
// Greeter客户端调用另一个RPC请求调用,串行化的同步阻塞
r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
|
7. Service定义 - 4类RPC模式
routeguide/route_guide.proto
gRPC服务定义
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
| // 版本
syntax = "proto3";
// 附加属性
option java_multiple_files = true;
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
package routeguide;
// 路线导航服务接口(4类)
service RouteGuide {
// 第一类: A simple RPC.
//
// 获取给定位置特征描述,如果指定位置没有特征描述,则返回空
rpc GetFeature(Point) returns (Feature) {}
// 第二类,服务端流下发:A server-to-client streaming RPC.
//
// 在矩形区域内(可能包含大量特征),获取可用的特征,结果是流(stream关键字说明),而非一次返回
rpc ListFeatures(Rectangle) returns (stream Feature) {}
// 第三类,客户端流上报:A client-to-server streaming RPC.
//
// 服务接受一个位置点的流移动,当形成结束时候返回路径汇总
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// 第四类,双向流:A Bidirectional streaming RPC.
//
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
// A latitude-longitude rectangle, represented as two diagonally opposite
// points "lo" and "hi".
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
// A feature names something at a given point.
//
// If a feature could not be named, the name is empty.
message Feature {
// The name of the feature.
string name = 1;
// The point where the feature is detected.
Point location = 2;
}
// A RouteNote is a message sent while at a given point.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number of
// detected features, and the total distance covered as the cumulative sum of
// the distance between each point.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
|