Caffemodel数据结构解析与Protocol Buffer技术详解(C++实例)

Caffe中,数据的读取、运算、存储都是采用Google Protocol Buffer来进行的,所以首先来较为详细的介绍下Protocol Buffer(PB)。

PB是一种轻便、高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。是一种效率和兼容性都很优秀的二进制数据传输格式,目前提供了 C++、Java、Python 三种语言的 API。Caffe采用的是C++和Python的API。

接下来,我用一个简单的例子来说明一下。

使用PB和 C++ 编写一个十分简单的例子程序。该程序由两部分组成。第一部分被称为Writer,第二部分叫做Reader。Writer 负责将一些结构化的数据写入一个磁盘文件,Reader则负责从该磁盘文件中读取结构化数据并打印到屏幕上。准备用于演示的结构化数据是HelloWorld,它包含两个基本数据:

ID,为一个整数类型的数据;

Str,这是一个字符串。

首先我们需要编写一个proto文件,定义我们程序中需要处理的结构化数据,Caffe是定义在caffe.proto文件中。在PB的术语中,结构化数据被称为 Message。proto文件非常类似java或C语言的数据定义。代码清单 1 显示了例子应用中的proto文件内容。

清单 1. proto 文件
 1 package lm; 
 2 
 3 message helloworld 
 4 
 5  { 
 6 
 7     required int32     id = 1;   // ID    
 8 
 9     required string    str = 2;  // str 
10 
11     optional int32     opt = 3;  // optional field 
12 
13  }
View Code

一个比较好的习惯是认真对待proto文件的文件名。比如将命名规则定于如下: packageName.MessageName.proto

在上例中,package名字叫做 lm,定义了一个消息helloworld,该消息有三个成员,类型为int32的id,另一个为类型为string的成员str。optional是一个可选的成员,即消息中可以不包含该成员,required表明是必须包含该成员。一般在定义中会出现如下三个字段属性:

对于required的字段而言,初值是必须要提供的,否则字段的便是未初始化的。 在Debug模式的buffer库下编译的话,序列化话的时候可能会失败,而且在反序列化的时候对于该字段的解析会总是失败的。所以,对于修饰符为required的字段,请在序列化的时候务必给予初始化。

对于optional的字段而言,如果未进行初始化,那么一个默认值将赋予该字段,当然也可以指定默认值。

对于repeated的字段而言,该字段可以重复多个,谷歌提供的这个 addressbook例子便有个很好的该修饰符的应用场景,即每个人可能有多个电话号码。在高级语言里面,我们可以通过数组来实现,而在proto定义文件中可以使用repeated来修饰,从而达到相同目的。当然,出现0次也是包含在内的。

写好proto文件之后就可以用PB编译器(protoc)将该文件编译成目标语言了。本例中我们将使用C++。假设proto文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:

1  protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
View Code

命令将生成两个文件:

lm.helloworld.pb.h, 定义了C++ 类的头文件;

lm.helloworld.pb.cc,C++类的实现文件。

在生成的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将使用这个类来对消息进行操作。诸如对消息的成员进行赋值,将消息序列化等等都有相应的方法。

如前所述,Writer将把一个结构化数据写入磁盘,以便其他人来读取。假如我们不使用 PB,其实也有许多的选择。一个可能的方法是将数据转换为字符串,然后将字符串写入磁盘。转换为字符串的方法可以使用 sprintf(),这非常简单。数字 123 可以变成字符串”123”。这样做似乎没有什么不妥,但是仔细考虑一下就会发现,这样的做法对写Reader的那个人的要求比较高,Reader 的作者必须了解Writer 的细节。比如”123”可以是单个数字 123,但也可以是三个数字 1、2 和 3等等。这么说来,我们还必须让Writer定义一种分隔符一样的字符,以便Reader可以正确读取。但分隔符也许还会引起其他的什么问题。最后我们发现一个简单的Helloworld 也需要写许多处理消息格式的代码。

如果使用 PB,那么这些细节就可以不需要应用程序来考虑了。使用PB,Writer 的工作很简单,需要处理的结构化数据由 .proto 文件描述,经过上一节中的编译过程后,该数据化结构对应了一个 C++ 的类,并定义在 lm.helloworld.pb.h 中。对于本例,类名为lm::helloworld。

Writer 需要include该头文件,然后便可以使用这个类了。现在,在Writer代码中,将要存入磁盘的结构化数据由一个lm::helloworld类的对象表示,它提供了一系列的 get/set 函数用来修改和读取结构化数据中的数据成员,或者叫field。

当我们需要将该结构化数据保存到磁盘上时,类 lm::helloworld 已经提供相应的方法来把一个复杂的数据变成一个字节序列,我们可以将这个字节序列写入磁盘。

对于想要读取这个数据的程序来说,也只需要使用类 lm::helloworld 的相应反序列化方法来将这个字节序列重新转换会结构化数据。这同我们开始时那个“123”的想法类似,不过PB想的远远比我们那个粗糙的字符串转换要全面,因此,我们可以放心将这类事情交给PB吧。程序清单 2 演示了 Writer 的主要代码。

清单 2. Writer 的主要代码
 1  #include "lm.helloworld.pb.h"
 2 
 3  4 
 5  int main(void) 
 6 
 7  { 
 8 
 9   lm::helloworld msg1; 
10 
11   msg1.set_id(101);          //设置id
12 
13   msg1.set_str(“hello”);   //设置str
14 
15   // 向磁盘中写入数据流fstream 
16 
17   fstream output("./log", ios::out | ios::trunc | ios::binary);  
18 
19   if (!msg1.SerializeToOstream(&output)) { 
20 
21       cerr << "Failed to write msg." << endl; 
22 
23       return -1; 
24 
25   }         
26 
27   return 0; 
28 
29  }
View Code

Msg1 是一个helloworld类的对象,set_id()用来设置id的值。SerializeToOstream将对象序列化后写入一个fstream流。我们可以写出Reader代码,程序清单3列出了 reader 的主要代码。

清单 3. Reader的主要代码
 1 #include "lm.helloworld.pb.h" 
 2 
 3  4 
 5  void ListMsg(const lm::helloworld & msg) { 
 6 
 7   cout << msg.id() << endl; 
 8 
 9   cout << msg.str() << endl; 
10 
11  } 
12 
13  int main(int argc, char* argv[]) { 
14 
15   lm::helloworld msg1; 
16 
17   { 
18 
19     fstream input("./log", ios::in | ios::binary); 
20 
21     if (!msg1.ParseFromIstream(&input)) { 
22 
23       cerr << "Failed to parse address book." << endl; 
24 
25       return -1; 
26 
27     } 
28 
29   } 
30 
31   ListMsg(msg1); 
32 
33 34 
35  }
View Code

同样,Reader 声明类helloworld的对象msg1,然后利用ParseFromIstream从一个fstream流中读取信息并反序列化。此后,ListMsg中采用get方法读取消息的内部信息,并进行打印输出操作。

运行Writer和Reader的结果如下:

 >writer 
 >reader 
 101 
 Hello

Reader 读取文件 log 中的序列化信息并打印到屏幕上。这个例子本身并无意义,但只要稍加修改就可以将它变成更加有用的程序。比如将磁盘替换为网络 socket,那么就可以实现基于网络的数据交换任务。而存储和交换正是PB最有效的应用领域。

到这里为止,我们只给出了一个简单的没有任何用处的例子。在实际应用中,人们往往需要定义更加复杂的 Message。我们用“复杂”这个词,不仅仅是指从个数上说有更多的 fields 或者更多类型的 fields,而是指更加复杂的数据结构:嵌套 Message,Caffe.proto文件中定义了大量的嵌套Message。使得Message的表达能力增强很多。代码清单 4 给出一个嵌套 Message 的例子。

清单 4. 嵌套 Message 的例子
 1  message Person {
 2   required string name = 1;
 3   required int32 id = 2;        // Unique ID number for this person.
 4   optional string email = 3;
 5   enum PhoneType {
 6     MOBILE = 0;
 7     HOME = 1;
 8     WORK = 2;
 9   }
10  
11   message PhoneNumber {
12     required string number = 1;
13     optional PhoneType type = 2 [default = HOME];
14   }
15   repeated PhoneNumber phone = 4;
16  }
View Code

在 Message Person 中,定义了嵌套消息 PhoneNumber,并用来定义 Person 消息中的 phone 域。这使得人们可以定义更加复杂的数据结构。

以上部分参考网址:http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/

在Caffe中也是类似于上例中的Writer和Reader去读写PB数据的。接下来,具体说明下Caffe中是如何存储Caffemodel的。在Caffe主目录下的solver.cpp文件中的一段代码展示了Caffe是如何存储Caffemodel的,代码清单5如下:

清单 5. Caffemodel存储代码
 1 template 
 2 
 3 void Solver::Snapshot() {
 4 
 5   NetParameter net_param;    // NetParameter为网络参数类
 6 
 7   // 为了中间结果,也会写入梯度值
 8 
 9   net_->ToProto(&net_param, param_.snapshot_diff());
10 
11   string filename(param_.snapshot_prefix());
12 
13   string model_filename, snapshot_filename;
14 
15   const int kBufferSize = 20;
16 
17   char iter_str_buffer[kBufferSize];
18 
19   // 每训练完1次,iter_就加1 
20 
21 snprintf(iter_str_buffer, kBufferSize, "_iter_%d", iter_ + 1);
22 
23   filename += iter_str_buffer;
24 
25   model_filename = filename + ".caffemodel"; //XX_iter_YY.caffemodel
26 
27   LOG(INFO) << "Snapshotting to " << model_filename;
28 
29   // 向磁盘写入网络参数
30 
31   WriteProtoToBinaryFile(net_param, model_filename.c_str());
32 
33   SolverState state;
34 
35   SnapshotSolverState(&state);
36 
37   state.set_iter(iter_ + 1);    //set
38 
39   state.set_learned_net(model_filename);
40 
41   state.set_current_step(current_step_);
42 
43   snapshot_filename = filename + ".solverstate";
44 
45   LOG(INFO) << "Snapshotting solver state to " << snapshot_filename;
46 
47   // 向磁盘写入网络state
48 
49   WriteProtoToBinaryFile(state, snapshot_filename.c_str());
50 
51 }
View Code

在清单5代码中,我们可以看到,其实Caffemodel存储的数据也就是网络参数net_param的PB,Caffe可以保存每一次训练完成后的网络参数,我们可以通过XX.prototxt文件来进行参数设置。在这里的 WriteProtoToBinaryFile函数与之前HelloWorld例子中的Writer函数类似,在这就不在贴出。那么我们只要弄清楚NetParameter类的组成,也就明白了Caffemodel的具体数据构成。在caffe.proto这个文件中定义了NetParameter类,如代码清单6所示。

清单6. Caffemodel存储代码
  1 message NetParameter {
  2 
  3   optional string name = 1;   // 网络名称
  4 
  5   repeated string input = 3;  // 网络输入input blobs
  6 
  7   repeated BlobShape input_shape = 8; // The shape of the input blobs
  8 
  9   // 输入维度blobs,4维(num, channels, height and width)
 10 
 11   repeated int32 input_dim = 4;
 12 
 13   // 网络是否强制每层进行反馈操作开关
 14 
 15 // 如果设置为False,则会根据网络结构和学习率自动确定是否进行反馈操作
 16 
 17   optional bool force_backward = 5 [default = false];
 18 
 19 // 网络的state,部分网络层依赖,部分不依赖,需要看具体网络
 20 
 21   optional NetState state = 6;
 22 
 23   // 是否打印debug log
 24 
 25   optional bool debug_info = 7 [default = false];
 26 
 27   // 网络层参数,Field Number 为100,所以网络层参数在最后
 28 
 29   repeated LayerParameter layer = 100; 
 30 
 31   // 弃用: 用 'layer' 代替
 32 
 33   repeated V1LayerParameter layers = 2;
 34 
 35 }
 36 
 37 // Specifies the shape (dimensions) of a Blob.
 38 
 39 message BlobShape {
 40 
 41   repeated int64 dim = 1 [packed = true];
 42 
 43 }
 44 
 45 message BlobProto {
 46 
 47   optional BlobShape shape = 7;
 48 
 49   repeated float data = 5 [packed = true];
 50 
 51   repeated float diff = 6 [packed = true];
 52 
 53   optional int32 num = 1 [default = 0];
 54 
 55   optional int32 channels = 2 [default = 0];
 56 
 57   optional int32 height = 3 [default = 0];
 58 
 59   optional int32 width = 4 [default = 0];
 60 
 61 }
 62 
 63  
 64 
 65 // The BlobProtoVector is simply a way to pass multiple blobproto instances
 66 
 67 around.
 68 
 69 message BlobProtoVector {
 70 
 71   repeated BlobProto blobs = 1;
 72 
 73 }
 74 
 75 message NetState {
 76 
 77   optional Phase phase = 1 [default = TEST];
 78 
 79   optional int32 level = 2 [default = 0];
 80 
 81   repeated string stage = 3;
 82 
 83 }
 84 
 85 message LayerParameter {
 86 
 87   optional string name = 1;   // the layer name
 88 
 89   optional string type = 2;   // the layer type
 90 
 91   repeated string bottom = 3; // the name of each bottom blob
 92 
 93   repeated string top = 4;    // the name of each top blob
 94 
 95   // The train/test phase for computation.
 96 
 97   optional Phase phase = 10;
 98 
 99   // Loss weight值:float
100 
101   // 每一层为每一个top blob都分配了一个默认值,通常是0或1
102 
103   repeated float loss_weight = 5;
104 
105   // 指定的学习参数
106 
107   repeated ParamSpec param = 6;
108 
109   // The blobs containing the numeric parameters of the layer.
110 
111   repeated BlobProto blobs = 7;
112 
113   // included/excluded.
114 
115   repeated NetStateRule include = 8;
116 
117   repeated NetStateRule exclude = 9;
118 
119   // Parameters for data pre-processing.
120 
121   optional TransformationParameter transform_param = 100;
122 
123   // Parameters shared by loss layers.
124 
125   optional LossParameter loss_param = 101;
126 
127   // 各种类型层参数
128 
129   optional AccuracyParameter accuracy_param = 102;
130 
131   optional ArgMaxParameter argmax_param = 103;
132 
133   optional ConcatParameter concat_param = 104;
134 
135   optional ContrastiveLossParameter contrastive_loss_param = 105;
136 
137   optional ConvolutionParameter convolution_param = 106;
138 
139   optional DataParameter data_param = 107;
140 
141   optional DropoutParameter dropout_param = 108;
142 
143   optional DummyDataParameter dummy_data_param = 109;
144 
145   optional EltwiseParameter eltwise_param = 110;
146 
147   optional ExpParameter exp_param = 111;
148 
149   optional HDF5DataParameter hdf5_data_param = 112;
150 
151   optional HDF5OutputParameter hdf5_output_param = 113;
152 
153   optional HingeLossParameter hinge_loss_param = 114;
154 
155   optional ImageDataParameter image_data_param = 115;
156 
157   optional InfogainLossParameter infogain_loss_param = 116;
158 
159   optional InnerProductParameter inner_product_param = 117;
160 
161   optional LRNParameter lrn_param = 118;
162 
163   optional MemoryDataParameter memory_data_param = 119;
164 
165   optional MVNParameter mvn_param = 120;
166 
167   optional PoolingParameter pooling_param = 121;
168 
169   optional PowerParameter power_param = 122;
170 
171   optional PythonParameter python_param = 130;
172 
173   optional ReLUParameter relu_param = 123;
174 
175   optional SigmoidParameter sigmoid_param = 124;
176 
177   optional SoftmaxParameter softmax_param = 125;
178 
179   optional SliceParameter slice_param = 126;
180 
181   optional TanHParameter tanh_param = 127;
182 
183   optional ThresholdParameter threshold_param = 128;
184 
185   optional WindowDataParameter window_data_param = 129;
186 
187 }
View Code

那么接下来的一段代码来演示如何解析Caffemodel,我解析用的model为MNIST手写库训练后的model,Lenet_iter_10000.caffemodel。

清单7. Caffemodel解析代码
 1  #include 
 2  #include <string.h>
 3  #include 
 4  #include 
 5  #include "proto/caffe.pb.h"
 6 
 7  using namespace std;
 8  using namespace caffe;
 9 
10  int main(int argc, char* argv[]) 
11  { 
12 
13   caffe::NetParameter msg; 
14 
15   fstream input("lenet_iter_10000.caffemodel", ios::in | ios::binary); 
16   if (!msg.ParseFromIstream(&input)) 
17   { 
18     cerr << "Failed to parse address book." << endl; 
19     return -1; 
20   } 
21   printf("length = %d\n", length);
22   printf("Repeated Size = %d\n", msg.layer_size());
23 
24   ::google::protobuf::RepeatedPtrField< LayerParameter >* layer = msg.mutable_layer();
25   ::google::protobuf::RepeatedPtrField< LayerParameter >::iterator it = layer->begin();
26   for (; it != layer->end(); ++it)
27   {
28     cout << it->name() << endl;
29     cout << it->type() << endl;
30     cout << it->convolution_param().weight_filler().max() << endl;
31   } 
32 
33   return 0;
34  }

View Code

这篇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字段而生成。这里生成的函数和上面acctid
36         //生成的那组函数基本相似。因此这里只是列出差异部分。
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* buddiesInfo = retrieveResp2.mutable_buddiesinfo();
31         RepeatedPtrField::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仍然提供了很多其它非常有用的功能,特别是针对序列化的目的地,比如文件流和网络流等。与此同时,也提供了完整的官方文档和规范的命名规则,在很多情况下,可以直接通过函数的名字便可获悉函数所完成的工作。


你可能感兴趣的:(caffe,caffe,protobuffer)