继上篇【gRPC】 在.Net core中使用gRPC了解了gRPC的使用,gRPC基于HTTP/2
和ProtoBuf
,ProtoBuf
就非常有必要好好了解一下了,
那么ProtoBuf
究竟是什么?
ProtoBuf =Google Protocol Buffer
是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
ProtoBuf 是一种数据表达方式,google又说它是数据交换格式,交换 ,也就是说着眼点在数据的传输上。
在数据表达方式上,可以类比json或者xml,但是不同于 json 可以直接被读取解析,需要
1.创建.proto文件,定义数据结构:维护一套对象协议
2.protoc编译.proto文件生成读写接口
3.调用接口实现序列化、反序列化以及读写
gRPC诞生于2015年,而ProtoBuf 最早从2001年开始就在谷歌内部使用了,后者强调的就是简单和性能,在谷歌内部广泛运用于存储和交换各种结构化信息,前者强调的是通信。
1. Message
systax="proto"message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;
}
syntax
:使用的语法,如果不指定,编译器就会按proto2
编译。文件第一行必须是非空,非注释。message
:SearchRequest
定义了3个字段
1.1 指定字段类型
你可以指定字段为标量类型,string,int32,当然也可以指定复合类型,包括枚举等其他一些类型。然后通过protocol buffer编译器去生成不同语言平台的代码。官方给出了相关的proto标量类型与不同语言平台类型映射表。
类型默认值
string>empty string
bytes>empty bytes
bool>false
数字类型>0
enums>定义的第一个枚举值0
枚举类型
message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;enum Corpus {UNIVERSAL = 0;WEB = 1;IMAGES = 2;LOCAL = 3;NEWS = 4;PRODUCTS = 5;VIDEO = 6;}Corpus corpus = 4;
}
每个枚举类型必须包含一个常量,第一个常量必须是0,
message MyMessage1 {enum EnumAllowingAlias {option allow_alias = true;UNKNOWN = 0;STARTED = 1;RUNNING = 1;}
}
allow_alias设置为true,就可以将相同的值分配给不同的枚举常量。
字段为消息
message SearchResponse {repeated Result results = 1;
}message Result {string url = 1;string title = 2;repeated string snippets = 3;
}
ps:repeated
可以用来存放N个相同类型的内容
导入定义
嵌套消息,在当前*.proto文件就如上使用。如果Result在其他.proto文件呢?
import "myproject/other_protos.proto";
字段为嵌套类型
message SearchResponse {message Result {string url = 1;string title = 2;repeated string snippets = 3;}repeated Result results = 1;
}
1.2 分配字段编号
每个字段都有一个独一无二的编号。这些编号作用就大了,因为消息是二进制格式,这些编号就是用来标识消息中的字段,这个可以类比一些通信协议中的编码格式。
1-15需要1个字节编码
16-2047需要两个字节
官方提示,频繁出现的消息元素定义至1-15,将来可能频繁出现的消息元素也要在1-15为其留点位置。
1.3 多个消息类型
message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;
}message SearchResponse {...
}
1.4 注释
/* SearchRequest represents a search query, with pagination options to* indicate which results to include in the response. */message SearchRequest {string query = 1;int32 page_number = 2; // Which page number do we want?int32 result_per_page = 3; // Number of results to return per page.
}
1.5 保留字段
如果通过完全删除字段或注释来更新消息类型。如果以后加载相同.proto的旧版本,可能会导致严重问题,包括数据损坏、隐私漏洞等。
比如删除了编号1 的字段,修改为其他字段,服务端已更新,客户端还是旧版本,客户端和服务端的编号为1的字段不一致。
确保不会发生这种情况的一种方法是指定保留已删除字段的字段号。如果将来有任何用户试图使用这些字段标识符,协议缓冲区编译器将会提示。语法如下:
message Foo {reserved 2, 15, 9 to 11;reserved "foo", "bar";
}
1.6 Protocol buffer 编译器
编译器会根据选择的语言平台生成相应的代码
2.Services
消息类型定义完成后,便是我们使用gRPC的重头戏,Service=RPC(Remote Procedure Call).在proto文件中定义RPC service接口,编译器就会根据你选择的语言平台存根生成服务接口代码。看如下示例:
service SearchService {rpc Search (SearchRequest) returns (SearchResponse);
}
message SearchRequest {string query = 1;int32 page_number = 2;int32 result_per_page = 3;enum Corpus {UNIVERSAL = 0;WEB = 1;IMAGES = 2;LOCAL = 3;NEWS = 4;PRODUCTS = 5;VIDEO = 6;}Corpus corpus = 4;
}
message SearchResponse {message Result {string url = 1;string title = 2;repeated string snippets = 3;}repeated Result results = 1;
}
service
-关键字SearchService
-服务名Search
-方法名SearchRequest
-传入的参数SearchResponse
-返回的参数
3.Packages
您可以向.proto文件添加一个包说明符,当然这个Packages是可选,主要是为了防止message 之间的命名冲突。
package foo.bar;
message Open { ... }
在C#中,除非在.proto文件中显式地指明选项csharp_namespace
,否则包名就会在转换为PascalCase格式后,作为名称空间。更多其他语言参考官方文档说明。
4.Options
4.1 文件级别
顶级,不在任何消息,枚举或者服务的定义
option csharp_namespace = "GrpcService";package greet;
这个就是生成代码时命名空间(java就是包嘛),如果不指定csharp_namespace,如上描述,命名空间就会取package的名称:greet。
4.2 消息级别
仅在消息定义内部
4.3 字段级别
仅在字段定义内部
4.4 类型级别
枚举类型,枚举值,服务类型,服务方法,但是目前这个级别的还没啥用,可能未来为了涌现的新需求会开始发挥作用。
更多详情,示例用法,参考官方
5.编译
在.Net Core 3.0中,在上面的几个关键部分书写完成,基本上就能针对proto文件进行自动编译生成服务端或客户端代码,只需要进行各自的开发即可,这如丝般顺滑的体验,当然是微软为开发者行的方便,但是我们还是有必要了解一下刀耕火种的方式(仅仅是了解,有枪可以用,谁还去拼刺刀,刀快还是枪快?):
5.1 下载编译器
https://github.com/protocolbuffers/protobuf/releases/tag/v3.12.2
win64包
5.2 编译
protoc --proto_path=IMPORT_PATH --csharp_out=DST_DIR path/to/file.proto
--proto_path
:proto文件的目录,可多次指定,-I
可以简化命令--cscharp_out
:输出C#文件的位置,其他语言平台顾名思义,就不一一赘述
--cpp_out
--java_out
--python_out
--go_out
--ruby_out
--objc_out
--php_out
DST_DIR
:可以指定为.zip,注意,如果输出存档已经存在,它将被覆盖;编译器不够智能,无法将文件添加到现有存档。
protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/*.proto
生成实体类,接口
protoc.exe -I="." .\greet.proto --csharp_out="./code"
生成grpc服务
protoc.exe -I="." .\greet.proto --csharp_out="./code" --grpc_out="./code" --plugin=protoc-gen-grpc=grpc_csharp_plugin.exe
--grpc_out: protoc-gen-grpc: 系统找不到指定的文件。
--grpc_out
:csharp_out
是输出类似于咱们平时写的实体类,接口,定义之类的。生成的文件叫,额,就叫*.cs吧.grpc_out
是跟服务相关,创建,调用,绑定,实现相关。生成的玩意叫xxxGrpc.cs。--plugin=protoc-gen-grpc=grpc_csharp_plugin.exe
这个就是csharp的插件。插件nuget帮我们下下来了,目录%UserProfile%\.nuget\packages\grpc.tools\2.29.0\tools\windows_x64\grpc_csharp_plugin.exe
这里通过命令刀耕火种生成xxxGrpc.cs失败了,目前没找到解决方案,相关Stack Overflow问题,但是并没有解决。或许微软官方知道原因,毕竟微软进行了工具集成,生成无误。有知道原因或者解决方案的,请在下方评论指出。灰常感谢~
6.序列化与反序列化
序列化
using Google.Protobuf;
Person john = new Person
{Id = 1234,Name = "John Doe",Email = "jdoe@example.com",Phones = { new Person.Types.PhoneNumber { Number = "555-4321", Type = Person.Types.PhoneType.Home } }
};
using (var output = File.Create("john.dat"))
{john.WriteTo(output);
}
反序列化
using gen_namespaceusing (var input = File.OpenRead("john.dat"))
{Person john;john = Person.Parser.ParseFrom(input);
}
参考链接
https://developers.google.com/protocol-buffers/
https://developers.google.com/protocol-buffers/docs/proto3
https://www.cnblogs.com/nmslanx/articles/8242105.html
https://www.jianshu.com/p/a24c88c0526a
https://www.cnblogs.com/makor/p/protobuf-and-grpc.html
https://en.wikipedia.org/wiki/Protocol_Buffers
https://developers.google.com/protocol-buffers/docs/csharptutorial#parsing-and-serialization