该系列Blog的内容主体主要源自于PRotocol Buffer的官方文档,而代码示例则抽取于当前正在开发的一个公司内部项目的Demo。这样做的目的主要在于不仅可以保持Google文档的良好风格和系统性,同时再结合一些比较实用和通用的用例,这样就更加便于公司内部的培训,以及和广大网友的技术交流。需要说明的是,Blog的内容并非line by line的翻译,其中包含一些经验性总结,与此同时,对于一些不是非常常用的功能并未予以说明,有兴趣的开发者可以直接查阅Google的官方文档。 一、为什么使用Protocol Buffer? 在回答这个问题之前,我们还是先给出一个在实际开发中经常会遇到的系统场景。比如:我们的客户端程序是使用java开发的,可能运行自不同的平台,如:linux、Windows或者是Android,而我们的服务器程序通常是基于Linux平台并使用C++开发完成的。在这两种程序之间进行数据通讯时存在多种方式用于设计消息格式,如: 1. 直接传递C/C++语言中一字节对齐的结构体数据,只要结构体的声明为定长格式,那么该方式对于C/C++程序而言就非常方便了,仅需将接收到的数据按照结构体类型强行转换即可。事实上对于变长结构体也不会非常麻烦。在发送数据时,也只需定义一个结构体变量并设置各个成员变量的值之后,再以char*的方式将该二进制数据发送到远端。反之,该方式对于Java开发者而言就会非常繁琐,首先需要将接收到的数据存于ByteBuffer之中,再根据约定的字节序逐个读取每个字段,并将读取后的值再赋值给另外一个值对象中的域变量,以便于程序中其他代码逻辑的编写。对于该类型程序而言,联调的基准是必须客户端和服务器双方均完成了消息报文构建程序的编写后才能展开,而该设计方式将会直接导致Java程序开发的进度过慢。即便是Debug阶段,也会经常遇到Java程序中出现各种域字段拼接的小错误。 2. 使用SOAP协议(WebService)作为消息报文的格式载体,由该方式生成的报文是基于文本格式的,同时还存在大量的xml描述信息,因此将会大大增加网络IO的负担。又由于XML解析的复杂性,这也会大幅降低报文解析的性能。总之,使用该设计方式将会使系统的整体运行性能明显下降。 对于以上两种方式所产生的问题,Protocol Buffer均可以很好的解决,不仅如此,Protocol Buffer还有一个非常重要的优点就是可以保证同一消息报文新旧版本之间的兼容性。至于具体的方式我们将会在后续的博客中给出。 二、定义第一个Protocol Buffer消息。 创建扩展名为.proto的文件,如:MyMessage.proto,并将以下内容存入该文件中。 message LogonReqMessage { required int64 acctID = 1; required string passwd = 2; } 这里将给出以上消息定义的关键性说明。 1. message是消息定义的关键字,等同于C++中的struct/class,或是Java中的class。 2. LogonReqMessage为消息的名字,等同于结构体名或类名。 3. required前缀表示该字段为必要字段,既在序列化和反序列化之前该字段必须已经被赋值。与此同时,在Protocol Buffer中还存在另外两个类似的关键字,optional和repeated,带有这两种限定符的消息字段则没有required字段这样的限制。相比于optional,repeated主要用于表示数组字段。具体的使用方式在后面的用例中均会一一列出。 4. int64和string分别表示长整型和字符串型的消息字段,在Protocol Buffer中存在一张类型对照表,既Protocol Buffer中的数据类型与其他编程语言(C++/Java)中所用类型的对照。该对照表中还将给出在不同的数据场景下,哪种类型更为高效。该对照表将在后面给出。 5. acctID和passwd分别表示消息字段名,等同于Java中的域变量名,或是C++中的成员变量名。 6. 标签数字1和2则表示不同的字段在序列化后的二进制数据中的布局位置。在该例中,passwd字段编码后的数据一定位于acctID之后。需要注意的是该值在同一message中不能重复。另外,对于Protocol Buffer而言,标签值为1到15的字段在编码时可以得到优化,既标签值和类型信息仅占有一个byte,标签范围是16到2047的将占有两个bytes,而Protocol Buffer可以支持的字段数量则为2的29次方减一。有鉴于此,我们在设计消息结构时,可以尽可能考虑让repeated类型的字段标签位于1到15之间,这样便可以有效的节省编码后的字节数量。 三、定义第二个(含有枚举字段)Protocol Buffer消息。 //在定义Protocol Buffer的消息时,可以使用和C++/Java代码同样的方式添加注释。 enum UserStatus { OFFLINE = 0; //表示处于离线状态的用户 ONLINE = 1; //表示处于在线状态的用户 } message UserInfo { required int64 acctID = 1; required string name = 2; required UserStatus status = 3; } 这里将给出以上消息定义的关键性说明(仅包括上一小节中没有描述的)。 1. enum是枚举类型定义的关键字,等同于C++/Java中的enum。 2. UserStatus为枚举的名字。 3. 和C++/Java中的枚举不同的是,枚举值之间的分隔符是分号,而不是逗号。 4. OFFLINE/ONLINE为枚举值。 5. 0和1表示枚举值所对应的实际整型值,和C/C++一样,可以为枚举值指定任意整型值,而无需总是从0开始定义。如: enum OperationCode { LOGON_REQ_CODE = 101; LOGOUT_REQ_CODE = 102; RETRIEVE_BUDDIES_REQ_CODE = 103; LOGON_RESP_CODE = 1001; LOGOUT_RESP_CODE = 1002; RETRIEVE_BUDDIES_RESP_CODE = 1003; } 四、定义第三个(含有嵌套消息字段)Protocol Buffer消息。 我们可以在同一个.proto文件中定义多个message,这样便可以很容易的实现嵌套消息的定义。如: enum UserStatus { OFFLINE = 0; ONLINE = 1; } message UserInfo { required int64 acctID = 1; required string name = 2; required UserStatus status = 3; } message LogonRespMessage { required LoginResult logonResult = 1; required UserInfo userInfo = 2; } 这里将给出以上消息定义的关键性说明(仅包括上两小节中没有描述的)。 1. LogonRespMessage消息的定义中包含另外一个消息类型作为其字段,如UserInfo userInfo。 2. 上例中的UserInfo和LogonRespMessage被定义在同一个.proto文件中,那么我们是否可以包含在其他.proto文件中定义的message呢?Protocol Buffer提供了另外一个关键字import,这样我们便可以将很多通用的message定义在同一个.proto文件中,而其他消息定义文件可以通过import的方式将该文件中定义的消息包含进来,如: import "myproject/CommonMessages.proto" 五、限定符(required/optional/repeated)的基本规则。 1. 在每个消息中必须至少留有一个required类型的字段。 2. 每个消息中可以包含0个或多个optional类型的字段。 3. repeated表示的字段可以包含0个或多个数据。需要说明的是,这一点有别于C++/Java中的数组,因为后两者中的数组必须包含至少一个元素。 4. 如果打算在原有消息协议中添加新的字段,同时还要保证老版本的程序能够正常读取或写入,那么对于新添加的字段必须是optional或repeated。道理非常简单,老版本程序无法读取或写入新增的required限定符的字段。 六、类型对照表。
.proto Type | Notes | C++ Type | Java Type |
double | double | double | |
float | float | float | |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long |
uint32 | Uses variable-length encoding. | uint32 | int |
uint64 | Uses variable-length encoding. | uint64 | long |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long |
sfixed32 | Always four bytes. | int32 | int |
sfixed64 | Always eight bytes. | int64 | long |
bool | bool | boolean | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String |
bytes | May contain any arbitrary sequence of bytes. | string | ByteString |
七、Protocol Buffer消息升级原则。 在实际的开发中会存在这样一种应用场景,既消息格式因为某些需求的变化而不得不进行必要的升级,但是有些使用原有消息格式的应用程序暂时又不能被立刻升级,这便要求我们在升级消息格式时要遵守一定的规则,从而可以保证基于新老消息格式的新老程序同时运行。规则如下: 1. 不要修改已经存在字段的标签号。 2. 任何新添加的字段必须是optional和repeated限定符,否则无法保证新老程序在互相传递消息时的消息兼容性。 3. 在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用。 4. int32、uint32、int64、uint64和bool等类型之间是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之间是兼容的,这意味着如果想修改原有字段的类型时,为了保证兼容性,只能将其修改为与其原有类型兼容的类型,否则就将打破新老消息格式的兼容性。 5. optional和repeated限定符也是相互兼容的。 八、Packages。 我们可以在.proto文件中定义包名,如: package ourproject.lyphone; 该包名在生成对应的C++文件时,将被替换为名字空间名称,既namespace ourproject { namespace lyphone。而在生成的Java代码文件中将成为包名。 九、Options。 Protocol Buffer允许我们在.proto文件中定义一些常用的选项,这样可以指示Protocol Buffer编译器帮助我们生成更为匹配的目标语言代码。Protocol Buffer内置的选项被分为以下三个级别: 1. 文件级别,这样的选项将影响当前文件中定义的所有消息和枚举。 2. 消息级别,这样的选项仅影响某个消息及其包含的所有字段。 3. 字段级别,这样的选项仅仅响应与其相关的字段。 下面将给出一些常用的Protocol Buffer选项。 1. option java_package = "com.companyname.projectname"; java_package是文件级别的选项,通过指定该选项可以让生成Java代码的包名为该选项值,如上例中的Java代码包名为com.companyname.projectname。与此同时,生成的Java文件也将会自动存放到指定输出目录下的com/companyname/projectname子目录中。如果没有指定该选项,Java的包名则为package关键字指定的名称。该选项对于生成C++代码毫无影响。 2. option java_outer_classname = "LYPhoneMessage"; java_outer_classname是文件级别的选项,主要功能是显示的指定生成Java代码的外部类名称。如果没有指定该选项,Java代码的外部类名称为当前文件的文件名部分,同时还要将文件名转换为驼峰格式,如:my_project.proto,那么该文件的默认外部类名称将为MyProject。该选项对于生成C++代码毫无影响。 注:主要是因为Java中要求同一个.java文件中只能包含一个Java外部类或外部接口,而C++则不存在此限制。因此在.proto文件中定义的消息均为指定外部类的内部类,这样才能将这些消息生成到同一个Java文件中。在实际的使用中,为了避免总是输入该外部类限定符,可以将该外部类静态引入到当前Java文件中,如:import static com.company.project.LYPhoneMessage.*。 3. option optimize_for = LITE_RUNTIME; optimize_for是文件级别的选项,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME。缺省情况下是SPEED。 SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。 CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。 LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的。因此我们在C++中链接Protocol Buffer库时仅需链接libprotobuf-lite,而非libprotobuf。在Java中仅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar。 注:对于LITE_MESSAGE选项而言,其生成的代码均将继承自MessageLite,而非Message。 4. [pack = true]: 因为历史原因,对于数值型的repeated字段,如int32、int64等,在编码时并没有得到很好的优化,然而在新近版本的Protocol Buffer中,可通过添加[pack=true]的字段选项,以通知Protocol Buffer在为该类型的消息对象编码时更加高效。如: repeated int32 samples = 4 [packed=true]。 注:该选项仅适用于2.3.0以上的Protocol Buffer。 5. [default = default_value]: optional类型的字段,如果在序列化时没有被设置,或者是老版本的消息中根本不存在该字段,那么在反序列化该类型的消息是,optional的字段将被赋予类型相关的缺省值,如bool被设置为false,int32被设置为0。Protocol Buffer也支持自定义的缺省值,如: optional int32 result_per_page = 3 [default = 10]。 十、命令行编译工具。 protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto 这里将给出上述命令的参数解释。 1. protoc为Protocol Buffer提供的命令行编译工具。 2. --proto_path等同于-I选项,主要用于指定待编译的.proto消息定义文件所在的目录,该选项可以被同时指定多个。 3. --cpp_out选项表示生成C++代码,--java_out表示生成Java代码,--python_out则表示生成Python代码,其后的目录为生成后的代码所存放的目录。 4. path/to/file.proto表示待编译的消息定义文件。
注:对于C++而言,通过Protocol Buffer编译工具,可以将每个.proto文件生成出一对.h和.cc的C++代码文件。生成后的文件可以直接加载到应用程序所在的工程项目中。如:MyMessage.proto生成的文件为MyMessage.pb.h和MyMessage.pb.cc。
http://www.cnblogs.com/stephen-liu74/archive/2013/01/02/2841485.html
这篇Blog仍然是以Google的官方文档为主线,代码实例则完全取自于我们正在开发的一个Demo项目,通过前一段时间的尝试,感觉这种结合的方式比较有利于培训和内部的技术交流。还是那句话,没有最好的,只有最适合的。我想写Blog也是这一道理吧,不同的技术主题可能需要采用不同的风格。好了,还是让我们尽早切入主题吧。 一、生成目标语言代码。 下面的命令帮助我们将MyMessage.proto文件中定义的一组Protocol Buffer格式的消息编译成目标语言(C++)的代码。至于消息的内容,我们会在后面以分段的形式逐一列出,同时也会在附件中给出所有源代码。 protoc -I=./message --cpp_out=./src ./MyMessage.proto 从上面的命令行参数中可以看出,待编译的文件为MyMessage.proto,他存放在当前目录的message子目录下。--cpp_out参数则指示编译工具我们需要生成目标语言是C++,输出目录是当前目录的src子目录。在本例中,生成的目标代码文件名是MyMessage.pb.h和MyMessage.pb.cc。 二、简单message生成的C++代码。 这里先定义一个最简单的message,其中只是包含原始类型的字段。 option optimize_for = LITE_RUNTIME; message LogonReqMessage { required int64 acctID = 1; required string passwd = 2; } 由于我们在MyMessage文件中定义选项optimize_for的值为LITE_RUNTIME,因此由该.proto文件生成的所有C++类的父类均为::google::protobuf::MessageLite,而非::google::protobuf::Message。在上一篇博客中已经给出了一些简要的说明,MessageLite类是Message的父类,在MessageLite中将缺少Protocol Buffer对反射的支持,而此类功能均在Message类中提供了具体的实现。对于我们的项目而言,整个系统相对比较封闭,不会和更多的外部程序进行交互,与此同时,我们的客户端部分又是运行在Android平台,有鉴于此,我们考虑使用LITE版本的Protocol Buffer。这样不仅可以得到更高编码效率,而且生成代码编译后所占用的资源也会更少,至于反射所能带来的灵活性和极易扩展性,对于该项目而言完全可以忽略。下面我们来看一下由message LogonReqMessage生成的C++类的部分声明,以及常用方法的说明性注释。
1 class LogonReqMessage : public ::google::protobuf::MessageLite { 2 public: 3 LogonReqMessage(); 4 virtual ~LogonReqMessage(); 5 6 // implements Message ---------------------------------------------- 7 //下面的成员函数均实现自MessageLite中的虚函数。 8 //创建一个新的LogonReqMessage对象,等同于clone。 9 LogonReqMessage* New() const;10 //用另外一个LogonReqMessage对象初始化当前对象,等同于赋值操作符重载(operator=)11 void CopyFrom(const LogonReqMessage& from);12 //清空当前对象中的所有数据,既将所有成员变量置为未初始化状态。13 void Clear();14 //判断当前状态是否已经初始化。15 bool IsInitialized() const;16 //在给当前对象的所有变量赋值之后,获取该对象序列化后所需要的字节数。17 int ByteSize() const;18 //获取当前对象的类型名称。19 ::std::string GetTypeName() const;20 21 // required int64 acctID = 1;22 //下面的成员函数都是因message中定义的acctID字段而生成。23 //这个静态成员表示AcctID的标签值。命名规则是k + FieldName(驼峰规则) + FieldNumber。24 static const int kAcctIDFieldNumber = 1;25 //如果acctID字段已经被设置返回true,否则false。26 inline bool has_acctid() const;27 //执行该函数后has_acctid函数将返回false,而下面的acctid函数则返回acctID的缺省值。28 inline void clear_acctid();29 //返回acctid字段的当前值,如果没有设置则返回int64类型的缺省值。30 inline ::google::protobuf::int64 acctid() const;31 //为acctid字段设置新值,调用该函数后has_acctid函数将返回true。32 inline void set_acctid(::google::protobuf::int64 value);33 34 // required string passwd = 2;35 //下面的成员函数都是因message中定义的passwd字段而生成。这里生成的函数和上面acctid36 //生成的那组函数基本相似。因此这里只是列出差异部分。37 static const int kPasswdFieldNumber = 2;38 inline bool has_passwd() const;39 inline void clear_passwd();40 inline const ::std::string& passwd() const;41 inline void set_passwd(const ::std::string& value);42 //对于字符串类型字段设置const char*类型的变量值。43 inline void set_passwd(const char* value);44 inline void set_passwd(const char* value, size_t size);45 //可以通过返回值直接给passwd对象赋值。在调用该函数之后has_passwd将返回true。46 inline ::std::string* mutable_passwd();47 //释放当前对象对passwd字段的所有权,同时返回passwd字段对象指针。调用此函数之后,passwd字段对象48 //的所有权将移交给调用者。此后再调用has_passwd函数时将返回false。49 inline ::std::string* release_passwd();50 private:51 ... ... 52 };下面是读写LogonReqMessage对象的C++测试代码和说明性注释。
1 void testSimpleMessage() 2 { 3 printf("==================This is simple message.================/n"); 4 //序列化LogonReqMessage对象到指定的内存区域。 5 LogonReqMessage logonReq; 6 logonReq.set_acctid(20); 7 logonReq.set_passwd("Hello World"); 8 //提前获取对象序列化所占用的空间并进行一次性分配,从而避免多次分配 9 //而造成的性能开销。通过该种方式,还可以将序列化后的数据进行加密。10 //之后再进行持久化,或是发送到远端。11 int length = logonReq.ByteSize();12 char* buf = new char[length];13 logonReq.SerializeToArray(buf,length);14 //从内存中读取并反序列化LogonReqMessage对象,同时将结果打印出来。15 LogonReqMessage logonReq2;16 logonReq2.ParseFromArray(buf,length);17 printf("acctID = %I64d, passWord = %s/n",logonReq2.acctid(),logonReq2.passwd().c_str());18 delete [] buf;19 }三、嵌套message生成的C++代码。 enum UserStatus { OFFLINE = 0; ONLINE = 1; } enum LoginResult { LOGON_RESULT_SUCCESS = 0; LOGON_RESULT_NOTEXIST = 1; LOGON_RESULT_ERROR_PASSWD = 2; LOGON_RESULT_ALREADY_LOGON = 3; LOGON_RESULT_SERVER_ERROR = 4; } message UserInfo { required int64 acctID = 1; required string name = 2; required UserStatus status = 3; } message LogonRespMessage { required LoginResult logonResult = 1; required UserInfo userInfo = 2; //这里嵌套了UserInfo消息。 } 对于上述消息生成的C++代码,UserInfo因为只是包含了原始类型字段,因此和上例中的LogonReqMessage没有太多的差别,这里也就不在重复列出了。由于LogonRespMessage消息中嵌套了UserInfo类型的字段,在这里我们将仅仅给出该消息生成的C++代码和关键性注释。
1 class LogonRespMessage : public ::google::protobuf::MessageLite { 2 public: 3 LogonRespMessage(); 4 virtual ~LogonRespMessage(); 5 6 // implements Message ---------------------------------------------- 7 ... ... //这部分函数和之前的例子一样。 8 9 // required .LoginResult logonResult = 1;10 //下面的成员函数都是因message中定义的logonResult字段而生成。11 //这一点和前面的例子基本相同,只是类型换做了枚举类型LoginResult。 12 static const int kLogonResultFieldNumber = 1;13 inline bool has_logonresult() const;14 inline void clear_logonresult();15 inline LoginResult logonresult() const;16 inline void set_logonresult(LoginResult value);17 18 // required .UserInfo userInfo = 2;19 //下面的成员函数都是因message中定义的UserInfo字段而生成。20 //这里只是列出和非消息类型字段差异的部分。21 static const int kUserInfoFieldNumber = 2;22 inline bool has_userinfo() const;23 inline void clear_userinfo();24 inline const ::UserInfo& userinfo() const;25 //可以看到该类并没有生成用于设置和修改userInfo字段set_userinfo函数,而是将该工作26 //交给了下面的mutable_userinfo函数。因此每当调用函数之后,Protocol Buffer都会认为27 //该字段的值已经被设置了,同时has_userinfo函数亦将返回true。在实际编码中,我们可以28 //通过该函数返回userInfo字段的内部指针,并基于该指针完成userInfo成员变量的初始化工作。29 inline ::UserInfo* mutable_userinfo();30 inline ::UserInfo* release_userinfo();31 private:32 ... ...33 };下面是读写LogonRespMessage对象的C++测试代码和说明性注释。
1 void testNestedMessage() 2 { 3 printf("==================This is nested message.================/n"); 4 LogonRespMessage logonResp; 5 logonResp.set_logonresult(LOGON_RESULT_SUCCESS); 6 //如上所述,通过mutable_userinfo函数返回userInfo字段的指针,之后再初始化该对象指针。 7 UserInfo* userInfo = logonResp.mutable_userinfo(); 8 userInfo->set_acctid(200); 9 userInfo->set_name("Tester");10 userInfo->set_status(OFFLINE);11 int length = logonResp.ByteSize();12 char* buf = new char[length];13 logonResp.SerializeToArray(buf,length);14 15 LogonRespMessage logonResp2;16 logonResp2.ParseFromArray(buf,length);17 printf("LogonResult = %d, UserInfo->acctID = %I64d, UserInfo->name = %s, UserInfo->status = %d/n"18 ,logonResp2.logonresult(),logonResp2.userinfo().acctid(),logonResp2.userinfo().name().c_str(),logonResp2.userinfo().status());19 delete [] buf;20 }四、repeated嵌套message生成的C++代码。 message BuddyInfo { required UserInfo userInfo = 1; required int32 groupID = 2; } message RetrieveBuddiesResp { required int32 buddiesCnt = 1; repeated BuddyInfo buddiesInfo = 2; } 对于上述消息生成的代码,我们将只是针对RetrieveBuddiesResp消息所对应的C++代码进行详细说明,其余部分和前面小节的例子基本相同,可直接参照。而对于RetrieveBuddiesResp类中的代码,我们也仅仅是对buddiesInfo字段生成的代码进行更为详细的解释。
1 class RetrieveBuddiesResp : public ::google::protobuf::MessageLite { 2 public: 3 RetrieveBuddiesResp(); 4 virtual ~RetrieveBuddiesResp(); 5 6 ... ... //其余代码的功能性注释均可参照前面的例子。 7 8 // repeated .BuddyInfo buddiesInfo = 2; 9 static const int kBuddiesInfoFieldNumber = 2;10 //返回数组中成员的数量。11 inline int buddiesinfo_size() const;12 //清空数组中的所有已初始化成员,调用该函数后,buddiesinfo_size函数将返回0。13 inline void clear_buddiesinfo();14 //返回数组中指定下标所包含元素的引用。15 inline const ::BuddyInfo& buddiesinfo(int index) const;16 //返回数组中指定下标所包含元素的指针,通过该方式可直接修改元素的值信息。17 inline ::BuddyInfo* mutable_buddiesinfo(int index);18 //像数组中添加一个新元素。返回值即为新增的元素,可直接对其进行初始化。19 inline ::BuddyInfo* add_buddiesinfo();20 //获取buddiesInfo字段所表示的容器,该函数返回的容器仅用于遍历并读取,不能直接修改。21 inline const ::google::protobuf::RepeatedPtrField< ::BuddyInfo >&22 buddiesinfo() const;23 //获取buddiesInfo字段所表示的容器指针,该函数返回的容器指针可用于遍历和直接修改。24 inline ::google::protobuf::RepeatedPtrField< ::BuddyInfo >*25 mutable_buddiesinfo();26 private:27 ... ...28 };下面是读写RetrieveBuddiesResp对象的C++测试代码和说明性注释。
1 void testRepeatedMessage() 2 { 3 printf("==================This is repeated message.================/n"); 4 RetrieveBuddiesResp retrieveResp; 5 retrieveResp.set_buddiescnt(2); 6 BuddyInfo* buddyInfo = retrieveResp.add_buddiesinfo(); 7 buddyInfo->set_groupid(20); 8 UserInfo* userInfo = buddyInfo->mutable_userinfo(); 9 userInfo->set_acctid(200);10 userInfo->set_name("user1");11 userInfo->set_status(OFFLINE);12 13 buddyInfo = retrieveResp.add_buddiesinfo();14 buddyInfo->set_groupid(21);15 userInfo = buddyInfo->mutable_userinfo();16 userInfo->set_acctid(201);17 userInfo->set_name("user2");18 userInfo->set_status(ONLINE);19 20 int length = retrieveResp.ByteSize();21 char* buf = new char[length];22 retrieveResp.SerializeToArray(buf,length);23 24 RetrieveBuddiesResp retrieveResp2;25 retrieveResp2.ParseFromArray(buf,length);26 printf("BuddiesCount = %d/n",retrieveResp2.buddiescnt());27 printf("Repeated Size = %d/n",retrieveResp2.buddiesinfo_size());28 //这里仅提供了通过容器迭代器的方式遍历数组元素的测试代码。29 //事实上,通过buddiesinfo_size和buddiesinfo函数亦可循环遍历。30 RepeatedPtrField<BuddyInfo>* buddiesInfo = retrieveResp2.mutable_buddiesinfo();31 RepeatedPtrField<BuddyInfo>::iterator it = buddiesInfo->begin();32 for (; it != buddiesInfo->end(); ++it) {33 printf("BuddyInfo->groupID = %d/n", it->groupid());34 printf("UserInfo->acctID = %I64d, UserInfo->name = %s, UserInfo->status = %d/n"35 , it->userinfo().acctid(), it->userinfo().name().c_str(),it->userinfo().status());36 }37 delete [] buf;38 }最后需要说明的是,Protocol Buffer仍然提供了很多其它非常有用的功能,特别是针对序列化的目的地,比如文件流和网络流等。与此同时,也提供了完整的官方文档和规范的命名规则,在很多情况下,可以直接通过函数的名字便可获悉函数所完成的工作。 本打算将该Blog中使用的示例代码以附件的方式上传,但是没有发现此功能,望谅解。
Protocol Buffer技术详解(Java实例)
该篇Blog和上一篇(C++实例)基本相同,只是面向于我们团队中的Java工程师,毕竟我们项目的前端部分是基于Android开发的,而且我们研发团队中目前主要使用的开发语言就是C++、Java和Python,其中Python主要用于编写各种工具程序。然而为了保证该篇Blog的完整性和独立性,我仍然会将上一篇Blog中已经出现的内容再一次赘述,同时对于Java中特有的部分也会着重介绍。 一、生成目标语言代码。 下面的命令帮助我们将MyMessage.proto文件中定义的一组Protocol Buffer格式的消息编译成目标语言(Java)的代码。至于消息的内容,我们会在后面以分段的形式逐一列出,同时也会在附件中给出所有源代码。 protoc -I=./message --java_out=./src ./MyMessage.proto 从上面的命令行参数中可以看出,待编译的文件为MyMessage.proto,他存放在当前目录的message子目录下。--java_out参数则指示编译工具我们需要生成目标语言是java,输出目录是当前目录的src子目录。这里需要补充说明的是,因为在MyMessage.proto文件中定义了option java_package = "com.lsk.lyphone"的文件级选项,所以输出的目前是src/com/lsk/lyphone,生成的目标代码文件名是MyMessage.java。 二、简单message生成的Java代码。 这里先定义一个最简单的message,其中只是包含原始类型的字段。 option java_package = "com.lsk.lyphone"; option java_outer_classname = "LYPhoneMessage"; option optimize_for = LITE_RUNTIME; message LogonReqMessage { required int64 acctID = 1; required string passwd = 2; } 对于选项java_package和java_outer_classname的功能,我们已经在之前的一篇Blog(语言规范)中进行了清晰的阐述,这里就不在另行介绍了。然而对于选项optimize_for,这里将结合本例给出一些实用性描述。 当前.proto文件中该选项的值为LITE_RUNTIME,因此由该.proto文件生成的所有Java类的父类均为com.google.protobuf.GeneratedMessageLite,而非com.google.protobuf.GeneratedMessage,同时与之对应的Builder类则均继承自com.google.protobuf.MessageLiteOrBuilder,而非com.google.protobuf.MessageOrBuilder。在之前的博客中已经给出了一些简要的说明,MessageLite接口是Message的父接口,在MessageLite中将缺少Protocol Buffer对反射的支持,而此功能均在Message接口中提供了接口规范,同时又在其实现类GeneratedMessage中给予了最小功能的实现。对于我们的项目而言,整个系统相对比较封闭,不会和更多的外部程序进行交互,与此同时,我们的客户端部分又是运行在Android平台,有鉴于此,我们考虑使用LITE版本的Protocol Buffer。这样不仅可以得到更高编码效率,而且生成代码编译后所占用的资源也会更少,至于反射所能带来的灵活性和极易扩展性,对于该项目而言完全可以忽略。下面我们来看一下由message LogonReqMessage生成的Java类的部分声明,以及常用方法的说明性注释。 在做各种case的对比性分析之前必须要事先声明的是,Protocol Buffer针对Java语言所生成的代码和C++相比存在一个非常重要的差别,即为每个消息均会生成一个Builder接口和一个与消息对应的实现类,该实现类又将同时实现生成的Builder接口和扩展Protocol Buffer内置的GeneratedMessageLite(或GeneratedMessage)类。这一点对于Protocol Buffer而言,是巧妙的使用了设计模式中的Builder模式。换言之,对于所有消息字段的修改操作均需要通过与其对应的Builder接口辅助完成。相信我们会通过对下面用例的学习可以得到更为清楚的认识。
1 //用于修改LogonReqMessage消息字段的辅助Builder接口。 2 //该接口会为消息中的每个字段均提供getter和setter方法。 3 public interface LogonReqMessageOrBuilder 4 extends com.google.protobuf.MessageLiteOrBuilder { 5 6 // required int64 acctID = 1; 7 boolean hasAcctID(); 8 long getAcctID(); 9 10 // required string passwd = 2; 11 boolean haspasswd(); 12 String getPasswd(); 13 } 14 //该类为final类,即不可以在被子类化了。这一点在Protocol Buffer的官方文档中给予了明确 15 //的说明,因为子类化将会破坏序列化和反序列化的过程。 16 public static final class LogonReqMessage extends 17 com.google.protobuf.GeneratedMessageLite 18 implements LogonReqMessageOrBuilder { 19 20 // Use LogonReqMessage.newBuilder() to construct. 21 // 由于所有构造函数均为私有方法,由此可见,我们不能直接new LogonReqMessage的对象 22 // 实例,而是只能通过与其对应Builder来构造,或是直接通过反序列化的方式生成。 23 private LogonReqMessage(Builder builder) { 24 super(builder); 25 } 26 //该静态方法为该类Builder接口的工厂方法。返回的Builder实现类在完成各个字段的 27 //初始化后,通过build()方法返回与其对应的消息实现类,即LogonReqMessage。 28 public static Builder newBuilder() { return Builder.create(); } 29 //通过该类的对象获取与其对应的Builder类对象,一般用于通过Builder类完成消息字段的修改。 30 public Builder toBuilder() { return newBuilder(this); } 31 32 private LogonReqMessage(boolean noInit) {} 33 //判断当前对象的所有字段是否都已经被初始化。 34 public final boolean isInitialized() { 35 ... ... 36 } 37 //获取已经被初始化后的对象序列化时所占用的字节空间。 38 public int getSerializedSize() { 39 ... ... 40 } 41 //从内存中饭序列化LogonReqMessage对象。 42 //Protocol Buffer中还提供其他一些接口方法,用于从不同的数据源反序列化对象。 43 public static com.lsk.lyphone.LYPhoneMessage.LogonReqMessage parseFrom(byte[] data) 44 throws com.google.protobuf.InvalidProtocolBufferException { 45 return newBuilder().mergeFrom(data).buildParsed(); 46 } 47 //功能和上一个函数相同,只是输入源改为InputStream接口。 48 public static com.lsk.lyphone.LYPhoneMessage.LogonReqMessage parseFrom(java.io.InputStream input) 49 throws java.io.IOException { 50 return newBuilder().mergeFrom(input).buildParsed(); 51 } 52 53 // required int64 acctID = 1; 54 // 下面的静态变量对应于该字段在.proto中定义的标签号。该变量的命名规则为:字段(全部大写) + _FIELD_NUMBER。 55 public static final int ACCTID_FIELD_NUMBER = 1; 56 public boolean hasAcctID() { 57 return ((bitField0_ & 0x00000001) == 0x00000001); 58 } 59 public long getAcctID() { 60 return acctID_; 61 } 62 63 // required string passwd = 2; 64 public static final int PASSWD_FIELD_NUMBER = 2; 65 public boolean hasPasswd() { 66 return ((bitField0_ & 0x00000002) == 0x00000002); 67 } 68 public String getPasswd() { 69 ... ... 70 } 71 //每一个Message类都会包含一个静态内部类,即与之对应的Builder类。上面代码中所涉及的Builder类均为该内部类。 72 public static final class Builder extends 73 com.google.protobuf.GeneratedMessageLite.Builder< 74 com.lsk.lyphone.LYPhoneMessage.LogonReqMessage, Builder> 75 implements com.lsk.lyphone.LYPhoneMessage.LogonReqMessageOrBuilder { 76 //清空当前对象中的所有设置。调用该函数之后,本例中的hasAcctID和hasPasswd都会返回false。 77 public Builder clear() { 78 super.clear(); 79 acctID_ = 0L; 80 bitField0_ = (bitField0_ & ~0x00000001); 81 passwd_ = ""; 82 bitField0_ = (bitField0_ & ~0x00000002); 83 return this; 84 } 85 //克隆出一个Builder对象。 86 public Builder clone() { 87 return create().mergeFrom(buildPartial()); 88 } 89 public com.lsk.lyphone.LYPhoneMessage.LogonReqMessage build() { 90 com.lsk.lyphone.LYPhoneMessage.LogonReqMessage result = buildPartial(); 91 if (!result.isInitialized()) { 92 throw newUninitializedMessageException(result); 93 } 94 return result; 95 } 96 // Builder类中修改外部消息类的方法。 97 // required int64 acctID = 1; 98 public boolean hasAcctID() { 99 return ((bitField0_ & 0x00000001) == 0x00000001);100 }101 public long getAcctID() {102 return acctID_;103 }104 //设置AcctID字段,该函数调用后hasAcctID函数将返回true。105 //这里之所以让返回值为Builder对象,就是可以让调用者在一条代码中方便的连续修改多个字段,106 //如:myMessage.setAcctID(100).setPasswd("MyName");107 public Builder setAcctID(long value) {108 bitField0_ |= 0x00000001;109 acctID_ = value;110 return this;111 }112 //清空AcctID字段,该函数调用后hasAcctID函数返回false。113 //这里之所以让返回值为Builder对象,就是可以让调用者在一条代码中方便的连续清空多个字段,114 //如:myMessage.clearAcctID().clearPasswd();115 public Builder clearAcctID() {116 bitField0_ = (bitField0_ & ~0x00000001);117 acctID_ = 0L;118 return this;119 }120 121 // required string passwd = 2;122 public boolean hasPasswd() {123 return ((bitField0_ & 0x00000002) == 0x00000002);124 }125 public String getPasswd() {126 ... ... 127 }128 public Builder setPasswd(String value) {129 ... ...130 }131 public Builder clearPasswd() {132 bitField0_ = (bitField0_ & ~0x00000002);133 passwd_ = getDefaultInstance().getPasswd();134 return this;135 }136 void setPasswd(com.google.protobuf.ByteString value) {137 bitField0_ |= 0x00000002;138 passwd_ = value;139 }140 }141 }在上面生成的代码中并没有列出与序列化相关的函数,这部分代码基本都是在父类中实现的,我们将在下面的例子中给出一些最基本的用法,有兴趣的开发者可以直接看Protocol Buffer中的源码,这部分代码比较通俗易懂。 下面是读写LogonReqMessage对象的Java测试代码和说明性注释。
1 private static void testSimpleMessage() { 2 System.out.println("==================This is simple message.================"); 3 //如前所述,不能直接构造该消息类对象,只能通过他的内部Builder类构造并完成所有字段的初始化。 4 LogonReqMessage.Builder logonReqBuilder = LogonReqMessage.newBuilder(); 5 logonReqBuilder.setAcctID(20); 6 logonReqBuilder.setPasswd("Hello World"); 7 //builder对象初始化完毕后,再通过build方法生成与之对应的消息类对象。 8 LogonReqMessage logonReq = logonReqBuilder.build(); 9 int length = logonReq.getSerializedSize();10 System.out.println("The result length is " + length);11 //直接序列化到内存中,之后可对该内存进行二次加工后再写到本地文件或发送到远端,如加密。12 byte[] buf = logonReq.toByteArray();13 14 try {15 LogonReqMessage logonReq2 = LogonReqMessage.parseFrom(buf);16 System.out.println("acctID = " + logonReq2.getAcctID() + "/tpassword = " + logonReq2.getPasswd());17 } catch (InvalidProtocolBufferException e) {18 e.printStackTrace();19 }20 //需要说明的是,文件中的内容是由之前C++实例代码写入的,这里这样写主要是一种验证。21 System.out.println("Reading data from local file generated by C++");22 try {23 LogonReqMessage logonReq3 = LogonReqMessage.parseFrom(new FileInputStream("C:/Mine/LogonReq.dat"));24 System.out.println("acctID = " + logonReq3.getAcctID() + "/tpassword = " + logonReq3.getPasswd());25 } catch (FileNotFoundException e) {26 e.printStackTrace();27 } catch (IOException e) {28 e.printStackTrace();29 }30 }三、嵌套message生成的Java代码。 enum UserStatus { OFFLINE = 0; ONLINE = 1; } enum LoginResult { LOGON_RESULT_SUCCESS = 0; LOGON_RESULT_NOTEXIST = 1; LOGON_RESULT_ERROR_PASSWD = 2; LOGON_RESULT_ALREADY_LOGON = 3; LOGON_RESULT_SERVER_ERROR = 4; } message UserInfo { required int64 acctID = 1; required string name = 2; required UserStatus status = 3; } message LogonRespMessage { required LoginResult logonResult = 1; required UserInfo userInfo = 2; //这里嵌套了UserInfo消息。 } 对于上述消息生成的Java代码,UserInfo因为只是包含了原始类型字段,因此和上例中的LogonReqMessage没有太多的差别,这里也就不在重复列出了。由于LogonRespMessage消息中嵌套了UserInfo类型的字段,在这里我们将仅仅给出该消息生成的Java代码和关键性注释。
1 public static final class LogonRespMessage extends 2 com.google.protobuf.GeneratedMessageLite 3 implements LogonRespMessageOrBuilder { 4 5 //Message类的通用性函数定义。 6 ... ... 7 8 // required .LoginResult logonResult = 1; 9 public static final int LOGONRESULT_FIELD_NUMBER = 1;10 public boolean hasLogonResult() {11 return ((bitField0_ & 0x00000001) == 0x00000001);12 }13 public com.lsk.lyphone.LYPhoneMessage.LoginResult getLogonResult() {14 return logonResult_;15 }16 17 // required .UserInfo userInfo = 2;18 public static final int USERINFO_FIELD_NUMBER = 2;19 public boolean hasUserInfo() {20 return ((bitField0_ & 0x00000002) == 0x00000002);21 }22 public com.lsk.lyphone.LYPhoneMessage.UserInfo getUserInfo() {23 return userInfo_;24 }25 //Message类的通用性函数定义。可参照上一小节中的代码和注释。26 ... ...27 28 public static final class Builder extends29 com.google.protobuf.GeneratedMessageLite.Builder<30 com.lsk.lyphone.LYPhoneMessage.LogonRespMessage, Builder>31 implements com.lsk.lyphone.LYPhoneMessage.LogonRespMessageOrBuilder {32 33 //一些适用于绝大多数Builder对象的通用性方法。34 ... ...35 36 //当前示例中Builder生成的代码和上一小节中生成的代码非常类似,这里就不一一赘述了。37 //和前面的例子相比一个重要的差别是setUserInfo函数多提供了一种函数签名,其参数为38 //UserInfo类的Builder对象。这样调用者在使用时可以直接将Builder对象作为参数传入。39 public Builder setUserInfo(com.lsk.lyphone.LYPhoneMessage.UserInfo.Builder builderForValue) {40 userInfo_ = builderForValue.build();41 bitField0_ |= 0x00000002;42 return this;43 }44 }45 }下面是读写LogonRespMessage对象的Java测试代码和说明性注释。
1 private static void testNestedMessage() { 2 System.out.println("==================This is nested message.================"); 3 LogonRespMessage.Builder logonRespBuilder = LogonRespMessage.newBuilder(); 4 logonRespBuilder.setLogonResult(LoginResult.LOGON_RESULT_SUCCESS); 5 UserInfo.Builder userInfo = UserInfo.newBuilder(); 6 userInfo.setAcctID(200); 7 userInfo.setName("Tester"); 8 userInfo.setStatus(UserStatus.OFFLINE); 9 //这里也可以直接传递userInfo对象作为参数。因为LogonRespBuilder类提供了setUserInfo的方法重载。10 logonRespBuilder.setUserInfo(userInfo.build());11 LogonRespMessage logonResp = logonRespBuilder.build();12 int length = logonResp.getSerializedSize();13 System.out.println("The result length is " + length);14 byte[] buf = logonResp.toByteArray();15 16 try {17 LogonRespMessage logonResp2 = LogonRespMessage.parseFrom(buf);18 UserInfo userInfo2 = logonResp2.getUserInfo();19 System.out.println("LogonResult = " + logonResp2.getLogonResult().toString() + " acctID = " 20 + userInfo2.getAcctID() + " name = " + userInfo2.getName() + " status = " + userInfo2.getStatus().toString());21 } catch (InvalidProtocolBufferException e) {22 e.printStackTrace();23 }24 System.out.println("Reading data from local file generated by C++");25 try {26 LogonRespMessage logonResp3 = LogonRespMessage.parseFrom(new FileInputStream("C:/Mine/LogonResp.dat"));27 UserInfo userInfo3 = logonResp3.getUserInfo();28 System.out.println("LogonResult = " + logonResp3.getLogonResult().toString() + " acctID = " 29 + userInfo3.getAcctID() + " name = " + userInfo3.getName() + " status = " + userInfo3.getStatus().toString());30 } catch (FileNotFoundException e) {31 e.printStackTrace();32 } catch (IOException e) {33 e.printStackTrace();34 }35 }四、repeated嵌套message生成的Java代码。 message BuddyInfo { required UserInfo userInfo = 1; required int32 groupID = 2; } message RetrieveBuddiesResp { required int32 buddiesCnt = 1; repeated BuddyInfo buddiesInfo = 2; } 对于上述消息生成的代码,我们将只是针对RetrieveBuddiesResp消息所对应的Java代码进行详细说明,其余部分和前面小节的例子基本相同,可直接参照。而对于RetrieveBuddiesResp类中的代码,我们也仅仅是对buddiesInfo字段生成的代码进行更为详细的解释。
1 public static final class RetrieveBuddiesResp extends 2 com.google.protobuf.GeneratedMessageLite 3 implements RetrieveBuddiesRespOrBuilder { 4 //这里均为Protocol Buffer生成的通用性代码。 5 ... ... 6 // repeated .BuddyInfo buddiesInfo = 2; 7 public static final int BUDDIESINFO_FIELD_NUMBER = 2; 8 //对于repeated类型的字段,均返回类型参数为字段类型的泛型容器对象。 9 public java.util.List<com.lsk.lyphone.LYPhoneMessage.BuddyInfo> getBuddiesInfoList() {10 return buddiesInfo_;11 }12 public java.util.List<? extends com.lsk.lyphone.LYPhoneMessage.BuddyInfoOrBuilder> getBuddiesInfoOrBuilderList() {13 return buddiesInfo_;14 }15 public int getBuddiesInfoCount() {16 return buddiesInfo_.size();17 }18 public com.lsk.lyphone.LYPhoneMessage.BuddyInfo getBuddiesInfo(int index) {19 return buddiesInfo_.get(index);20 }21 public com.lsk.lyphone.LYPhoneMessage.BuddyInfoOrBuilder getBuddiesInfoOrBuilder(int index) {22 return buddiesInfo_.get(index);23 }24 25 //这里仍有一些Protocol Buffer生成的通用性代码。26 ... ...27 28 public static final class Builder extends29 com.google.protobuf.GeneratedMessageLite.Builder<30 com.lsk.lyphone.LYPhoneMessage.RetrieveBuddiesResp, Builder>31 implements com.lsk.lyphone.LYPhoneMessage.RetrieveBuddiesRespOrBuilder {32 33 //这里仅列出和操作repeated字段相关的方法,其他的方法和前面的例子基本一致。34 // repeated .BuddyInfo buddiesInfo = 2;35 //本来打算给出比较详细的说明,但是看到Google为每个函数的命名之后就放弃这个想法,36 //这样一来不仅可以避免画蛇添足,而且也节省了时间。:) 37 public java.util.List<com.lsk.lyphone.LYPhoneMessage.BuddyInfo> getBuddiesInfoList() {38 return java.util.Collections.unmodifiableList(buddiesInfo_);39 }40 public int getBuddiesInfoCount() {41 return buddiesInfo_.size();42 }43 public com.lsk.lyphone.LYPhoneMessage.BuddyInfo getBuddiesInfo(int index) {44 return buddiesInfo_.get(index);45 }46 public Builder setBuddiesInfo(int index, com.lsk.lyphone.LYPhoneMessage.BuddyInfo value) {47 ... ...48 }49 public Builder setBuddiesInfo(int index, com.lsk.lyphone.LYPhoneMessage.BuddyInfo.Builder builderForValue) {50 ... ...51 }52 public Builder addBuddiesInfo(com.lsk.lyphone.LYPhoneMessage.BuddyInfo value) {53 ... ...54 }55 public Builder addBuddiesInfo(int index, com.lsk.lyphone.LYPhoneMessage.BuddyInfo value) {56 ... ...57 }58 public Builder addBuddiesInfo(com.lsk.lyphone.LYPhoneMessage.BuddyInfo.Builder builderForValue) {59 ... ...60 }61 public Builder addBuddiesInfo(62 int index, com.lsk.lyphone.LYPhoneMessage.BuddyInfo.Builder builderForValue) {63 ... ...64 }65 public Builder addAllBuddiesInfo(66 java.lang.Iterable<? extends com.lsk.lyphone.LYPhoneMessage.BuddyInfo> values) {67 ... ...68 }69 public Builder clearBuddiesInfo() {70 ... ...71 }72 public Builder removeBuddiesInfo(int index) {73 ... ...74 }75 }76 }下面是读写RetrieveBuddiesResp对象的Java测试代码和说明性注释。
1 private static void testRepeatedMessage() { 2 System.out.println("==================This is repeated message.================"); 3 RetrieveBuddiesResp.Builder retrieveBuddiesBuilder = RetrieveBuddiesResp.newBuilder(); 4 retrieveBuddiesBuilder.setBuddiesCnt(2); 5 BuddyInfo.Builder buddyInfoBuilder = BuddyInfo.newBuilder(); 6 buddyInfoBuilder.setGroupID(20); 7 UserInfo.Builder userInfoBuilder = UserInfo.newBuilder(); 8 userInfoBuilder.setAcctID(200); 9 userInfoBuilder.setName("user1");10 userInfoBuilder.setStatus(UserStatus.OFFLINE);11 buddyInfoBuilder.setUserInfo(userInfoBuilder.build());12 retrieveBuddiesBuilder.addBuddiesInfo(buddyInfoBuilder.build());13 14 buddyInfoBuilder = BuddyInfo.newBuilder();15 buddyInfoBuilder.setGroupID(21);16 userInfoBuilder = UserInfo.newBuilder();17 userInfoBuilder.setAcctID(201);18 userInfoBuilder.setName("user2");19 userInfoBuilder.setStatus(UserStatus.ONLINE);20 buddyInfoBuilder.setUserInfo(userInfoBuilder);21 retrieveBuddiesBuilder.addBuddiesInfo(buddyInfoBuilder);22 RetrieveBuddiesResp buddiesResp = retrieveBuddiesBuilder.build();23 24 int length = buddiesResp.getSerializedSize();25 System.out.println("The result length is " + length);26 byte[] buf = buddiesResp.toByteArray();27 28 try {29 RetrieveBuddiesResp buddiesResp2 = RetrieveBuddiesResp.parseFrom(buf);30 System.out.println("BuddiesCount = " + buddiesResp2.getBuddiesCnt());31 System.out.println("Repeated Size = " + buddiesResp2.getBuddiesInfoCount());32 for (int i = 0; i < buddiesResp2.getBuddiesInfoCount(); ++i) {33 BuddyInfo buddyInfo = buddiesResp2.getBuddiesInfo(i);34 UserInfo userInfo = buddyInfo.getUserInfo();35 System.out.println("GroupID = " + buddyInfo.getGroupID() + " UserInfo.acctID = " + userInfo.getAcctID()36 + " UserInfo.name = " + userInfo.getName() + " UserInfo.status = " + userInfo.getStatus());37 }38 39 } catch (InvalidProtocolBufferException e) {40 e.printStackTrace();41 }42 System.out.println("Reading data from local file generated by C++");43 try {44 RetrieveBuddiesResp buddiesResp3 = RetrieveBuddiesResp.parseFrom(new FileInputStream("C:/Mine/RetrieveBuddiesResp.dat"));45 System.out.println("BuddiesCount = " + buddiesResp3.getBuddiesCnt());46 System.out.println("Repeated Size = " + buddiesResp3.getBuddiesInfoCount());47 List<BuddyInfo> buddiesInfo = buddiesResp3.getBuddiesInfoList();48 for (BuddyInfo buddyInfo : buddiesInfo) {49 UserInfo userInfo = buddyInfo.getUserInfo();50 System.out.println("GroupID = " + buddyInfo.getGroupID() + " UserInfo.acctID = " + userInfo.getAcctID()51 + " UserInfo.name = " + userInfo.getName() + " UserInfo.status = " + userInfo.getStatus());52 }53 } catch (FileNotFoundException e) {54 e.printStackTrace();55 } catch (IOException e) {56 e.printStackTrace();57 }58 }对于Java而言,我们可以通过Maven工具生成两个jar包,其中一个是protobuf-java-2.4.1.jar,主要用于optimize_for选项为非LITE_RUNTIME的情况,而另一个protobuf-java-2.4.1-lite.jar文件则恰恰与之相反。另外,我通过Beyond Compare工具对这两个jar包进行了二进制比较后发现,他们是完全相同的。这里之所以仍以LITE版本为例,主要还是因为和之前一篇Blog(C++实例)想匹配。 最后需要说明的是,Protocol Buffer仍然提供了很多其它非常有用的功能,特别是针对序列化的目的地,比如文件流和网络流等。与此同时,也提供了完整的官方文档和规范的命名规则,在很多情况下,可以直接通过函数的名字便可获悉函数所完成的工作。
新闻热点
疑难解答