Aprendizaje y uso de C++ Protobuf

Introducción

Los buffers de protocolo son una forma extensible, independiente del idioma y de la plataforma de serializar datos estructurados , que se pueden usar para protocolos de comunicación (de datos) , almacenamiento de datos y más. La información transmitida durante la comunicación se empaqueta a través de la estructura de datos del mensaje definida por Protobuf y luego se compila en un flujo de código binario para su transmisión o almacenamiento.

Los buffers de protocolo son un método de serialización de datos estructurados flexible, eficiente y automatizado, comparable a XML, pero más pequeño (de 3 a 10 veces), más rápido (de 20 a 100 veces) y más simple que XML.

Al utilizar Protobuf, debe escribir un archivo IDL (lenguaje de descripción de interfaz) y definir la estructura de datos en él . Solo se pueden serializar y deserializar las estructuras de datos predefinidas . Entre ellos, la serialización consiste en convertir objetos en datos binarios y la deserialización consiste en convertir datos binarios en objetos.

tutorial


Este tutorial proporciona una introducción básica al programador de C++ sobre el uso de búferes de protocolo. Al crear una aplicación de ejemplo simple, le muestra cómo

  • Defina el formato del mensaje en el archivo .proto.
  • Utilice el compilador de búfer de protocolo.
  • Escriba y lea mensajes utilizando la API de búferes de protocolo C++.

Esta no es una guía completa para usar Protocol Buffers en C++. Para obtener información de referencia más detallada, consulte la Guía del lenguaje de búfer de protocolo (proto2), la Guía del lenguaje de búfer de protocolo (proto3), la Referencia de API de C++, la Guía de código generado de C++ y la Referencia de codificación.


El ejemplo que usaremos es una aplicación de "libreta de direcciones" muy simple que lee y escribe los detalles de contacto de las personas en un archivo. Cada persona tiene un nombre, DNI, dirección de correo electrónico y número de teléfono de contacto en la libreta de direcciones.

¿ Cómo serializar y recuperar datos estructurados como este ? Hay varias formas de solucionar este problema:

  • Las estructuras de datos en la memoria sin formato se pueden enviar/guardar en formato binario . Este es un enfoque frágil, ya que el código de recepción/lectura debe compilarse exactamente con el mismo diseño de memoria, endianidad, etc. Además, como los archivos acumulan datos en su formato original y las copias del software viajan por cable en este formato, es difícil ampliar el formato.
  • Podría inventar una forma ad hoc de codificar el elemento de datos como una sola cadena, por ejemplo, 4 entradas como "12:3:-23:67". Este es un enfoque simple y flexible, aunque requiere escribir código de codificación y análisis de una sola vez, y el análisis impone un pequeño costo de tiempo de ejecución. Esto se utiliza mejor para codificar datos muy simples.
  • Serializar datos a XML. Este enfoque es muy atractivo porque XML es (más o menos) legible por humanos y tiene muchos idiomas. Si desea compartir datos con otras personas, esta es una buena aplicación/proyecto. Sin embargo, XML consume mucho espacio y codificarlo/decodificarlo puede imponer una enorme penalización en el rendimiento de la aplicación. Además, navegar por un árbol XMLDOM es más frecuente que navegar por campos simples dentro de una clase.

Puede utilizar Protobuf en su lugar para estas opciones. Protocol Buffers es la solución flexible, eficiente y automatizada para abordar con precisión este problema. Con Protobuf, puedes escribir .protouna descripción de la estructura de datos que deseas almacenar. El compilador de Protobuf crea así una clase que implementa la codificación automática de datos de Protobuf y analiza el formato binario eficiente. Las clases generadas son los campos que componen el búfer de protocolo y son responsables de leer el búfer de protocolo como una unidad. Es importante destacar que el formato Protocol Buffers respalda la idea de extender el formato a lo largo del tiempo de tal manera que el código aún pueda leer datos codificados en el formato anterior.

El código de ejemplo se incluye en el directorio "ejemplos" del paquete de código fuente  .

Definición del formato de su protocolo

Para crear una aplicación de libreta de direcciones , necesita .protoun archivo de inicio. .protoLa definición en el archivo es simple: agrega el mensaje para cada estructura de datos que desea serializar y luego le asigna el nombre y el tipo de cada campo en el mensaje. .protoA continuación se muestra el archivo  que define el mensaje addressbook.proto.

syntax = "proto2";

package tutorial;

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    optional string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}


Como puedes ver, su sintaxis es similar a C++ o Java. Repasemos cada parte para ver qué hace.


.protoLos archivos comienzan con una declaración de paquete , lo que ayuda a evitar conflictos de nombres entre diferentes proyectos . En C++, las clases generadas se colocarán en un espacio de nombres que coincida con el nombre del paquete.

A continuación, tiene las definiciones de sus mensajes. Los mensajes contienen un conjunto de campos escritos . Hay muchos tipos de datos simples estándar disponibles como tipos de campo, incluidos bool, int32, yfloatdoublestring . También puede agregar más tipos de campos de estructura a los mensajes utilizando otros tipos de mensajes; en el ejemplo anterior, Personmensaje contiene mensaje contiene mensaje . Incluso puede definir tipos de mensajes que están anidados dentro de otros mensajes; como puede ver,  las definiciones de tipos están dentro de . También puede definir el tipo si desea que uno de sus campos tenga uno de la lista de valores predefinidos; aquí desea especificar que el número de teléfono puede ser uno de los siguientes tipos de teléfono : o .PhoneNumber AddressBookPersonPhoneNumberPersonenumMOBILEHOMEWORK

Los indicadores "=1", "=2" en cada elemento identifican el campo que se utilizará en la codificación binaria . Los números de campo del 1 al 15 requieren un byte menos para codificarse que los números más altos, por lo que, como optimización, puede decidir utilizar estos números de elementos repetidos o de uso común, dejando los números de campo 16 y 18. Más alto para elementos opcionales que se utilizan con menos frecuencia. Cada campo de elemento necesita recodificar el número de campo, por lo que los campos repetidos, especialmente, esta es una buena forma de optimizar.

Cada campo debe anotarse con uno de los siguientes modificadores:

  • optional: El campo se puede configurar o no . Si es un valor de campo opcional, utilice el valor predeterminado. typePara tipos simples, puede especificar su propio valor predeterminado, como hicimos con el número de teléfono en nuestro ejemplo . De lo contrario, se utilizan los valores predeterminados del sistema: cero para tipos numéricos, cadena para cadenas vacías y falso para booleanos. Para mensajes incrustados, el valor predeterminado es siempre la "instancia predeterminada" o "prototipo" del mensaje, que no tiene campos establecidos. Llamar a un descriptor de acceso para un campo opcional (o obligatorio) que no siempre está configurado explícitamente devuelve el valor predeterminado para ese campo.
  • repeated: El campo se puede repetir cualquier número de veces (incluido cero). El orden de los valores repetidos se conservará en los buffers de protocolo. Trate los campos repetidos como matrices de tamaño dinámico .
  • required: Se debe proporcionar un valor para el campo ; de lo contrario, el mensaje se considerará "no inicializado". Si libprotobufse compila en modo de depuración, la serialización de mensajes no inicializados provocará que falle una aserción. En una compilación optimizada, se omitirá la verificación y el mensaje se escribirá de todos modos. Sin embargo, el análisis de un mensaje no inicializado siempre fallará (al regresar del método de análisis false). Aparte de eso, los campos obligatorios se comportan exactamente como los campos opcionales.

Importante

 Debe tener mucho cuidado al marcar campos como  Required Is Foreverrequired . Si en algún momento desea dejar de escribir o enviar un campo obligatorio, cambie el campo a opcional: los lectores antiguos considerarán el mensaje y el campo está incompleto y podrán rechazarlo. o desecharlos sin querer. Deberías considerar los amortiguadores para ti. Los campos están fuertemente desfavorecidos dentro de Google  required ; la mayoría de los mensajes definidos en la sintaxis de proto2 usan  optional y y  repeated solo (Proto3 no admite solo (Proto3 no admite  required campos en absoluto.) todos los campos de).

Encontrará una guía completa para escribir .protoarchivos, incluidos todos los tipos de campos posibles, en  la Guía de lenguaje de buffers de protocolo . No busque algo como herencia de clases: los buffers de protocolo no sirven.

Compilando sus buffers de protocolo Compilando buffers de protocolo

Ahora que lo tienes .proto, lo siguiente que debes hacer es generar las clases que necesitas para leer y escribir AddressBook(por lo tanto Person, y  PhoneNumber) mensajes. protocPara hacer esto, debe ejecutar el compilador del búfer de protocolo .proto:

  1. Si no tiene el compilador instalado,  descargue el paquete y siga las instrucciones en el archivo README.

  2. Ahora ejecute el compilador, especificando el directorio fuente (donde todavía existe el código fuente de su aplicación, si no proporciona un valor), el directorio de destino (donde desea que vaya el código generado; generalmente el mismo) y la ruta. a tu .en $SRC_DIReste  .protocaso,tú……:

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

Dado que las clases de C++ son obligatorias, --cpp_outse proporciona la opción -similar para otros lenguajes compatibles.

Esto generará los siguientes archivos en el directorio de destino especificado:

    • addressbook.pb.h, el encabezado que declara las clases generadas.
    • addressbook.pb.cc, que contiene la implementación de sus clases.

La API de búfer de protocolo API de búfer de protocolo

Echemos un vistazo al código generado para ver qué clases y funciones creó el compilador para usted. Si observa Addressbook.pb.h, puede ver que hay una clase para cada mensaje especificado en Addressbook.proto. Si observa de cerca la clase Persona, puede ver que el compilador generó descriptores de acceso para cada campo. Por ejemplo, para los campos de nombre, identificación, correo electrónico y teléfonos, se pueden utilizar los siguientes métodos:

// name
  inline bool has_name() const;
  inline void clear_name();
  inline const ::std::string& name() const;
  inline void set_name(const ::std::string& value);
  inline void set_name(const char* value);
  inline ::std::string* mutable_name();

  // id
  inline bool has_id() const;
  inline void clear_id();
  inline int32_t id() const;
  inline void set_id(int32_t value);

  // email
  inline bool has_email() const;
  inline void clear_email();
  inline const ::std::string& email() const;
  inline void set_email(const ::std::string& value);
  inline void set_email(const char* value);
  inline ::std::string* mutable_email();

  // phones
  inline int phones_size() const;
  inline void clear_phones();
  inline const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
  inline ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
  inline const ::tutorial::Person_PhoneNumber& phones(int index) const;
  inline ::tutorial::Person_PhoneNumber* mutable_phones(int index);
  inline ::tutorial::Person_PhoneNumber* add_phones();

Como puede ver, el captador tiene exactamente el mismo nombre que el campo en minúsculas y el método de establecimiento comienza con set_. También existe has_un método de campo singular (obligatorio u opcional), si el campo está preparado. Finalmente, cada campo tiene un clear_método que restaura el campo a un estado vacío.

Mientras que los campos numéricos idsolo tienen el conjunto básico de descriptores de acceso descritos anteriormente,  namey emaillos campos tienen dos métodos adicionales al igual que las cadenas: un captador que le permite mutable_ obtener un puntero a una cadena directamente y una configuración de establecimiento mutable_email()Si tuviera un campo de mensaje repetido en este ejemplo, también tendría un email método en lugar de mutable_un método.

También existen algunos métodos especiales para campos repetidos: si observa phoneslos campos repetidos, verá

  • Compruebe si hay campos duplicados _size(en otras palabras, cuántos números de teléfono  Persontiene 2#2).
  • Obtiene el número de teléfono especificado mediante un índice.
  • Actualiza un número de teléfono existente en el índice especificado.
  • Agregue otro número de teléfono al mensaje, que luego podrá editar (hay uno para el tipo escalar repetido add_, solo le permite pasar el nuevo valor).

Consulte la Referencia de código generado en C++ para obtener detalles sobre qué miembros genera el compilador de protocolos para cualquier definición de campo específica  .

Enumeraciones y clases anidadas

El código generado incluye una enumeración PhoneTypeque corresponde a la suya . .protoPuede llamar a este tipo Person::PhoneTypey sus valores son  Person::MOBILE, Person::HOMEy Person::WORK(los detalles de implementación son un poco más complicados, pero no necesita conocerlos para usar enumeraciones).

El compilador también genera para usted una clase llamada  Person::PhoneNumber. Si observa el código, puede ver que la clase "real" en realidad se llama Person_PhoneNumber, pero define internamente un typedef  Personque le permite tratarla como una clase anidada. El único caso en el que esto haría una diferencia es si lo desea en otro archivo: no puede declarar hacia adelante tipos anidados en C++, pero puede declarar hacia adelante  Person_PhoneNumber.

método de mensaje estándar

Cada clase de mensaje también contiene otros métodos que le permiten inspeccionar o manipular el mensaje completo, incluidos:

  • bool IsInitialized() const;: Verifique que todos los campos obligatorios estén ingresados ​​y listos.
  • string DebugString() const;: Devuelve un mensaje legible por humanos, especialmente útil para la depuración.
  • void CopyFrom(const Person& from);: Utilice el valor del mensaje proporcionado.
  • void Clear();: Borra todos los elementos a un estado vacío.

Estos métodos y los métodos de E/S descritos en la siguiente sección implementan  Messageuna interfaz compartida por todas las clases de búfer del protocolo C++. Consulte la documentación API completa de Message para obtener más información  .

análisis y serialización

Finalmente, cada clase Protocol Buffers tiene métodos para escribir y leer mensajes en el  formato binario del tipo seleccionado , estos incluyen:

  • bool SerializeToString(string* output) const;: serializa el mensaje y almacena los bytes en la cadena dada. Tenga en cuenta que los bytes son binarios, no texto; simplemente usamos stringclases como contenedores convenientes.
  • bool ParseFromString(const string& data);: de la cadena dada
  • bool SerializeToOstream(ostream* output) const;: escribir mensaje en C++ #2
  • bool ParseFromIstream(istream* input);: del C++ dado  istream.

Estas son sólo algunas de las opciones proporcionadas para el análisis y la serialización. Nuevamente, consulte  la Referencia de la API de mensajes para obtener  una lista completa.

escribe un mensaje

Ahora intentemos usar las clases de búfer de protocolo. Lo primero que puede hacer la aplicación de libreta de direcciones es escribir datos personales en su archivo de libreta de direcciones. Para hacer esto, necesita crear y completar clases de búfer de protocolo y luego escribirlas en el flujo de salida.

A continuación se muestra un programa que lee un archivo AddressBook, agrega uno nuevo  Personsegún la entrada del usuario AddressBook, escribe el nuevo en él y escribe el nuevo indefinido en el archivo nuevamente. El compilador del protocolo resalta las llamadas o referencias directas.

#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;

// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
  cout << "Enter person ID number: ";
  int id;
  cin >> id;
  person->set_id(id);
  cin.ignore(256, '\n');

  cout << "Enter name: ";
  getline(cin, *person->mutable_name());

  cout << "Enter email address (blank for none): ";
  string email;
  getline(cin, email);
  if (!email.empty()) {
    person->set_email(email);
  }

  while (true) {
    cout << "Enter a phone number (or leave blank to finish): ";
    string number;
    getline(cin, number);
    if (number.empty()) {
      break;
    }

    tutorial::Person::PhoneNumber* phone_number = person->add_phones();
    phone_number->set_number(number);

    cout << "Is this a mobile, home, or work phone? ";
    string type;
    getline(cin, type);
    if (type == "mobile") {
      phone_number->set_type(tutorial::Person::MOBILE);
    } else if (type == "home") {
      phone_number->set_type(tutorial::Person::HOME);
    } else if (type == "work") {
      phone_number->set_type(tutorial::Person::WORK);
    } else {
      cout << "Unknown phone type.  Using default." << endl;
    }
  }
}

// Main function:  Reads the entire address book from a file,
//   adds one person based on user input, then writes it back out to the same
//   file.
int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  if (argc != 2) {
    cerr << "Usage:  " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
    return -1;
  }

  tutorial::AddressBook address_book;

  {
    // Read the existing address book.
    fstream input(argv[1], ios::in | ios::binary);
    if (!input) {
      cout << argv[1] << ": File not found.  Creating a new file." << endl;
    } else if (!address_book.ParseFromIstream(&input)) {
      cerr << "Failed to parse address book." << endl;
      return -1;
    }
  }

  // Add an address.
  PromptForAddress(address_book.add_people());

  {
    // Write the new address book back to disk.
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!address_book.SerializeToOstream(&output)) {
      cerr << "Failed to write address book." << endl;
      return -1;
    }
  }

  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();

  return 0;
}

Cuidado con GOOGLE_PROTOBUF_VERIFY_VERSIONlas macros. Es una buena práctica, aunque no estrictamente necesaria, ejecutar esta biblioteca de macrobúfer antes de utilizar el protocolo C++. Verifica que no haya vinculado accidentalmente una versión de la biblioteca que sea incompatible con la versión de los encabezados que compiló. Si se detecta una discrepancia en la versión, el programa se cancelará. Notas .pb.ccEsta macro es llamada automáticamente por cada archivo al inicio.

Tenga en cuenta también ShutdownProtobufLibrary()la llamada al final del programa. Todo lo que esto hace es eliminar cualquier objeto global asignado por la biblioteca de búfer de protocolo. Esto es innecesario para la mayoría de los programas, ya que el proceso debe salir de todos modos y el sistema operativo se encargará de recuperar toda su memoria. Sin embargo, si lo requiere el verificador de pérdida de memoria utilizado o si está escribiendo una biblioteca que se puede cargar y descargar varias veces, es posible que desee forzar a Protocol Buffers a limpiar todo.

Definir el tipo de mensaje

Primero veamos un ejemplo muy simple. Suponga que desea definir un formato de mensaje de solicitud donde cada solicitud de búsqueda tenga una cadena de consulta, las páginas específicas de resultados que le interesan y la cantidad de resultados para cada página. A continuación se muestra el archivo que utiliza para definir los tipos de mensajes .proto.

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • La primera línea del archivo especifica qué sintaxis está utilizandoproto3 : si no lo hace, el compilador del búfer de protocolo asumirá que está utilizando  el prototipo 2. Esta debe ser la primera línea del archivo que no esté vacía ni sin comentarios.
  • SearchRequestUna definición de mensaje especifica tres campos (pares nombre/valor) , uno para incluir mensajes de este tipo. Cada campo tiene un nombre y un tipo.

Especificar tipo de campo

En el ejemplo anterior, todos los campos son de tipo escalar : dos números enteros ( page_numbery results_per_page) y una cadena ( query). También es posible especificar campos para tipos enumerados y compuestos, como otros tipos de mensajes.

Supongo que te gusta

Origin blog.csdn.net/qq_44632658/article/details/130978314
Recomendado
Clasificación