protobuf学习笔记-官方示例

这篇博客详细介绍了Protocol Buffers(protobuf)的学习过程,包括编译器安装、.proto文件定义、编译Protocol Buffer、API使用、消息读写、扩展性和高级用法。示例是一个地址本应用,讲解了message的定义、字段类型和修饰符,以及如何生成和使用Java类进行序列化和反序列化操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

示例简介

本例是一个可以读写联系信息的“地址本”应用,每个人在地址本里有个id、名字、邮箱地址和联系电话。


编译器安装

  • window
    1. 下载protocol compiler
    2. 将下载到的可执行文件放置到相应目录,并配置到环境变量path中
  • linux

定义protocol格式

为了创建地址本应用,我们需要开始定义一个.proto文件。.proto文件定义很简单:为需要序列化的数据都添加一个message,然后指定message中字段指定类型和名称。具体定义如下所示:

package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

message Person {
  required string name = 1;
  required int32 id = 2;
  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;
}

message AddressBook {
  repeated Person person = 1;
}

如你所见,上面的语法与C++和Java非常相似。下面我们一步步分析该文件:

  • package:避免不同的项目命名冲突
  • java_package和java_outer_classname

    • java_package:指定生成的类会在哪个包下,如果不显示指定,使用package的声明
    • java_outer_classname:指定.proto文件包含的所有类的外部类名称,如果不显示指定,使用.ptoto文件名称的驼峰形式作为类名
  • message:一个message是一系列字段的集合。

  • field
    • type:bool、int32、float、string、enum、message
    • modifier:
      • required:require修饰的字段值必须设置,否则在反序列话时会遇到RuntimeException,序列化这样的数据也会遇到IOException。
      • optional:optional修饰的字段值可有可无,对于简单数据类型,可以设置默认值。否则,系统自己设置默认值:数字类型为0,字符类型为空字符串,布尔值为false,
      • repeated:repeated修饰的字段值可能有0个或多个,并且protobuf保留了这些值的顺序
    • “=1, =2”:每个字段都有类似这样的指定,这个是用于标记每个元素在二进制编码中的标识。1-15比更多的数字可以少用一个byte,所以在优化的时候,可以给那些公共使用或者重复的字段指定1-15。

编译Protocol Buffer

在命令行使用protoc命令可以生成类来读写地址本,在命令行里执行protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto,生成的com/example/tutorial/AddressBookProtos.java在指定的路径下。protoc的命令行用法如下所示:

Usage: protoc [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
-IPATH, --proto_path=PATH   Specify the directory in which to search for
                          imports.  May be specified multiple times;
                          directories will be searched in order.  If not
                          given, the current working directory is used.
--version                   Show version info and exit.
-h, --help                  Show this text and exit.
--encode=MESSAGE_TYPE       Read a text-format message of the given type
                          from standard input and write it in binary
                          to standard output.  The message type must
                          be defined in PROTO_FILES or their imports.
--decode=MESSAGE_TYPE       Read a binary message of the given type from
                          standard input and write it in text format
                          to standard output.  The message type must
                          be defined in PROTO_FILES or their imports.
--decode_raw                Read an arbitrary protocol message from
                          standard input and write the raw tag/value
                          pairs in text format to standard output.  No
                          PROTO_FILES should be given when using this
                          flag.
-oFILE,                     Writes a FileDescriptorSet (a protocol buffer,
--descriptor_set_out=FILE defined in descriptor.proto) containing all of
                          the input files to FILE.
--include_imports           When using --descriptor_set_out, also include
                          all dependencies of the input files in the
                          set, so that the set is self-contained.
--include_source_info       When using --descriptor_set_out, do not strip
                          SourceCodeInfo from the FileDescriptorProto.
                          This results in vastly larger descriptors that
                          include information about the original
                          location of each decl in the source file as
                          well as surrounding comments.
--error_format=FORMAT       Set the format in which to print errors.
                          FORMAT may be 'gcc' (the default) or 'msvs'
                          (Microsoft Visual Studio format).
--print_free_field_numbers  Print the free field numbers of the messages
                          defined in the given proto files. Groups share
                          the same field number space with the parent
                          message. Extension ranges are counted as
                          occupied fields numbers.
--plugin=EXECUTABLE         Specifies a plugin executable to use.
                          Normally, protoc searches the PATH for
                          plugins, but you may specify additional
                          executables not in the path using this flag.
                          Additionally, EXECUTABLE may be of the form
                          NAME=PATH, in which case the given plugin name
                          is mapped to the given executable even if
                          the executable's own name differs.
--cpp_out=OUT_DIR           Generate C++ header and source.
--java_out=OUT_DIR          Generate Java source file.
--python_out=OUT_DIR        Generate Python source file.

Protocol Buffer API简介

查看生成的代码发现:
1. AddressBookProtos.java定义了一个叫AddressBookProtos的类,AddressBookProtos类嵌套了addressbook.proto中声明的每一个message对应的类
2. 每一个message类有一个自己的Builder类,用来创建类实例
3. message类和builder类都有自动生成的字段访问方法,但是message只有getter方法,builder有getter和setter
Person类的getter代码:

    // required string name = 1;
    public boolean hasName();
    public String getName();

    // required int32 id = 2;
    public boolean hasId();
    public int getId();

    // optional string email = 3;
    public boolean hasEmail();
    public String getEmail();

    // repeated .tutorial.Person.PhoneNumber phone = 4;
    public List<PhoneNumber> getPhoneList();
    public int getPhoneCount();
    public PhoneNumber getPhone(int index);

Person.Builder的getter和setter代码

    // required string name = 1;
    public boolean hasName();
    public java.lang.String getName();
    public Builder setName(String value);
    public Builder clearName();

    // required int32 id = 2;
    public boolean hasId();
    public int getId();
    public Builder setId(int value);
    public Builder clearId();

    // optional string email = 3;
    public boolean hasEmail();
    public String getEmail();
    public Builder setEmail(String value);
    public Builder clearEmail();

    // repeated .tutorial.Person.PhoneNumber phone = 4;
    public List<PhoneNumber> getPhoneList();
    public int getPhoneCount();
    public PhoneNumber getPhone(int index);
    public Builder setPhone(int index, PhoneNumber value);
    public Builder addPhone(PhoneNumber value);
    public Builder addAllPhone(Iterable<PhoneNumber> value);
    public Builder clearPhone();

4. 每个字段都有个Java-Beans风格的getter和setter方法,单一值的字段还有个has方法,表示该字段是否有赋值。此外每个字段都有个clear方法,用于置空该字段。
5. 多值字段还有一些额外方法:获取值个数的count方法,通过下标获取和修改指定数组元素的getter和setter方法,增加新的元素的add方法,增加一个容器内全部元素的addAll方法
6. 访问方法都是使用驼峰式的命名方法,即使.proto文件使用带下划线的小写。为了针对所有语言有一个好的命名规范,在.proto文件通常使用带下划线的小写字母的命名方式。
7. message类实例:message类实例与String相似,都是不可变的。创建一个message类实例:

1. 实例化message类的builder类实例,使用newBuilder()方法
2. 通过builder类实例设置字段值
3. 调用builder类实例的build()方法

8. message类和build类的其他通用方法

1. isInitialized():检查必填字段是否设置了值
2. toString():返回可阅读的message内容,通常用于调试
3. mergeFrom(Message other):合并other的内容到当前实例
4. clear():重置所有字段为空值

9. 解析于序列化

- byte[] toByteArray():序列化一个message,返回一个byte数组
-static Person parseFrom(byte[] data):解析给定的byte数组为message
- void writeTo(OutputStream output):序列化一个message,且写入指定的输出流
- static Person parseFrom(InputStream input):读取并解析指定输入流的message

10. Protocol buffer类扩展:不可以通过继承的方式来给生成的类添加方法和属性,可以通过包装类来实现


写一个消息

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;

class AddPerson {
  // This function fills in a Person message based on user input.
  static Person PromptForAddress(BufferedReader stdin,
                                 PrintStream stdout) throws IOException {
    Person.Builder person = Person.newBuilder();

    stdout.print("Enter person ID: ");
    person.setId(Integer.valueOf(stdin.readLine()));

    stdout.print("Enter name: ");
    person.setName(stdin.readLine());

    stdout.print("Enter email address (blank for none): ");
    String email = stdin.readLine();
    if (email.length() > 0) {
      person.setEmail(email);
    }

    while (true) {
      stdout.print("Enter a phone number (or leave blank to finish): ");
      String number = stdin.readLine();
      if (number.length() == 0) {
        break;
      }

      Person.PhoneNumber.Builder phoneNumber =
        Person.PhoneNumber.newBuilder().setNumber(number);

      stdout.print("Is this a mobile, home, or work phone? ");
      String type = stdin.readLine();
      if (type.equals("mobile")) {
        phoneNumber.setType(Person.PhoneType.MOBILE);
      } else if (type.equals("home")) {
        phoneNumber.setType(Person.PhoneType.HOME);
      } else if (type.equals("work")) {
        phoneNumber.setType(Person.PhoneType.WORK);
      } else {
        stdout.println("Unknown phone type.  Using default.");
      }

      person.addPhone(phoneNumber);
    }

    return person.build();
  }

  // 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.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");
      System.exit(-1);
    }

    AddressBook.Builder addressBook = AddressBook.newBuilder();

    // Read the existing address book.
    try {
      addressBook.mergeFrom(new FileInputStream(args[0]));
    } catch (FileNotFoundException e) {
      System.out.println(args[0] + ": File not found.  Creating a new file.");
    }

    // Add an address.
    addressBook.addPerson(
      PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
                       System.out));

    // Write the new address book back to disk.
    FileOutputStream output = new FileOutputStream(args[0]);
    addressBook.build().writeTo(output);
    output.close();
  }
}

读一个消息

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;

class ListPeople {
  // Iterates though all people in the AddressBook and prints info about them.
  static void Print(AddressBook addressBook) {
    for (Person person: addressBook.getPersonList()) {
      System.out.println("Person ID: " + person.getId());
      System.out.println("  Name: " + person.getName());
      if (person.hasEmail()) {
        System.out.println("  E-mail address: " + person.getEmail());
      }

      for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {
        switch (phoneNumber.getType()) {
          case MOBILE:
            System.out.print("  Mobile phone #: ");
            break;
          case HOME:
            System.out.print("  Home phone #: ");
            break;
          case WORK:
            System.out.print("  Work phone #: ");
            break;
        }
        System.out.println(phoneNumber.getNumber());
      }
    }
  }

  // Main function:  Reads the entire address book from a file and prints all
  //   the information inside.
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");
      System.exit(-1);
    }

    // Read the existing address book.
    AddressBook addressBook =
      AddressBook.parseFrom(new FileInputStream(args[0]));

    Print(addressBook);
  }
}

扩展

在发布使用protocol buffer的代码后,肯定还会对protocol buffer格式定义改进。但为了保持新版本的向后兼容性, 旧版本的向前兼容性,新版本必须遵循以下规则:
1. 不能修改存在字段的标识数字
2. 不能添加和删除必须字段
3. 可以删除可选和重复字段
4. 可以增加可选和删除字段,但必须使用新的标识数字


高级使用

  • 参考api
  • 反射:与XML/JSON的相互转换
### C++ gRPC 学习教程和示例代码 #### 一、环境搭建 为了能够顺利运行C++中的gRPC程序,需要先完成开发环境的配置。这通常涉及到安装必要的依赖库以及设置编译工具链。具体来说,可以按照官方文档指导来准备所需的软件包,比如Protocol Buffers编译器`protoc`及其对应的C++插件,还有gRPC核心库等[^1]。 ```bash sudo apt-get install build-essential autoconf libtool pkg-config git clone https://github.com/protocolbuffers/protobuf.git cd protobuf ./autogen.sh && ./configure && make -j$(nproc) && sudo make install ``` 对于gRPC本身,则可以通过如下命令获取并构建: ```bash git clone --recurse-submodules -b v1.48.x https://github.com/grpc/grpc cd grpc mkdir -p cmake/build && cd cmake/build cmake ../.. make -j$(nproc) sudo make install ``` #### 二、创建Protobuf文件 接下来就是定义服务接口,在`.proto`文件里描述消息结构和服务方法。这里给出一个简单的例子——HelloWorld服务,其中包含了一个名为SayHello的方法用于接收请求并向客户端返回响应信息。 ```protobuf syntax = "proto3"; option cc_enable_arenas = true; package helloworld; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; } ``` 保存上述内容到`helloworld.proto`之后,利用之前提到过的`protoc`命令行工具将其转换成相应的头文件与源码文件以便后续使用。 #### 三、编写服务器端逻辑 基于前面所生成的服务类模板,现在可以在项目中实现具体的业务处理函数了。下面展示的是如何继承自动生成出来的基类,并重写虚函数以提供实际功能的部分代码片段。 ```cpp #include <iostream> #include "helloworld.grpc.pb.h" using namespace std; using grpc::Server; using grpc::ServerBuilder; using grpc::ServerContext; using grpc::Status; using helloworld::Greeter; using helloworld::HelloReply; using helloworld::HelloRequest; class GreeterServiceImpl final : public Greeter::Service { public: Status SayHello(ServerContext* context, const HelloRequest* request, HelloReply* reply) override { string prefix("Hello "); reply->set_message(prefix + request->name()); return Status::OK; } }; ``` 这段代码实现了当接收到客户端发起的调用时会执行的操作:拼接字符串形成回复文本并通过参数传递给对方。 #### 四、启动监听进程 有了完整的协议声明加上对应的功能模块后就可以着手建立网络连接等待远端访问啦! ```cpp void RunServer() { string server_address("0.0.0.0:50051"); GreeterServiceImpl service; ServerBuilder builder; // Listen on the given address without any authentication mechanism. builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); // Register "service" as the instance through which we'll communicate with // clients. In this case it corresponds to an *synchronous* service. builder.RegisterService(&service); // Finally assemble the server. unique_ptr<Server> server(builder.BuildAndStart()); cout << "Server listening on " << server_address << endl; // Wait for the server to shutdown. Note that some other thread must be // responsible for shutting down the server for this call to ever return. server->Wait(); } ``` 此部分负责初始化HTTP/2传输层设施并将指定地址开放出去供外界联系;同时注册好先前定义好的处理器对象使得每次有新链接进来都能找到合适的地方去解析数据流进而触发相应动作。 #### 五、客户端编程指南 最后一步自然是要让应用程序具备主动出击的能力咯~即构造出能向远程主机发出请求的消息体格式化为wire format再经由socket发送过去得到回应为止的过程。 ```cpp void RunClient() { string target_str("localhost:50051"); // Instantiate the client. It requires a channel, out of which the actual RPCs // are created. This channel models a connection to an endpoint specified by // the argument; you may provide extra arguments to indicate credentials, // compression Level etc. shared_ptr<Channel> channel = CreateChannel(target_str, InsecureChannelCredentials()); // Stub acts like a proxy object representing remote side entity. unique_ptr<Greeter::Stub> stub(Greeter::NewStub(channel)); // Data we are sending to the server. HelloRequest request; request.set_name("you"); // Container for the data we expect from the server. HelloReply reply; // Context for the client. It could be used to convey extra information to // the server and/or tweak certain RPC behaviors. ClientContext context; // The actual RPC. Status status = stub->SayHello(&context, request, &reply); // Act upon its status. if (status.ok()) { cout << "Greeter received: " << reply.message() << endl; } else { cerr << status.error_code() << ": " << status.error_message() << endl; } } ``` 以上便是整个流程的大致介绍,当然这只是冰山一角而已,更多高级特性和最佳实践还需要读者朋友们自行探索学习哦~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值