文章目录
1.设计通信协议
为了能让对端知道如何给包分界,⽬前⼀般有以下做法:
- 以固定⼤⼩字节数⽬来分界,如每个包100个字节,对端每收⻬100个字节,就当成⼀个包来解析;
- 以特定符号来分界,如每个包都以特定的字符来结尾(如\r\n),当在字节流中读取到该字符时,则表
明上⼀个包到此为⽌ ;- 固定包头+包体结构,这种结构中⼀般包头部分是⼀个固定字节⻓度的结构,并且包头中会有⼀个特定的字段指定包体的⼤⼩。收包时,先接收固定字节数的头部,解出这个包完整⻓度,按此⻓度接收包体。这是⽬前各种⽹络应⽤⽤的最多的⼀种包格式;header+ body
- 在序列化后的buffer前⾯增加⼀个字符流的头部,其中有个字段存储包总⻓度,根据特殊字符(⽐如根
据\n或者\0)判断头部的完整性。这样通常⽐3要麻烦⼀些,HTTP和REDIS采⽤的是这种⽅式。收包
的时候,先判断已收到的数据中是否包含结束符,收到结束符后解析包头,解出这个包完整⻓度,按此⻓度接收包体。
2.协议设计范例
范例一 IM即时通讯协议
范例二 云平台节点服务器
3.序列化和反序列化基础
- 序列化和反序列化概念
- 什么情况需要序列化
- 如何实现序列化
4.xml/json/protobuf对比及使用场景
- 格式对比
- 特点区别
1.文本协议调试非常方便,能够直接打印出来,序列化后以字符串的方式存在
2.XML序列化后占用的字节比json多
-
速度对比
-
应用场景对比
1.web应用,例如http、websocket,对于登录、注册账号等业务,不会使用二进制的序列化协议(因为protobuf会依赖一个文件IDL,比如说对外提供http注册服务,我们只提供url就行,这里用json最为合适)
2.Android\QT对应的配置文件都是选择XML,Ui文件描述都是XML描述比较直观,游戏的关卡存本地数据也可能用到XML去做,
3.即时通讯、聊天消息用protobuf
- 带宽计算对比
5.protobuf安装
1.解压
tar zxvf protobuf-cpp-3.8.0.tar.gz
2.编译
cd protobuf-3.8.0/
./configure
make
sudo make install
3.显示版本信息
protoc --version
4.编写proto文件
5.讲proto文件生成对应的.cc和.h文件
6.编译范例
比如:
g++ -o code_test code_test.cpp IM.BaseDefine.pb.cc IM.Login.pb.cc -lprotobuf -lpthread
6.protobuf协议
6.1 protobuf协议的工作流程(借鉴im_new文件夹下的:client.c和server.c)
- idl:接口描述语言
1.如下:IM.Login.proto(message的IMLoginReq可以认为是一个类)
2.作用:通过工具生成对应的.cc文件,通过编译产生不同平台的文件,这个编译转换工具是protobuf自带有的
- client:客户端发送登录信息(与IMLoginReq一一对应的)–序列化
序列化
- server:服务器收到数据
客户端序列化的msg_size对应服务器的GetBodyLength(),k客户端的szData对应服务器的GetBodyData(),
- 公共接口(skeleton)
ParseFromArray服务器反序列化
SerializeToArray客户端序列化
- 一般是包头+包体做传输协议,protobuf作为包体
举例:
上面先写作为包头的pdu结构体,再写包体,包体就是protobuf
然后调用send发送到服务器
- 如何确定不同的对象
1.发送和接受双方都要有.proto编译生成的.cc和.h文件,这两个文件是公用的
2.包头的service_id和command_id用于区分不同的对象
6.2 protobuf的使用及代码讲解(举例来自4-addressbook文件夹:读写本地二进制文件)
// See README.txt for information and build instructions.
//
// Note: START and END tags are used in comments to define sections used in
// tutorials. They are not part of the syntax for Protocol Buffers.
//
// To get an in-depth walkthrough of this file and the related examples, see:
// https://developers.google.com/protocol-buffers/docs/tutorials
// [START declaration]
syntax = "proto3"; //不写proto3,默认是proto2
package tutorial; //package类似C++的命名空间
import "google/protobuf/timestamp.proto"; //已经写好的proto文件可以用import去引用
// [END declaration]
// [START java_declaration] //java的
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]
// [START csharp_declaration] //c#的
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
// [END csharp_declaration]
// [START messages]
message Person { //message类似C++的class
//这些1,2,3是标号,不是初始值
string name = 1; //这些标量数据类型可以根据表对应的去看
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType { //支持枚举,enum跟C++一模一样
MOBILE = 0; //移动电话
HOME = 1; //家庭电话
WORK = 2; //工作电话
}
message PhoneNumber {//类里面可以嵌套类,比如下面就是嵌套了一个枚举
string number = 1; //字符串,电话号码
PhoneType type = 2;//电话号码的类型
}
repeated PhoneNumber phones = 4;//重复0到多个,可以认为是数组,一个人有多个电话
google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.
message AddressBook {
//字段是people,对象是Person
repeated Person people = 1;//电话本有多人的电话
}
// [END messages]
- protobuf编译原理(1,2,3代表的是标号)
- 如何将proto文件生成对应的.cc和.h文件
1.protoc -I=/路径1 --cpp_out=./路径2 /路径1 /路径1/addressbook.proto
2.路径1为.proto所在的路径
3.路径2为.cc和.h生成的位置
4.-I=后接.proto文件的目录,--cpp_out表示输出.cc和.h输出到那个目录
将指定proto文件生成.pb.cc和.pb.h
proto -I=./ --cpp_out=./ test.proto
将对应目录的所有proto文件生成.pb.cc和.pb.h
proto -I=./ --cpp_out=./ *.proto
这样就多出来addressbook.pb.cc和addressbook.pb.h这两个文件了
- 运行生成电话本
- 调用list读这个book
也可以添加信息
- 代码:
addperson.c
// See README.txt for information and build instructions.
#include <ctime>
#include <fstream>
#include <google/protobuf/util/time_util.h>
#include <iostream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
using google::protobuf::util::TimeUtil;
// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
//1.输出id
cout << "Enter person ID number: ";
int id;
cin >> id;
person->set_id(id);//设置id
cin.ignore(256, '\n');//输入256字符,以\n为分界线
//2.输入名字
cout << "Enter name: ";
getline(cin, *person->mutable_name());
//3.输入邮件名字
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if (!email.empty()) {
person->set_email(email);
}
//4.输出电话号,输入空为结束
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
string number;
getline(cin, number);
if (number.empty()) {//空就结束
break;
}
//3.添加电话号
tutorial::Person::PhoneNumber* phone_number = person->add_phones();
//设置电话号
phone_number->set_number(number);
//输入电话的类型
cout << "Is this a mobile, home, or work phone? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
//设置最后更新的时间
*person->mutable_last_updated() = TimeUtil::SecondsToTimestamp(time(NULL));
}
// 正确方式 g++ -o add_person add_person.cc addressbook.pb.cc -lprotobuf -std=c++11 -lpt
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
//1.确定编译版本
GOOGLE_PROTOBUF_VERIFY_VERSION;
//2.要填写标成什么名字,比如book
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
//3.定义,类似定义tutorial命名空间下的AddressBook类
tutorial::AddressBook address_book;
{
// Read the existing address book.
//3.文件流形式打开二进制文件写
fstream input(argv[1], ios::in | ios::binary);
if (!input) {//若不存在就创建
cout << argv[1] << ": File not found. Creating a new file." << endl;
} else if (!address_book.ParseFromIstream(&input)) {//若解析失败就
cerr << "Failed to parse address book." << endl;
return -1;
}
}
// Add an address.
//4.创建类对象传进去,填写相关信息
PromptForAddress(address_book.add_people());
{
//5.讲信息写到二进制文件里面
// Write the new address book back to disk.
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
//5.删除所有对象内存
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
listPerson.c
// See README.txt for information and build instructions.
//读取电话本
#include <fstream>
#include <google/protobuf/util/time_util.h>
#include <iostream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
using google::protobuf::util::TimeUtil;
// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
//1.遍历,把所有person都拿出来解析
for (int i = 0; i < address_book.people_size(); i++) {
const tutorial::Person& person = address_book.people(i);
//2.拿第一个参数id,第二个参数名字,第三个参数邮件
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if (person.email() != "") {
cout << " E-mail address: " << person.email() << endl;
}
//3.循环遍历拿电话号码数据信息
for (int j = 0; j < person.phones_size(); j++) {
const tutorial::Person::PhoneNumber& phone_number = person.phones(j);
switch (phone_number.type()) {
case tutorial::Person::MOBILE:
cout << " Mobile phone #: ";
break;
case tutorial::Person::HOME:
cout << " Home phone #: ";
break;
case tutorial::Person::WORK:
cout << " Work phone #: ";
break;
default:
cout << " Unknown phone #: ";
break;
}
cout << phone_number.number() << endl;
}
//4.打印更新信息
if (person.has_last_updated()) {
cout << " Updated: " << TimeUtil::ToString(person.last_updated()) << endl;
}
}
}
// protoc -I=./ --cpp_out=./ *.proto
// 错误方式 g++ -o list_people list_people.cc addressbook.pb.cc -lpthread -lprotobuf -std=c
// 正确方式 g++ -o list_people list_people.cc addressbook.pb.cc -lprotobuf -std=c++11 -lpthr
// Main function: Reads the entire address book from a file and prints all
// the information inside.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
//1.确认protobuf版本
GOOGLE_PROTOBUF_VERIFY_VERSION;
//2.确定参数
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
//3.床啊金对象
tutorial::AddressBook address_book;
{
//4.打开二进制文件解析
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
//5.传进去解析
ListPeople(address_book);
//6.删除所有protobuf对象信息
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
7.protobuf协议语法指南
定义消息类型
先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
- 指定字段类型
两个是整型字段,一个是字符串字段。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。
- 分配标识符1,2,3
1.我们可以看到在上面定义的消息中,给每个字段都定义了唯一的数字值。这些数字是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
2.标识符如下:
- required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的;
- optional:消息格式中该字段可以有0个或1个值(不超过1个)。
- repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。
由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项[packed=true]来保证更高效的编码。如:
repeated int32 samples = 4 [packed=true];
required是永久性的:在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。在这种情况下,你应该考虑编写特别针对于应用程序的、自定义的消息校验函数。Google的一些工程师得出了一个结论:使用required弊多于利;他们更 愿意使用optional和repeated而不是required。当然,这个观点并不具有普遍性。
- 可以添加更多消息类型
在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
message SearchResponse {
...
}
- 添加注释
向.proto文件添加注释,可以使用C/C++风格的 // 和 /* … */ 语法格式
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
required string query = 1;
optional int32 page_number = 2; // Which page number do we want?
optional int32 result_per_page = 3; // Number of results to return per page.
}
- 保留字段
如果通过将字段完全删除或将其注释来更新消息类型,则在将来,当用户更新其消息类型时,他们可以重用那些字段的编号。 如果以后加载相同.proto文件的旧版本,这可能会导致严重问题,包括数据损坏,隐私错误等。 确保不会发生这种情况的一种方法是指定已删除字段的字段编号为“reserved”。 如果将来的任何用户尝试使用这些字段标识符,协议缓冲编译器将会发出抱怨。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
- 从.proto文件生成了什么?
当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
- 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
- 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
- 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
- 标量类型
一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:
在 Protocol Buffer 编码 中你可以找到有关序列化 message 时这些类型如何被编码的详细信息。
[1] 在 Java 中,无符号的 32 位和 64 位整数使用它们对应的带符号表示,第一个 bit 位只是简单的存储在符号位中。
[2] 在所有情况下,设置字段的值将执行类型检查以确保其有效。
[3] 64 位或无符号 32 位整数在解码时始终表示为 long,但如果在设置字段时给出 int,则可以为int。在所有情况下,该值必须适合设置时的类型。见 [2]。
[4] Python 字符串在解码时表示为 unicode,但如果给出了 ASCII 字符串,则可以是 str(这条可能会发生变化)。
- Optional字段和默认值
如上所述,消息描述中的一个元素可以被标记为“可选的”(optional)。一个格式良好的消息可以包含0个或一个optional的元素。当解析消息时,如果没有给它的optional字段指定一个值,那么该消息的相应字段的值就会被置为默认值。默认值可以在消息描述文件中指定。例如,要为 SearchRequest消息的result_per_page字段指定默认值10,在定义消息格式时如下所示:
optional int32 result_per_page = 3 [default = 10];
枚举
当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)就可以了。一个enum类型的字段只能用指定的常量集中的一个值作为其值(如果尝试指定不同的值,解析器就会把它当作一个未知的字段来对待)。在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
你可以为枚举常量定义别名。 需要设置option allow_alias为 true, 否则 protocol编译器会产生错误信息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。你可以在一个消息定义的内部定义枚举,就像上面的例子那样。你也可以在消息的外部定义枚举类型,这样这些枚举值可以在同一.proto文件中定义的任何消息中重复使用。当然也可以在一个消息使用在另一个消息中定义的枚举类型——采用MessageType.EnumType的语法格式。
使用其他的Message类型
你可以将其他message类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在同一.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
- 导入文件(在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?)
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如
import "myproject/other_protos.proto";
默认情况下你只能使用直接导入的.proto文件中的定义。然而, 有时候你需要移动一个.proto文件到一个新的位置。现在,你可以在旧位置放置一个虚拟 .proto 文件,以使用命令 import public将所有导入转发到新位置,而不是直接移动 .proto 文件并在一次更改中更新所有调用点。导入包含 import public 语句的 proto 的任何人都可以导入公共依赖项。例如:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
使用命令 -I/–proto_path 让 protocol 编译器在指定的一组目录中搜索要导入的文件。如果没有给出这个命令选项,它将查找调用编译器所在的目录。通常,你应将 --proto_path 设置为项目的根目录,并对所有导入使用完全限定名称。
- 使用proto3消息类型
我们有可能会在proto2的消息中导入并使用proto3消息,反之亦然。然而在proto3语法中不能使用proto2语法的枚举类型。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
嵌套类型
可以在其他 message 类型中定义和使用 message 类型,如下例所示 - 此处Result消息在SearchResponse 消息中定义:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果要在其父消息类型之外重用此消息类型, 使用的格式为Parent.Type:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
可以嵌套任意多层的消息:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
更新 message 类型
如果现有的 message 类型不再满足你的所有需求 - 例如,你希望 message 格式具有额外的字段 - 但你仍然希望使用基于旧的message格式产生的代码,请不要担心!在不破坏任何现有代码的情况下更新 message 类型非常简单。请记住以下规则:
- 请勿更改任何现有字段的字段编号。
- 添加的任何新字段都应该是 optional 或 repeated。这意味着基于“旧的”消息格式的代码而序列化的任何消息仍可以由被新生成的代码解析,因为这些消息不会缺少任何 required 元素。你应该为这些元素设置合理的默认值,以便新代码可以正确地与旧代码生成的 message 进行交互。同样,你的新代码创建的 message 可以由旧代码解析:旧的二进制文件在解析时只是忽略新字段。但是未丢弃这个新字段,如果稍后序列化消息,则将新字段与其一起序列化。因此,如果将消息传递给新代码,则新字段仍然可用。(兼容性强)
- 可以删除非required字段,只要在新的 message 类型中不再使用该字段的编号。也许你希望的是重命名该字段,那么可以添加前缀 “OBSOLETE_”,或者将字段编号保留(reserved),以便将来你的 .proto文件 的用户不会不小心重用这个编号。
- 只要类型和编号保持不变,非required字段就可以转换为扩展 extensions,反之亦然。
- int32,uint32,int64,uint64 和 bool 都是兼容的 - 这意味着你可以将字段从这些类型更改为另一种类型,而不会破坏向前或向后兼容性。如果从中解析出一个不符合相应类型的数字,你将获得与在 C++ 中将该数字转换为该类型时相同的效果(例如,如果将 64 位数字作为 int32 读取,它将被截断为 32 位)。
- sint32 和 sint64 彼此兼容,但与其他整数类型不兼容。
- 只要bytes是有效的 UTF-8,string 和 bytes 就是兼容的。
如果bytes包含 message 的编码版本,则内嵌的 message 与 bytes 兼容。- fixed32 与 sfixed32 兼容,fixed64 与 sfixed64 兼容。
- optional 与 repeated 兼容。给定repeated字段的序列化数据作为输入,期望该字段为 optional 的客户端将采用最后一个输入值(如果它是基本类型字段)或合并所有输入元素(如果它是 message 类型字段)。
- 可以更改默认值,但是默认值不会在网络中传输。因此,如果程序接收到的消息中某一字段未设置值,那边该字段的值将被设定为相应的协议版本中定义的默认值,而不是发送代码中定义的默认值。
- enum 与 int32,uint32,int64 和 uint64兼容(注意,如果它们不匹配,值将被截断),但要注意当客户端代码反序列化message时,可能会有不同的结果。值得注意的是,当 message 被反序列化时,将丢弃无法识别的 enum 值,这使得字段的 has… 型的accessor返回 false, 并且其 getter 返回 enum 定义中列出的第一个值,或者如果返回默认值如果有指定一个默认值的话。对于repeated 枚举字段,任何无法识别的值都将从列表中删除。但是,整数字段将始终保留其值。因此,在有可能接收超出范围的枚举值时,将整数升级为 enum 这一操作需要非常小心。
- 在当前的 Java 和 C++ 实现中,当删除无法识别的 enum 值时,它们与其他未知字段一起存储。请注意,如果此数据被序列化,然后由识别出这些值的客户端重新解析,则会导致奇怪的行为。对于optional 字段,如果在反序列化原来的 message 之后写入新值,那么客户端读到的仍是旧值。对于repeated 字段,即使在该字段识别并且添加了新值之后,旧值仍有可能会出现, 这意味着该字段是无序的。
- 将单个 optional 值更改为新的oneof 的成员是安全且二进制兼容的。如果你确定没有代码一次设置多个,则将多个 optional 字段移动到新的 oneof 中可能是安全的。但是将任何字段移动到现有的 oneof 是不安全的。
Extensions(留白待扩展)
通过扩展,你可以声明 message 中的一系列字段编号用于第三方扩展。扩展是那些未由原始 .proto 文件定义的字段的占位符。这允许通过使用这些字段编号来定义部分或全部字段,从而将其它 .proto 文件定义的字段添加到当前 message 定义中。我们来看一个例子:
message Foo {
// ...
extensions 100 to 199;
}
这表示 Foo 中的字段数 [100,199] 的范围是为扩展保留的。其他用户现在可以使用指定范围内的字段编号在他们自己的 .proto 文件中为 Foo 添加新字段,例如:
extend Foo {
optional int32 bar = 126;
}
这会将名为 bar 且编号为 126 的字段添加到 Foo 的原始定义中。
当用户的 Foo 消息被编码时,其格式与用户在 Foo 中常规定义新字段的格式完全相同。但是,在应用程序代码中访问扩展字段的方式与访问常规字段略有不同 - 生成的数据访问代码具有用于处理扩展的特殊访问器。那么,举个例子,下面就是如何在 C++ 中设置 bar 的值:
Foo foo;
foo.SetExtension(bar, 15);
类似地,Foo 类定义模板化访问器 HasExtension(),ClearExtension(),GetExtension(),MutableExtension() 和 AddExtension()。它们都具有与正常字段生成的访问器相匹配的语义。
请注意,扩展可以是任何字段类型,包括 message 类型,但不能是 oneofs 或 maps。
Oneof(强制执行可选字段的选项,可节省空间)
如果你的 message 包含许多可选字段,并且最多只能同时设置其中一个字段,则可以使用 oneof 功能强制执行此行为并节省内存。
Oneof 字段类似于可选字段,除了 oneof 共享内存中的所有字段,并且最多只能同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。你可以使用特殊的 case() 或 WhichOneof() 方法检查 oneof 字段中当前是哪个值(如果有)被设置,具体方法取决于你选择的语言。
- 使用 Oneof
要在 .proto 中定义 oneof,请使用 oneof 关键字,后跟你的 oneof 名称,在本例中为 test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后,将 oneof 字段添加到 oneof 定义中。你可以添加任何类型的字段,但不能使用 required,optional 或 repeated 关键字。如果需要向 oneof 添加重复字段,可以使用包含重复字段的 message。
在生成的代码中,oneof 字段与常规 optional 方法具有相同的 getter 和 setter。你还可以使用特殊方法检查 oneof 中的值(如果有)。
- Oneof 特性
1.设置 oneof 字段将自动清除 oneof 的所有其他成员。因此,如果你设置了多个字段,则只有你设置的最后一个字段仍然具有值。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
2.如果解析器遇到同一个 oneof 的多个成员,则在解析的消息中仅使用看到的最后一个成员。
3.oneof 不支持扩展
4.oneof 不能使用 repeated
5.反射 API 适用于 oneof 字段
6.如果你使用的是 C++,请确保你的代码不会导致内存崩溃。以下示例代码将崩溃,因为已通过调用 set_name() 方法删除了 sub_message。
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
7.同样在 C++中,如果你使用 Swap() 交换了两条 oneofs 消息,则每条消息将以另一条消息的 oneof 实例结束:在下面的示例中,msg1 将具有 sub_message 而 msg2 将具有 name。
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());
- 向后兼容性问题
添加或删除其中一个字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。
- 标签重用问题
1.将 optional 可选字段移入或移出 oneof:在序列化和解析 message 后,你可能会丢失一些信息(某些字段将被清除)。但是,你可以安全地将单个字段移动到新的 oneof 中,并且如果已知只有一个字段被设置,则可以移动多个字段。
2.删除 oneof 字段并将其重新添加回去:在序列化和解析 message 后,这可能会清除当前设置的 oneof 字段。
3.拆分或合并 oneof:这与移动常规的 optional 字段有类似的问题。
Maps(要在数据定义中创建关联映射)
map<key_type, value_type> map_field = N;
其中 key_type 可以是任何整数或字符串类型(任何标量类型除浮点类型和 bytes)。请注意,枚举不是有效的 key_type。value_type 可以是除 map 之外的任何类型。因此,举个例子,如果要创建项目映射,其中每个 “Project” message 都与字符串键相关联,则可以像下面这样定义它:
- Maps 特性
- maps 不支持扩展
- maps 不能是 repeated、optional、required
- map 值的网络序和 map 迭代序未定义,因此你不能依赖于特定顺序的 map 项
- 生成 .proto 的文本格式时,maps 按键排序。数字键按数字排序
- 当解析或合并时,如果有重复的 map 键,则使用最后看到的键。从 文本格式解析 map 时,如果存在重复键,则解析可能会失败。
- 向后兼容性
map 语法等效于以下内容,因此不支持 map 的 protocol buffers 实现仍可处理你的数据:
message MapFieldEntry {
optional key_type key = 1;
optional value_type value = 2;
}
repeated MapFieldEntry map_field = N
Packages(类似C++的域名)
你可以将可选的package说明符添加到 .proto 文件,以防止 protocol message 类型之间的名称冲突。
package foo.bar;
message Open { ... }
然后,你可以在定义 message 类型的字段时使用package说明符:
message Foo {
...
required foo.bar.Open open = 1;
...
}
package 对生成的代码的影响取决于你所选择的语言:
1.在 C++ 中,生成的类包含在 C++ 命名空间中。例如,Open 将位于命名空间 foo::bar 中。
2.在 Java 中,除非在 .proto 文件中明确提供选项 java_package,否则该包将用作 Java 包
3.在 Python 中,package 指令被忽略,因为 Python 模块是根据它们在文件系统中的位置进行组织的
4.在Go中,package 指令被忽略,生成的.pb.go文件位于以相应的go_proto_library规则命名的包中。
补充:
请注意,即使 package 指令不直接影响生成的代码,但是例如在 Python 中,仍然强烈建议指定 .proto 文件的包,否则可能导致描述符中的命名冲突,并使 proto 对于其他语言不方便。
- Packages 和名称解析
protocol buffer 语言中的类型名称解析与 C++ 类似:首先搜索最里面的范围,然后搜索下一个范围,依此类推,每个包被认为是其父包的 “内部”。开头的 ‘.’(例如 .foo.bar.Baz)意味着从最外层的范围开始。
protocol buffer 编译器通过解析导入的 .proto 文件来解析所有类型名称。每种语言的代码生成器都知道如何使用相应的语言类型,即使它具有不同的范围和规则。
定义服务
如果要将 message 类型与 RPC(远程过程调用)系统一起使用,则可以在 .proto 文件中定义 RPC 服务接口,protocol buffer 编译器将以你选择的语言生成服务接口和stub(桩)。因此,例如,如果要定义一个 RPC 服务,其中包含一个根据 SearchRequest 返回 SearchResponse 的方法,可以在 .proto 文件中定义它,如下所示:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
默认情况下,protocol 编译器将生成一个名为 SearchService 的抽象接口和相应的"桩" 实现。桩会转发所有对 RpcChannel 的调用,而 RpcChannel 又是一个抽象接口,你必须根据自己的 RPC 系统自行定义。例如,你可以实现一个 RpcChannel,它将 message 序列化并通过 HTTP 将其发送到服务器。换句话说,生成的桩提供了一个类型安全的接口,用于进行基于 protocol-buffer 的 RPC 调用,而不会将你锁定到任何特定的 RPC 实现中。所以,在 C++ 中,你可能会得到这样的代码:
using google::protobuf;
protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;
void DoSearch() {
// You provide classes MyRpcChannel and MyRpcController, which implement
// the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
channel = new MyRpcChannel("somehost.example.com:1234");
controller = new MyRpcController;
// The protocol compiler generates the SearchService class based on the
// definition given above.
service = new SearchService::Stub(channel);
// Set up the request.
request.set_query("protocol buffers");
// Execute the RPC.
service->Search(controller, request, response, protobuf::NewCallback(&Done));
}
void Done() {
delete service;
delete channel;
delete controller;
}
所有服务类还实现了 Service 接口,它提供了一种在编译时不知道方法名称或其输入和输出类型的情况下来调用特定方法的方法。在服务器端,这可用于实现一个可以注册服务的 RPC 服务器。
using google::protobuf;
class ExampleSearchService : public SearchService {
public:
void Search(protobuf::RpcController* controller,
const SearchRequest* request,
SearchResponse* response,
protobuf::Closure* done) {
if (request->query() == "google") {
response->add_result()->set_url("http://www.google.com");
} else if (request->query() == "protocol buffers") {
response->add_result()->set_url("http://protobuf.googlecode.com");
}
done->Run();
}
};
int main() {
// You provide class MyRpcServer. It does not have to implement any
// particular interface; this is just an example.
MyRpcServer server;
protobuf::Service* service = new ExampleSearchService;
server.ExportOnPort(1234, service);
server.Run();
delete service;
return 0;
}
如果你不想使用自己现有的 RPC 系统,可以使用 gRPC: 一个由谷歌开发的与语言和平台无关的开源 RPC 系统。gRPC 特别适用于 protocol buffers,并允许你使用特殊的 protocol buffers 编译器插件直接从 .proto 文件生成相关的 RPC 代码。但是,由于使用 proto2 和 proto3 生成的客户端和服务器之间存在潜在的兼容性问题,我们建议你使用 proto3 来定义 gRPC 服务。如果你确实希望将 proto2 与 gRPC 一起使用,则需要使用 3.0.0 或更高版本的 protocol buffers 编译器和库。
选项 Options
.proto 文件中的各个声明可以使用一些选项进行诠释。选项不会更改声明的含义,但可能会影响在特定上下文中处理它的方式。可用选项的完整列表在 google/protobuf/descriptor.proto中定义。
一些选项是文件级选项,这意味着它们应该在更高层的范围内编写,而不是在任何消息,枚举或服务定义中。一些选项是 message 消息级选项,这意味着它们应该写在 message 消息定义中。一些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法上,但是,目前在这几个项目上并没有任何有用的选项。
以下是一些最常用的选项:
- 自定义选项
Protocol Buffers 甚至允许你定义和使用自己的选项。请注意,这是高级功能,大多数人不需要。由于选项是由 google/protobuf/descriptor.proto(如 FileOptions 或 FieldOptions)中定义的消息定义的,因此定义你自己的选项只需要扩展这些消息。例如:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
这里我们通过扩展 MessageOptions 定义了一个新的 message 级选项。然后,当我们使用该选项时,必须将选项名称括在括号中以指示它是扩展名。我们现在可以在 C++ 中读取 my_option 的值,如下所示:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
这里,MyMessage::descriptor()->options() 返回 MyMessage 的 MessageOptions protocol message。从中读取自定义选项就像阅读任何其他扩展。
在java中会写
String value = MyProtoFile.MyMessage.getDescriptor().getOptions().getExtension(MyProtoFile.myOption);
在 Python 中它将是:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
.Extensions[my_proto_file_pb2.my_option]
可以在 Protocol Buffers 语言中为每种结构自定义选项。这是一个使用各种选项的示例:
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002;
}
extend google.protobuf.EnumOptions {
optional bool my_enum_option = 50003;
}
extend google.protobuf.EnumValueOptions {
optional uint32 my_enum_value_option = 50004;
}
extend google.protobuf.ServiceOptions {
optional MyEnum my_service_option = 50005;
}
extend google.protobuf.MethodOptions {
optional MyMessage my_method_option = 50006;
}
option (my_file_option) = "Hello world!";
message MyMessage {
option (my_message_option) = 1234;
optional int32 foo = 1 [(my_field_option) = 4.5];
optional string bar = 2;
}
enum MyEnum {
option (my_enum_option) = true;
FOO = 1 [(my_enum_value_option) = 321];
BAR = 2;
}
message RequestType {}
message ResponseType {}
service MyService {
option (my_service_option) = FOO;
rpc MyMethod(RequestType) returns(ResponseType) {
// Note: my_method_option has type MyMessage. We can set each field
// within it using a separate "option" line.
option (my_method_option).foo = 567;
option (my_method_option).bar = "Some string";
}
}
请注意,如果要在除定义它之外的包中使用自定义选项,则必须在选项名称前加上包名称,就像对类型名称一样。例如:
// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
option (foo.my_option) = "Hello world!";
}
最后一件事:由于自定义选项是扩展名,因此必须为其分配字段编号,就像任何其他字段或扩展名一样。在上面的示例中,我们使用了 50000-99999 范围内的字段编号。此范围保留供个别组织内部使用,因此你可以自由使用此范围内的数字用于内部应用程序。但是,如果你打算在公共应用程序中使用自定义选项,则务必确保你的字段编号是全局唯一的。要获取全球唯一的字段编号,请发送请求以向 protobuf全球扩展注册表添加条目。通常你只需要一个扩展号。你可以通过将多个选项放在子消息中来实现一个扩展号声明多个选项:
message FooOptions {
optional int32 opt1 = 1;
optional string opt2 = 2;
}
extend google.protobuf.FieldOptions {
optional FooOptions foo_options = 1234;
}
// usage:
message Bar {
optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
// alternative aggregate syntax (uses TextFormat):
optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
另请注意,每种选项类型(文件级别,消息级别,字段级别等)都有自己的数字空间,例如,你可以使用相同的数字声明 FieldOptions 和 MessageOptions 的扩展名。
生成类
要生成 Java,Python 或 C++代码,你需要使用 .proto 文件中定义的 message 类型,你需要在 .proto 上运行 protocol buffer 编译器 protoc。
Protocol 编译器的调用如下:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
IMPORT_PATH 指定在解析导入指令时查找 .proto 文件的目录。如果省略,则使用当前目录。可以通过多次传递 --proto_path 选项来指定多个导入目录;他们将按顺序搜索。-I = IMPORT_PATH 可以用作 --proto_path 的缩写形式。
可以提供一个或多个输出指令:
- –cpp_out在 DST_DIR 中生成 C++ 代码。
- –java_out在DST_DIR中生成 Java 代码。
- –python_out 在 DST_DIR 中生成 Python 代码。
为了方便起见,如果 DST_DIR 以 .zip 或 .jar 结尾,编译器会将输出写入到具有给定名称的单个 ZIP 格式的存档文件。.jar 输出还将根据 Java JAR 规范的要求提供清单文件。请注意,如果输出存档已存在,则会被覆盖;编译器不够智能,无法将文件添加到现有存档中。
你必须提供一个或多个 .proto 文件作为输入。可以一次指定多个 .proto 文件。虽然文件是相对于当前目录命名的,但每个文件必须驻留在其中一个 IMPORT_PATH 中,以便编译器可以确定其规范名称
8.测试代码解析分类(我写来自己看的…)
1-im
2-im_new
3-person(类似登录和传输数据)
code_test.cpp(int和string转二进制并打印)
pb_speed.cpp(正常protobuf数据进行序列化和反序列成二进制)
4-addressbook(类似注册)(需要用GOOGLE_PROTOBUF_VERIFY_VERSION确认版本信息)
add_person.cc将输入信息转为二进制信息
list_people.cc将二进制信息转化为输出信息