GRPC

RPC

gRPC

Protobuf

特点

1、操作更简单。

2、序列化后生成的代码体积更小

3、解析速度更快。(比xml快两个数量级)

4、与XML相比,protobuf的缺点是不易读。众所周知,XML是一种自描述语言,一看就可知道其作用,见文知意。而protobuf序列化后是一串二进制代码,如果没有对应的协议格式(即.proto文件),想要读懂它难如登天。另外,如果目标是一种基于文版的标签式文档(如html),则XML更具优势

关于protobuf的历史及其特点,可参考Google网站:点击打开链接

语法规则

protobuf协议的文件后缀名为.proto。一个简单的protobuf协议如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
message Person {
//标识符| 类型 | 字段名字| 标号
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}

1、标识符

protobuf协议的标识符为message或enum,如示例中的Person和PhoneType。message标识一条消息,enum标识一个枚举类型。使用protobuf编译器将协议文件编译后,message和enum都会生成一个类。

2、字段

协议字段格式如下:

1
role type name = tag [default value];

role有三种取值:

required:表示该字段必须有值,不能为空,否则message被认为是未初始化的。如果试图build一个未初始化的message将会抛出RuntimeException。解析未初始化的message会抛出IOException。

optional:表示该段为可选值,可以不进行设置。如果不设置,会设置一个默认值。可以设置一个默认值,正如示例中的type字段。否则使用系统默认值,数字类型默认为0;字符串类型默认为空字符串;逻辑类型默认为false;内部自定义的message,默认值通常是message的实例或原型。

repeated:表示该字段可以重复,可等同于动态数组。

注意:使用required字段一定要小心,因为该字段是永久性的。如果以后因为某种原因,想不用该字段,或者要将该字段改成optional或者repeated,那么使用旧接口读取新的协议时,如果发现没有该字段,他们会认为该字段是不完整的,会拒接接收该消息,或者直接丢弃。

3、编码

wireType 编码方式 编码长度 储存方式 代码数据类型
0 Varint 变长(1-10个字节) T-V int32,int64,uint32,uint64,bool,enum (负数使用sint32,sint64,)
1 64-bit 固定8字节 T-V fixed64,sfixed64,double
2 Length-delimi 变长 T-L-V String
3 Start group 弃用 弃用 弃用
4 End group 弃用 弃用 弃用
5 32-bit 固定4字节 T-V Fixed32,sfixed32,float

T - V 编码举例:

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
message request {
int63 user_id = 1; // tagNum = 1, wireType = 0,
}

{"user_id":2}
json需要: 14个字符 112字节
protobuf: 2个字节 相差52倍

假设 value为 2, 则编码出的T-V为:
+-----+---+-----------------+
|00001|000|00000010|
+-----+---+-----------------+
tagNum type data

{"user_id":300}
json需要: 16个字符 128字节
protobuf: 3个字节 差42倍

假设 value为 300, 则编码出的T-V为:
300(00000001 00101100)
第一个字节 第二 第三
+-----+---+-----------------------+
|00001|000| 10101100 00000010| 下个T-V
+-----+---+-----------------------+
tagNum type data

Tag高位=0: 一个byte
data的第一个字节最高位为1,说明下一个字节还要继续读

T - L - V 就是在上面的基础上增加了length,用来表达变长的内容:

嵌套对象编码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
message request {
User user = 1; // tagNum = 1, wireType = 2,
}
message User {
int64 user_id = 1; // tagNum = 1
}

假设 request = { user_id: 2}, 则编码出的T-L-V为:
Tag length value
Tag value
+---------+--------+---------+---------
|00001010 |00000010|000010000|00000010|
1<<3 | 2 2 byte 1<<3 | 0 2

通过解request 知道第一个字段是User,再拿到第一个字段的value去解User,
知道User第一个字段是int64,解析出data为2。 一个嵌套对象即解析完毕

案例

流程:

  1. 编写.proto描述文件
  2. 编译生成.pb.go文件
  3. 服务端实现约定的接口并提供服务
  4. 客户端按照约定调用.pb.go文件中的方法请求服务

项目结构:

1
2
3
4
5
6
7
8
9
|—— hello/
|—— client/
|—— main.go // 客户端
|—— server/
|—— main.go // 服务端
|—— proto/
|—— hello/
|—— hello.proto // proto描述文件
|—— hello.pb.go // proto编译后文件

编写描述文件:hello.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syntax = "proto3"; // 指定proto版本
package hello; // 指定默认包名

// 指定golang包名
option go_package = "./hello";

// 定义Hello服务
service Hello {
// 定义SayHello方法
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}

// HelloRequest 请求结构
message HelloRequest {
string name = 1;
}

// HelloResponse 响应结构
message HelloResponse {
string message = 1;
}

hello.proto文件中定义了一个Hello Service,该服务包含一个SayHello方法,同时声明了HelloRequestHelloResponse消息结构用于请求和响应。客户端使用HelloRequest参数调用SayHello方法请求服务端,服务端响应HelloResponse消息。一个最简单的服务就定义好了。

编译生成.pb.go文件

1
2
3
4
$ cd proto/hello

# 编译hello.proto
$ protoc -I . --go_out=plugins=grpc:. ./hello.proto

在当前目录内生成的hello.pb.go文件,按照.proto文件中的说明,包含服务端接口HelloServer描述,客户端接口及实现HelloClient,及HelloRequestHelloResponse结构体。

注意:不要手动编辑该文件

实现服务端接口 server/main.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"net"

pb "rpc-demo/proto/hello" // 引入编译生成的包
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)

const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)

// 定义helloService并实现约定的接口
type helloService struct{}

// HelloService Hello服务
var HelloService = helloService{}

// SayHello 实现Hello服务接口
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
resp := new(pb.HelloResponse)
resp.Message = fmt.Sprintf("Hello %s.", in.Name)

return resp, nil
}

func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
grpclog.Fatalf("Failed to listen: %v", err)
}

// 实例化grpc Server
s := grpc.NewServer()

// 注册HelloService
pb.RegisterHelloServer(s, HelloService)

grpclog.Println("Listen on " + Address)
s.Serve(listen)
}

服务端引入编译后的proto包,定义一个空结构用于实现约定的接口,接口描述可以查看hello.pb.go文件中的HelloServer接口描述。实例化grpc Server并注册HelloService,开始提供服务。

运行:

1
2
$ go run main.go
Listen on 127.0.0.1:50052 //服务端已开启并监听50052端

实现客户端调用 client/main.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
30
31
32
33
34
35
package main

import (
pb "rpc-demo/proto/hello" // 引入proto包
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)

const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)

func main() {
// 连接
conn, err := grpc.Dial(Address, grpc.WithInsecure())
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()

// 初始化客户端
c := pb.NewHelloClient(conn)

// 调用方法
req := &pb.HelloRequest{Name: "gRPC"}
res, err := c.SayHello(context.Background(), req)

if err != nil {
grpclog.Fatalln(err)
}

grpclog.Println(res.Message)
}

客户端初始化连接后直接调用hello.pb.go中实现的SayHello方法,即可向服务端发起请求,使用姿势就像调用本地方法一样。

运行:

1
2
$ go run main.go
Hello gRPC. // 接收到服务端响应

last update time 2022-03-26