【详细解析版】Protobuf3使用手册

Protobuf使用手册--中文版,好不容易找到了,分享一下给大家。

目录

第1章 定义.proto 文件

1.1 定义package

1.2 定义message

1.3 定义属性

1.3.1 标注

1.3.2 类型

1.3.3 属性顺序号

1.4 可选项

1.4.1 import可选项

1.4.2 packed

1.4.3 default

1.5 大数据量使用建议

1.5.1 repeated message类型

1.5.2 repeated raw类型

1.6 Protocol Buffer消息升级原则

 

第2章 编译 .proto 文件

第3章 使用message

3.1 类成员变量的访问

3.2 标准message方法

3.3 编码和解码函数

3.4 简单message生成的C++代码

3.5 嵌套message生成的C++代码

3.6 repeated嵌套message生成的C++代码


第1章 定义.proto 文件

首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。proto 文件非常类似 java 或者 C 语言的数据定义,可以使用C或C++风格的注释。下面是一个proto文件的例子。

syntax = "proto3";
package tutorial;
 
 
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
 
 
message Person {
required string name = 1;
required int32 id = 2;        // Unique ID number for this person.
optional string email = 3;
 
 
 
enum PhoneType {
 
  MOBILE = 0;
  HOME = 1;
  WORK = 2;
 
}
 
 
message PhoneNumber {
 
  required string number = 1;
  optional PhoneType type = 2 [default = HOME];
 
}
 
repeated PhoneNumber phone = 4;
 
}
 
 
// Our address book file is just one of these.
 
message AddressBook {
repeated Person person = 1;
 
}

一个proto文件主要包含package定义、message定义和属性定义三个部分,还有一些可选项。

文件的第一行指定了你正在使用proto3语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。

 

1.1 定义package

Package在c++中对应namespace

对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package。

 

1.2 定义message

Message在C++中对应class。Message中定义的全部属性在class中全部为private的。

Message的嵌套使用可以嵌套定义,也可以采用先定义再使用的方式。

Message的定义末尾可以采用java方式在不加“;”,也可以采用C++定义方式在末尾加上“;”,这两种方式都兼容,建议采用java定义方式。

向.proto文件添加注释,可以使用C/C++/java风格的双斜杠(//) 语法格式。

 

1.3 定义属性

属性定义分为四部分:标注+类型+属性名+属性顺序号+[默认值],其示意如下所示。

标注

类型

属性名

属性顺序号

[默认值]

required

string

name

= 1

[default=””];

       其中属性名与C++和java语言类似,不再解释;下面分别对标注、类型和属性顺序号加以详细介绍。

其中包名和消息名以及其中变量名均采用java的命名规则——驼峰式命名法,驼峰式命名法规则见附件1。

 

1.3.1 标注

标注包括“required”、“optional”、“repeated”三种,其中

required表示该属性为必选属性,否则对应的message“未初始化”,debug模式下导致断言,release模式下解析失败;

optional表示该属性为可选属性,不指定,使用默认值(int或者char数据类型默认为0,string默认为空,bool默认为false,嵌套message默认为构造,枚举则为第一个)

repeated表示该属性为重复字段,可看作是动态数组,类似于C++中的vector。

如果为optional属性,发送端没有包含该属性,则接收端在解析式采用默认值。对于默认值,如果已设置默认值,则采用默认值,如果未设置,则类型特定的默认值为使用,例如string的默认值为””。

 

1.3.2 类型

Protobuf的属性基本包含了c++需要的所有基本属性类型。

protobuf属性

C++属性

java属性

备注

double

double

double

固定8个字节

float

float

float

固定4个字节

int32

int32

int32

使用变长编码,对于负数编码效率较低,如果经常使用负数,建议使用sint32

int64

int64

int64

使用变长编码,对于负数编码效率较低,如果经常使用负数,建议使用sint64

uint32

uint32

int

使用变长编码

uint64

uint64

long

使用变长编码

sint32

int32

int

采用zigzag压缩,对负数编码效率比int32高

sint64

int64

long

采用zigzag压缩,对负数编码效率比int64高

fixed32

uint32

int

总是4字节,如果数据>2^28,编码效率高于unit32

fixed64

uint64

long

总是8字节,如果数据>2^56,编码效率高于unit32

sfixed32

int32

int

总是4字节

sfixed64

int64

long

总是8字节

bool

bool

boolean

 

string

string

String

一个字符串必须是utf-8编码或者7-bit的ascii编码的文本

bytes

string

ByteString

可能包含任意顺序的字节数据

 

1.3.2.1 Union类型定义

Protobuf没有提供union类型,如果希望使用union类型,可以采用enum和optional属性定义的方式。

例如,如果已经定义了Foo、Bar、Baz等message,则可以采用如下定义。

message OneMessage {
 
  enum Type { FOO = 1; BAR = 2; BAZ = 3; }
 
  // 标识要填写的字段,必填
  required Type type = 1;
 
  // 将填写以下内容之一,可选
  optional Foo foo = 2;
  optional Bar bar = 3;
  optional Baz baz = 4;
 
}

1.3.3 属性顺序号

属性顺序号是protobuf为了提高数据的压缩和可选性等功能定义的,需要按照顺序进行定义,且不允许有重复。

 

1.4 可选项

1.4.1 import可选项

Import可选项用于包含其它proto文件中定义的message或enum类型等。标准格式如下

import “phonetype.proto”;

使用时,import的文件必须与当前文件处于同一个文件夹下,protoc无法完成不处于同一个文件夹下的import选项。

 

1.4.2 packed

packed (field option): 如果该选项在一个整型基本类型上被设置为真,则采用更紧凑的编码方式。当然使用该值并不会对数值造成任何损失。在2.3.0版本之前,解析器将会忽略那些 非期望的包装值。因此,它不可能在不破坏现有框架的兼容性上而改变压缩格式。在2.3.0之后,这种改变将是安全的,解析器能够接受上述两种格式,但是在 处理protobuf老版本程序时,还是要多留意一下。

repeated int32 samples = 4 [packed=true];

 

 

1.4.3 default

[default = default_value]: optional类型的字段,如果在序列化时没有被设置,或者是老版本的消息中根本不存在该字段,那么在反序列化该类型的消息是,optional的字段将被赋予类型相关的缺省值,如bool被设置为false,int32被设置为0。Protocol Buffer也支持自定义的缺省值,如:

  optional int32 result_per_page = 3 [default = 10]


1.5 大数据量使用建议

在使用过程中发现,对于大数据量的协议报文(循环超过10000条),如果repeated修饰的属性为对象类型(诸如message 、Bytes、string等称为“对象类型”,其余的诸如int32、int64、float等等称为“原始类型”)时,效率非常低,而且占用的进程内存也非常大,建议采用如下方式优化。

1.5.1 repeated message类型

在message 中对repeated 标识的 message类型的字段需要做大量ADD操作时,可以考虑尽量避免嵌套message或者减少嵌套的message个数。

 

1.5.2 repeated raw类型

在message中对repeated 标识的原始数据类型的字段需要做大量ADD操作(例如超过3千)时,可以考虑预分配数据空间,避免重复大量地分配空间。

 

1.5.3 repeated Bytes类型

在protobuf中,Bytes基于C++ STL中的string实现,因为string内存管理的原因,程序空间往往较大。所以应用如果有很多repeated Bytes类型的字段的话,进程显示耗用大量内存,这与vector<string>的情况基本一致。

1.6 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限定符也是相互兼容的。

 

第2章 编译 .proto 文件

可以通过定义好的.proto文件来生成Java、Python、C++代码,需要基于.proto文件运行protocol buffer编译器protoc。如果你没有安装编译器,下载安装包并遵照README安装。运行的命令如下所示:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto

MPORT_PATH声明了一个.proto文件所在的具体目录。如果忽略该值,则使用当前目录。如果有多个目录则可以 对--proto_path 写多次,它们将会顺序的被访问并执行导入。-I=IMPORT_PATH是它的简化形式。

当然也可以提供一个或多个输出路径:

  • --cpp_out 在目标目录DST_DIR中产生C++代码,可以在C++代码生成参考中查看更多。
  • --java_out 在目标目录DST_DIR中产生Java代码,可以在 Java代码生成参考中查看更多。
  • --python_out 在目标目录 DST_DIR 中产生Python代码,可以在Python代码生成参考中查看更多。
  • --go_out 在目标目录 DST_DIR 中产生Go代码,可以在GO代码生成参考中查看更多。
  • --ruby_out在目标目录 DST_DIR 中产生Go代码,参考正在制作中。
  • --javanano_out在目标目录DST_DIR中生成JavaNano,JavaNano代码生成器有一系列的选项用于定制自定义生成器的输出:你可以通过生成器的README查找更多信息,JavaNano参考正在制作中。
  • --objc_out在目标目录DST_DIR中产生Object代码,可以在Objective-C代码生成参考中查看更多。
  • --csharp_out在目标目录DST_DIR中产生Object代码,可以在C#代码生成参考中查看更多。
  • --php_out在目标目录DST_DIR中产生Object代码,可以在PHP代码生成参考中查看更多。

作为一个方便的拓展,如果DST_DIR以.zip或者.jar结尾,编译器会将输出写到一个ZIP格式文件或者符合JAR标准的.jar文件中。注意如果输出已经存在则会被覆盖,编译器还没有智能到可以追加文件。
- 你必须提议一个或多个.proto文件作为输入,多个.proto文件可以只指定一次。虽然文件路径是相对于当前目录的,每个文件必须位于其IMPORT_PATH下,以便每个文件可以确定其规范的名称。

 

第3章 使用message

3.1 类成员变量的访问

在生成的.h文件中定义了类成员的访问方法。例如,对于Person类,定义了name、id、email、phone等成员的访问方法。

获取成员变量值直接采用使用成员变量名(全部为小写),设置成员变量值,使用在成员变量名前加set_的方法。

对于普通成员变量(required和optional)提供has_方法判断变量值是否被设置;提供clear_方法清除设置的变量值。

对于string类型,提供多种set_方法,其参数不同。同时,提供了一个mutable_方法,返回变量值的可修改指针。

对于repeated变量,提供了其它一些特殊的方法:

l   _size方法:返回repeated field’s

l   通过下脚标访问其中的数组成员组

l   通过下脚标返回其中的成员的mutable_的方法

l   _add方法:增加一个成员。

3.2 标准message方法

生成的.h文件中的class都继承自::google::protobuf::Message类,Message类提供了一些方法可以检查或者操作整个message,包括

l   bool IsInitialized() const;检查是否所有required变量都已经初始化;      

l   string DebugString() const;返回message的可阅读的表示,主要用于调试程序;

l   void CopyFrom(const Person& from);使用一个message的值覆盖本message;

l   void Clear();清空message的所有成员变量值。

 

3.3 编码和解码函数

每个message类都提供了写入和读取message数据的方法,包括

l   bool SerializeToString(string* output) const;把message编码进output。 

l   bool ParseFromString(const string& data);从string解码到message

l   bool SerializeToArray(char* buf,int size) const;把message编码进数组buf.

l   bool ParseFromArray(const char* buf,int size);把buf解码到message。此解码方法效率较ParseFromString高很多,所以一般用这种方法解码。

l   bool SerializeToOstream(ostream* output) const;把message编码进ostream

l   bool ParseFromIstream(istream* input);从istream解码到message

备注:发送接收端所使用的加码解码方法不一定非得配对,即发送端用SerializeToString 接收端不一定非得用ParseFromString ,可以使用其他解码方法。

 

3.4 简单message生成的C++代码

这里先定义一个最简单的message,其中只是包含原始类型的字段。

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类中提供了具体的实现。使用LITE版本的Protocol Buffer。这样不仅可以得到更高编码效率,而且生成代码编译后所占用的资源也会更少,至于反射所能带来的灵活性和极易扩展性。下面我们来看一下由message LogonReqMessage生成的C++类的部分声明,以及常用方法的说明性注释。

class LogonReqMessage : public ::google::protobuf::MessageLite {
 
    public:
        LogonReqMessage();
        virtual ~LogonReqMessage();
 
 
        // implements Message ----------------------------------------------
        //下面的成员函数均实现自MessageLite中的虚函数。
        //创建一个新的LogonReqMessage对象,等同于clone。
        LogonReqMessage* New() const;
        //用另外一个LogonReqMessage对象初始化当前对象,等同于赋值操作符重载(operator=)
        void CopyFrom(const LogonReqMessage& from);
        //清空当前对象中的所有数据,既将所有成员变量置为未初始化状态。
        void Clear();
        //判断当前状态是否已经初始化。
        bool IsInitialized() const;
        //在给当前对象的所有变量赋值之后,获取该对象序列化后所需要的字节数。
        int ByteSize() const;
        //获取当前对象的类型名称。
        inline ::std::string GetTypeName() const;
 
 
 
        // required int64 acctID = 1;
        //下面的成员函数都是因message中定义的acctID字段而生成。
        //这个静态成员表示AcctID的标签值。命名规则是k + FieldName(驼峰规则) + FieldNumber。
        static const int kAcctIDFieldNumber = 1;
        //如果acctID字段已经被设置返回true,否则false。
        inline bool has_acctid() const;
        //执行该函数后has_acctid函数将返回false,而下面的acctid函数则返回acctID的缺省值。
        inline void clear_acctid();
        //返回acctid字段的当前值,如果没有设置则返回int64类型的缺省值。
        inline ::google::protobuf::int64 acctid() const;
        //为acctid字段设置新值,调用该函数后has_acctid函数将返回true。
        inline void set_acctid(::google::protobuf::int64 value);
 
   
 
        // required string passwd = 2;
        //下面的成员函数都是因message中定义的passwd字段而生成。这里生成的函数和上面acctid
        //生成的那组函数基本相似。因此这里只是列出差异部分。
        static const int kPasswdFieldNumber = 2;
        inline bool has_passwd() const;
        inline void clear_passwd();
        inline const ::std::string& passwd() const;
        inline void set_passwd(const ::std::string& value);
        //对于字符串类型字段设置const char*类型的变量值。
        inline void set_passwd(const char* value);
        inline void set_passwd(const char* value, size_t size);
        //可以通过返回值直接给passwd对象赋值。在调用该函数之后has_passwd将返回true。
        inline ::std::string* mutable_passwd();
        //释放当前对象对passwd字段的所有权,同时返回passwd字段对象指针。调用此函数之后,passwd字段对象
 
        //的所有权将移交给调用者。此后再调用has_passwd函数时将返回false。
        inline ::std::string* release_passwd();
 
    private:
 
        //... ...
 
    };

下面是读写LogonReqMessage对象的C++测试代码和说明性注释。

void testSimpleMessage()
 
    {
 
        printf("==================This is simple message.================\n");
        //序列化LogonReqMessage对象到指定的内存区域。
        LogonReqMessage logonReq;
        logonReq.set_acctid(20);
        logonReq.set_passwd("Hello World");
        //提前获取对象序列化所占用的空间并进行一次性分配,从而避免多次分配
        //而造成的性能开销。通过该种方式,还可以将序列化后的数据进行加密。
        //之后再进行持久化,或是发送到远端。
        int length = logonReq.ByteSize();
        char* buf = new char[length];
        logonReq.SerializeToArray(buf,length);
        //从内存中读取并反序列化LogonReqMessage对象,同时将结果打印出来。
        LogonReqMessage logonReq2;
        logonReq2.ParseFromArray(buf,length);
        printf("acctID = %I64d, password = %s\n",logonReq2.acctid(),logonReq2.passwd().c_str());
 
        delete [] buf;
 
    }

3.5 嵌套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++代码和关键性注释。


class LogonRespMessage : public ::google::protobuf::MessageLite {

    public:

        LogonRespMessage();

        virtual ~LogonRespMessage();
 
        // implements Message 

        //这部分函数和之前的例子一样。  

        // required .LoginResult logonResult = 1;

        //下面的成员函数都是因message中定义的logonResult字段而生成。

        //这一点和前面的例子基本相同,只是类型换做了枚举类型LoginResult。   

        static const int kLogonResultFieldNumber = 1;

        inline bool has_logonresult() const;

        inline void clear_logonresult();

        inline LoginResult logonresult() const;

        inline void set_logonresult(LoginResult value);  

        // required .UserInfo userInfo = 2;

        //下面的成员函数都是因message中定义的UserInfo字段而生成。

        //这里只是列出和非消息类型字段差异的部分。

        static const int kUserInfoFieldNumber = 2;

        inline bool has_userinfo() const;

        inline void clear_userinfo();

        inline const ::UserInfo& userinfo() const;

        //可以看到该类并没有生成用于设置和修改userInfo字段set_userinfo函数,而是将该工作

        //交给了下面的mutable_userinfo函数。因此每当调用函数之后,Protocol Buffer都会认为

        //该字段的值已经被设置了,同时has_userinfo函数亦将返回true。在实际编码中,我们可以

        //通过该函数返回userInfo字段的内部指针,并基于该指针完成userInfo成员变量的初始化工作。

        inline ::UserInfo* mutable_userinfo();

        inline ::UserInfo* release_userinfo();

    private:

        //... ...

    };

下面是读写LogonRespMessage对象的C++测试代码和说明性注释。

void testNestedMessage()

    {

        printf("==================This is nested message.================\n");

        LogonRespMessage logonResp;

        logonResp.set_logonresult(LOGON_RESULT_SUCCESS);

        //如上所述,通过mutable_userinfo函数返回userInfo字段的指针,之后再初始化该对象指针。

        UserInfo* userInfo = logonResp.mutable_userinfo();

        userInfo->set_acctid(200);

        userInfo->set_name("Tester");

        userInfo->set_status(OFFLINE);

        int length = logonResp.ByteSize();

        char* buf = new char[length];

        logonResp.SerializeToArray(buf,length);

        LogonRespMessage logonResp2;

        logonResp2.ParseFromArray(buf,length);

        printf("LogonResult = %d, UserInfo->acctID = %I64d, UserInfo->name = %s, UserInfo->status = %d\n"

            ,logonResp2.logonresult(),logonResp2.userinfo().acctid(),logonResp2.userinfo().name().c_str(),logonResp2.userinfo().status());

        delete [] buf;

    }

3.6 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字段生成的代码进行更为详细的解释。


class RetrieveBuddiesResp : public ::google::protobuf::MessageLite {

    public:

        RetrieveBuddiesResp();

        virtual ~RetrieveBuddiesResp();

        //其余代码的功能性注释均可参照前面的例子。
    
        // repeated .BuddyInfo buddiesInfo = 2;

        static const int kBuddiesInfoFieldNumber = 2;

        //返回数组中成员的数量。

        inline int buddiesinfo_size() const;

        //清空数组中的所有已初始化成员,调用该函数后,buddiesinfo_size函数将返回0。

        inline void clear_buddiesinfo();

        //返回数组中指定下标所包含元素的引用。

        inline const ::BuddyInfo& buddiesinfo(int index) const;

        //返回数组中指定下标所包含元素的指针,通过该方式可直接修改元素的值信息。

        inline ::BuddyInfo* mutable_buddiesinfo(int index);

        //像数组中添加一个新元素。返回值即为新增的元素,可直接对其进行初始化。

        inline ::BuddyInfo* add_buddiesinfo();

        //获取buddiesInfo字段所表示的容器,该函数返回的容器仅用于遍历并读取,不能直接修改。

        inline const ::google::protobuf::RepeatedPtrField< ::BuddyInfo >&

          buddiesinfo() const;

        //获取buddiesInfo字段所表示的容器指针,该函数返回的容器指针可用于遍历和直接修改。

        inline ::google::protobuf::RepeatedPtrField< ::BuddyInfo >*

          mutable_buddiesinfo();

    private:

        //... ...

    };

下面是读写RetrieveBuddiesResp对象的C++测试代码和说明性注释。

void testRepeatedMessage()

    {

        printf("==================This is repeated message.================\n");

        RetrieveBuddiesResp retrieveResp;

        retrieveResp.set_buddiescnt(2);

        BuddyInfo* buddyInfo = retrieveResp.add_buddiesinfo();

        buddyInfo->set_groupid(20);

        UserInfo* userInfo = buddyInfo->mutable_userinfo();

        userInfo->set_acctid(200);

        userInfo->set_name("user1");

        userInfo->set_status(OFFLINE);

   

        buddyInfo = retrieveResp.add_buddiesinfo();

        buddyInfo->set_groupid(21);

        userInfo = buddyInfo->mutable_userinfo();

        userInfo->set_acctid(201);

        userInfo->set_name("user2");

        userInfo->set_status(ONLINE);

   

        int length = retrieveResp.ByteSize();

        char* buf = new char[length];

        retrieveResp.SerializeToArray(buf,length);

   

        RetrieveBuddiesResp retrieveResp2;

        retrieveResp2.ParseFromArray(buf,length);

        printf("BuddiesCount = %d\n",retrieveResp2.buddiescnt());

        printf("Repeated Size = %d\n",retrieveResp2.buddiesinfo_size());

        //这里仅提供了通过容器迭代器的方式遍历数组元素的测试代码。

        //事实上,通过buddiesinfo_size和buddiesinfo函数亦可循环遍历。

        RepeatedPtrField<BuddyInfo>* buddiesInfo = retrieveResp2.mutable_buddiesinfo();

        RepeatedPtrField<BuddyInfo>::iterator it = buddiesInfo->begin();

        for (; it != buddiesInfo->end(); ++it) {

            printf("BuddyInfo->groupID = %d\n", it->groupid());

            printf("UserInfo->acctID = %I64d, UserInfo->name = %s, UserInfo->status = %d\n"

                , it->userinfo().acctid(), it->userinfo().name().c_str(),it->userinfo().status());

        }

        delete [] buf;

    }

 

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值