1 ProtoBuf入门
1.1 简介
ProtoBuf全称ProtoBuffer,是微服务框架中使用的一种数据交换协议工具,用于网络通信和数据交换等场景。
与json和xml相比,json和xml是基于文本格式,而ProtoBuf是基于二进制格式,所以在ProtoBuf比json和xml更小(3-10倍),更快(20—100倍),更简单。
1.2 安装
1.2.1 安装ProtoBuf
官网下载地址:https://github.com/protocolbuffers/ProtoBuf/releases
下载之后需要将protoc所在的bin目录添加到电脑的环境变量中。
Mac电脑可以直接使用brew安装。
1.2.2 安装go语言工具包
默认安装的protoc可以将proto文件编译为java,C++等语言的代码文件,但是需要编译为go语言的,需要下载go语言的工具包。
// 下载go语言工具包命令
go get github.com/golang/protobuf/protoc-gen-go
1.3 使用
1.3.1 编写proto代码
syntax = "proto3"; // 定义proto版本号
package hello; // 定义包名,用于proto
option go_package = "./;hello"; // 定义go包名,用于生成.pd.go文件
message Say{ // 定义消息体
int64 id = 1;
string hello = 2;
repeated string word = 3;
}
1.3.2 生成go代码
proto代码可以直接生成对应的go代码,go代码的后缀对应为pb.go,切换到proto所在目录使用以下命令即可生成go代码。
protoc --go_out=. hello.proto
生成go代码之后目录如下所示:
2 ProtoBuf进阶
ProtoBuf和go语言最大的区别在于ProtoBuf语句需要在结尾添加分号。
2.1 关键字
- syntax:是必须写的,而且要定义在第一行;目前proto3是主流,不写默认使用proto2
- package:定义我们proto文件的包名
- option go_package:定义生成的pb.go的包名,我们通常在proto文件中定义。如果不在proto文件中定义,也可以在使用protoc生成代码时指定pb.go文件的包名
- message:非常重要,用于定义消息结构体。
2.2 消息体编译
写完ProtoBuf代码之后,可以通过protoc命令将ProtoBuf编译成各种语言版本的代码。
编译器命令如下:
protoc [OPTION] PROTO_FILES
OPTION:命令选项,常用的OPTION选项如下:
--go_out=OUT_DIR 指定代码生成目录,生成 Go 代码
--cpp_out=OUT_DIR 指定代码生成目录,生成 C++ 代码
--csharp_out=OUT_DIR 指定代码生成目录,生成 C# 代码
--java_out=OUT_DIR 指定代码生成目录,生成 java 代码
--js_out=OUT_DIR 指定代码生成目录,生成 javascript 代码
--objc_out=OUT_DIR 指定代码生成目录,生成 Objective C 代码
--php_out=OUT_DIR 指定代码生成目录,生成 php 代码
--python_out=OUT_DIR 指定代码生成目录,生成 python 代码
--ruby_out=OUT_DIR 指定代码生成目录,生成 ruby 代码
PROTO_FILES:要编译的proto消息定义文件,支持多个
2.3 消息message
message(消息),在ProtoBuf中表示定义的数据结构,类似于go中定义的结构体。
2.3.1 使用
语法:
message 消息名 {
消息体
}
示例:
message Request {
string query = 1;
int32 page = 2;
int32 limit = 3;
}
示例中定义了一个Request类型的消息,其中有三个字段,字符串类型的query、int32类型的page和limit。
2.3.2 字段类型
ProtoBuf支持多种数据类型,与go语言类型对应如下:
.proto Type | Go Type |
---|---|
double | float64 |
float | float32 |
int32 | int32 |
uint32 | uint32 |
uint64 | uint64 |
sint32 | int32 |
sint64 | int64 |
fixed32 | uint32 |
fixed64 | uint64 |
sfixed32 | int32 |
sfixed64 | int64 |
bool | bool |
string | string |
bytes | []byte |
2.3.3 分配标识号
分配标识号就是在消息体定义中每个字段后面的唯一的数字,这个就是标识号,在同一个消息体中,每个字段的标识号都是唯一的,不同消息体可以拥有相同的标识号。
标识号的主要作用是用来在消息的二进制格式中标识各个字段的,一旦指定之后就不能修改。
2.3.4 保留标识号
在标识号的使用中,[1,15]之间的标识号在编码的时候占用一个字节,[16,2047]之间的标识号占用两个字节,所以在使用的时候应该为将来有可能添加的、频繁出现的字段预留一些标识号。
想要预留一些标识号,留给以后使用,可以使用关键字reserved:
message Test {
reserved 2, 5, 7 to 10; // 保留2,5,7到10这些标识号
}
如果在代码中使用了这些预留的标识号,那么编译器无法编译通过。
2.4 数组类型
在ProtoBuf中定义数组类型,是通过在字段前面增加repeated关键字实现的,标记之后就代表当前字段是一个数组。
示例:
message Msg {
// 整形数组
repeated int64 arrays = 1;
// 字符串数组
repeated string names = 2;
}
2.5 枚举类型
枚举在Java中是很常见的一种类型,但是go语言是不直接支持枚举的,而且也没有Enum关键字。
枚举的应用场景:当定义一个消息类型时,有时候某字段只能是某一种类型的某一个值,这时候就需要使用到枚举,例如定义一个性别字段,那么这个字段就只能为性别类型中的男或者女。
代码示例:
syntax = "proto3";//指定版本信息,非注释的第一行
enum SexType //枚举消息类型,使用enum关键词定义,一个性别类型的枚举类型
{
UNKONW = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
MALE = 1; //1男
FEMALE = 2; //2女
}
// 定义一个用户消息
message UserInfo
{
string name = 1; // 姓名字段
SexType sex = 2; // 性别字段,使用SexType枚举类型,表示该字段只能为SexType中的0,1,2三个中的一个
}
2.6 消息嵌套
在实际的开发过程中,可能需要定义很多的proto文件,如果需要做到消息的复用,则需要使用消息嵌套。
在go语言的开发中,经常会使用到结构体的嵌套,例如一个结构体内有另外一个结构体的属性,在ProtoBuf中同样支持消息的嵌套,可以在一个消息中嵌套另外一个消息,字段类型可以是另外一个消息类型。
2.6.1 引用其他消息类型
// 定义Article消息
message Article {
string url = 1;
string title = 2;
repeated string tags = 3; // 字符串数组类型
}
// 定义ListArticle消息
message ListArticle {
// 引用上面定义的Article消息类型,作为articles字段的类型
repeated Article articles = 1; // repeated关键词标记,说明articles字段是一个数组
}
2.6.2 消息嵌套
message ListArticle {
// 嵌套消息定义
message Article {
string url = 1;
string title = 2;
repeated string tags = 3;
}
// 引用嵌套的消息定义
repeated Article articles = 1;
}
2.6.3 import
在实际开发中,可能需要定义很多消息,如果都写在一个proto文件中,不方便维护。可以将消息写在不同的proto文件中,需要的时候通过import关键字导入其他proto文件中定义的消息。
article.proto
syntax = "proto3";
package nesting;
option go_package = "./;article";
message Article {
string url = 1;
string title = 2;
repeated string tags = 3; // 字符串数组类型
}
list_article.proto
syntax = "proto3";
// 导入Article消息定义
import "article.proto";
package nesting;
option go_package = "./;article";
// 定义ListArticle消息
message ListArticle {
// 使用导入的Result消息
repeated Article articles = 1;
}
2.7 Map类型
在go语言开发中,Map是常用的一种类型,在ProtoBuf中同样是支持Map类型的。
2.7.1 Map语法
map<key_type, value_type> map_field = N;
key_type
可以是任何整数或字符串类型(除浮点类型和字节之外的任何标量类型)。- 注意:枚举不是有效的
key_type
。 value_type
可以是除另一个映射之外的任何类型。- Map 字段不能使用
repeated
关键字修饰。
2.7.2 map的例子
syntax = "proto3";
package map;
option go_package = "./;score";
message Student{
int64 id = 1; //id
string name = 2; //学生姓名
map<string, int32> score = 3; //学科 分数的map
}
2.3实战
2.3.1 定义proto文件
student_info.proto
syntax = "proto3";
import "address.proto";
package stu;
option go_package = "./;student";
enum SexType {
UNKNOWN = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
MALE = 1; //1男
FEMALE = 2; //2女
}
message StudentInfo{
int64 id = 1;
string name = 2;
SexType sex = 3;
repeated string hobby = 4;
map<string, int32> score = 5;
Address address = 6;
}
address.proto
syntax = "proto3";
package stu;
option go_package = "./;student";
message Address{
reserved 3 to 8;
string country = 1;
string city = 2;
}
2.3.2 生成go代码
protoc --go_out=. student_info.proto address.proto
2.3.3 main代码运行
package main
import (
student "example.com/m/proto"
"fmt"
"github.com/golang/protobuf/proto"
)
func main() {
stu := student.StudentInfo{}
stu.Id = 1
stu.Name = "kun"
stu.Sex = student.SexType(1)
hobby := []string{"唱", "跳", "Rap", "篮球"}
stu.Hobby = hobby
score := map[string]int32{"语文": 80, "数学": 90}
stu.Score = score
stu.Address = &student.Address{Country: "中国", City: "北京"}
fmt.Println("原结构体字符串:", stu.String())
marshal, err := proto.Marshal(&stu)
if err != nil {
return
}
fmt.Println("结构体转二进制:", marshal)
newStu := student.StudentInfo{}
err = proto.Unmarshal(marshal, &newStu)
if err != nil {
return
}
fmt.Println("二进制转结构体:", newStu.String())
}
运行结果:
3 案例
3.1 安装插件
// 之前在proto中安装过protoc-gen-go,这里安装protoc-gen-go-grpc就行
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
3.2 编写proto文件
新建一个go项目,新建proto文件夹,增加以下代码:
syntax = "proto3";
package hello;
option go_package = "../hello";
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
使用插件生成Go语言代码
protoc --go_out=./ --go-grpc_out=./ helloworld.proto
生成之后代码如下所示:
注意:生成代码之后如果代码爆红,可以使用go mod tidy
更新依赖
3.3 实现接口
在自动生成的helloworld_grpc.pb.go文件中有这样一段代码:
// GreeterServer is the server API for Greeter service.
// All implementations must embed UnimplementedGreeterServer
// for forward compatibility
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
mustEmbedUnimplementedGreeterServer()
}
// UnimplementedGreeterServer must be embedded to have forward compatible implementations.
type UnimplementedGreeterServer struct {
}
func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}
这一段代码上面部分的GreeterServer
接口中的SayHello
就是我们之前在proto中定义的服务中的方法,而下面的代码就是针对上面这个接口的实现。
我们也需要实现这个接口,并重写方法,在hello文件夹下新建一个helloworld.go文件:
type HelloWorldServer struct{}
func (HelloWorldServer) SayHello(ctx context.Context, request *HelloRequest) (*HelloReply, error) {
return &HelloReply{Message: "hello " + request.Name}, nil
}
func (HelloWorldServer) mustEmbedUnimplementedGreeterServer() {
//TODO implement me
panic("implement me")
}
3.4 服务端代码
新建server文件夹,在文件夹中新建server.go文件:
func main() {
// tcp协议监听指定端口号
listen, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatalln("listen error", err)
}
// 实例化gRPC服务
server := grpc.NewServer()
// 服务注册
hello.RegisterGreeterServer(server, hello.HelloWorldServer{})
// 启动服务
err = server.Serve(listen)
if err != nil {
log.Fatalln("server error", err)
}
}
3.5 客户端代码
新建client文件夹,在文件夹中新建client.go文件:
func main() {
// 通过gRPC.Dial()方法建立服务连接
conn, err := grpc.Dial("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalln("connection error", err)
}
// 连接关闭
defer conn.Close()
client := hello.NewGreeterClient(conn)
// 客户端调用在proto中定义的SayHello()rpc方法,发起请求,接收服务端响应
sayHello, err := client.SayHello(context.Background(), &hello.HelloRequest{Name: "lee"})
if err != nil {
log.Fatalln("say hello error", err)
}
fmt.Println(sayHello)
}
3.6 运行
首先运行server端代码,等启动没有报错之后再运行客户端代码,可以在客户端中输出以下内容则成功。
4 Stream
在HTTP/1.1时代,同一个时刻只能对一个请求进行处理或者响应,也就是说,下一个请求必须等当前请求处理完之后才能继续进行。
HTTP/1.1需要注意的是,在服务端没有response的时候,客户端是可以发起多个request的,但服务端依旧是顺序对请求进行处理,并按照收到的请求的次序予以返回。
HTTP/2时代,多路复用的特性让一次同时处理多个请求成为了现实,并且同一个TCP通道中的请求不分先后、不会堵塞,HTTP/2中引入了流(Stream)和帧(Frame)的概念,当TCP通道简历之后,后续的所有操作都是以流的方式发送的,而二进制帧则是组成流的最小单位,属于协议层上的流式传输。
HTTP/2在一个TCP连接的基础上虚拟出多个Stream,Stream之间可以并发的请求和处理,并且HTTP/2以二进制帧(Frame)的方式进行数据传送,并引入了头部压缩(HPACK),大大提升了交互效率。
Stream
关键字,当该关键字修饰参数时,表示这是一个客户端流式的gRPC接口;当该参数修饰返回值时,表示这是一个服务端流式的gRPC接口;当该关键字同时修饰参数和返回值时,表示这是一个双向流式的gRPC接口。
// 普通 RPC
rpc Simple(Request) returns (Response);
// 客户端流式 RPC
rpc ClientStream(stream Request) returns (Response);
// 服务端流式 RPC
rpc Simple(Request) returns (stream Response);
// 双向流式 RPC
rpc Simple(stream Request) returns (stream Response);
4.1 客户端流
在之前的helloworld.proto
文件中添加客户端流式 rpc 接口:
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc ClientStream (stream HelloRequest) returns (HelloReply){}
}
执行命令重新生成go文件:
protoc --go_out=./ --go-grpc_out=./ helloworld.proto
在helloworld.go文件中实现ClientStream
:
func (HelloWorldServer) ClientStream(stream Greeter_ClientStreamServer) error {
count := 0
for {
// 获取客户端流
recv, err := stream.Recv()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
fmt.Println("接收:", recv, count)
count++
// 当次数大于10时,发送响应并关闭流
if count > 10 {
res := HelloReply{Message: "success " + strconv.Itoa(count)}
err := stream.SendAndClose(&res)
if err != nil {
return err
}
return nil
}
}
}
修改客户端代码:
func main() {
// 通过gRPC.Dial()方法建立服务连接
conn, err := grpc.Dial("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalln("connection error", err)
}
// 连接关闭
defer conn.Close()
// 实例化客户端连接
client := hello.NewGreeterClient(conn)
// 客户端调用SayHelloStream接口获取流
stream, err := client.ClientStream(context.Background())
if err != nil {
log.Fatalln("connection error", err)
}
// 使用通道来判断是否接收到数据
res := make(chan struct{}, 1)
go request(stream, res)
// 当通道中接收到数据则打印出服务端响应的内容
select {
case <-res:
recv, err := stream.CloseAndRecv()
if err != nil {
log.Fatalln(err)
}
fmt.Println("response:", recv.Message)
}
}
// 客户端朝服务端不停的发送消息
func request(stream hello.Greeter_ClientStreamClient, res chan struct{}) {
count := 0
for {
// 通过流发送消息到服务端
err := stream.Send(&hello.HelloRequest{Name: "lee"})
if err != nil {
log.Fatalln(err)
}
time.Sleep(time.Millisecond * 100)
count++
// 发送个数大于10时,停止发送,并写入内容到res通道中
if count > 10 {
res <- struct{}{}
break
}
}
}
运行之后服务端和客户端效果如下:
4.2 服务端流
新增服务端流的rpc接口:
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc ClientStream (stream HelloRequest) returns (HelloReply){}
rpc ServerStream (HelloRequest) returns (stream HelloReply){}
}
在helloworld.go文件中实现ServerStream
:
func (HelloWorldServer) ServerStream(request *HelloRequest, stream Greeter_ServerStreamServer) error {
count := 0
// 不停发送数据给客户端
for {
err := stream.Send(&HelloReply{Message: "hello " + strconv.Itoa(count)})
if err != nil {
return err
}
count++
time.Sleep(time.Millisecond * 100)
if count > 10 {
break
}
}
return nil
}
客户端代码:
func main() {
// 通过gRPC.Dial()方法建立服务连接
conn, err := grpc.Dial("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalln("connection error", err)
}
// 连接关闭
defer conn.Close()
// 实例化客户端连接
client := hello.NewGreeterClient(conn)
// 获取到服务端流
stream, err := client.ServerStream(context.Background(), &hello.HelloRequest{Name: "lee"})
if err != nil {
log.Fatalln("stream err", err)
}
// 循环打印出服务端发送的消息
for {
recv, err := stream.Recv()
if err != nil {
if err == io.EOF {
fmt.Println("客户端接收完成")
err := stream.CloseSend()
if err != nil {
log.Fatalln("CloseSend err", err)
}
return
}
log.Fatalln("Recv err", err)
}
fmt.Println("客户端接收到数据:", recv.Message)
}
}
4.3 双向流
新增双向流的rpc接口:
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc ClientStream (stream HelloRequest) returns (HelloReply){}
rpc ServerStream (HelloRequest) returns (stream HelloReply){}
rpc ClientAndServerStream (stream HelloRequest) returns (stream HelloReply){}
}
在helloworld.go文件中实现ClientAndServerStream
:
func (HelloWorldServer) ClientAndServerStream(stream Greeter_ClientAndServerStreamServer) error {
for {
// 接收消息
recv, err := stream.Recv()
if err != nil {
return err
}
fmt.Println("服务端接收到消息:", recv.Name)
time.Sleep(time.Millisecond * 100)
// 发送消息
err = stream.Send(&HelloReply{Message: "hello"})
if err != nil {
return err
}
}
}
客户端代码:
func main() {
// 通过gRPC.Dial()方法建立服务连接
conn, err := grpc.Dial("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalln("connection error", err)
}
// 连接关闭
defer conn.Close()
// 实例化客户端连接
client := hello.NewGreeterClient(conn)
// 调用接口获取服务端流
stream, err := client.ClientAndServerStream(context.Background())
if err != nil {
log.Fatalln(err)
}
for {
// 发送消息
err := stream.Send(&hello.HelloRequest{Name: "lee"})
if err != nil {
log.Fatalln(err)
}
time.Sleep(time.Millisecond * 100)
// 接收消息
recv, err := stream.Recv()
if err != nil {
log.Fatalln(err)
}
fmt.Println("客户端接收消息:", recv.Message)
}
}
本文参与了1024 程序员节活动,欢迎正在阅读的你也加入。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。