Protocol Buffer para C#


¿Por qué usar los  Protocol Buffer?
El ejemplo que vamos a utilizar es una aplicación de "libreta de direcciones" muy simple que puede leer y escribir datos de contacto de personas desde y hacia un archivo. Cada persona en la libreta de direcciones tiene un nombre, una identificación, una dirección de correo electrónico y un número de teléfono de contacto.

¿Cómo serializas y recuperas datos estructurados como este? 

Hay algunas formas de resolver este problema:

Use la serialización binaria de .NET con System.Runtime.Serialization.Formatters.Binary.BinaryFormatter y las clases asociadas. 

Esto termina siendo muy frágil frente a los cambios, costoso en términos de tamaño de datos en algunos casos. Tampoco funciona muy bien si necesita compartir datos con aplicaciones escritas para otras plataformas.

Puede inventar una forma ad-hoc para codificar los elementos de datos en una sola cadena, como codificar 4 entradas como "12: 3: -23: 67". Este es un enfoque simple y flexible, aunque requiere escribir códigos únicos de codificación y análisis sintáctico, y el análisis impone un pequeño costo de tiempo de ejecución. Esto funciona mejor para codificar datos muy simples.
Serializar los datos a XML Este enfoque puede ser muy atractivo ya que XML es (algo así) legible por humanos y hay bibliotecas vinculantes para muchos idiomas. Esta puede ser una buena opción si desea compartir datos con otras aplicaciones / proyectos. Sin embargo, XML es notoriamente intensivo en espacio, y la codificación / decodificación puede imponer una gran penalización de rendimiento en las aplicaciones. Además, navegar un árbol XML DOM es considerablemente más complicado que navegar campos simples en una clase normalmente.

Los Protocol Buffer son la solución flexible, eficiente y automatizada para resolver exactamente este problema. Con los búferes de protocolo, usted escribe una descripción .proto de la estructura de datos que desea almacenar. A partir de eso, el compilador de búfer de protocolo crea una clase que implementa la codificación automática y el análisis de los datos del búfer de protocolo con un formato binario eficiente. La clase generada proporciona getters y setters para los campos que componen un buffer de protocolo y se ocupa de los detalles de lectura y escritura del buffer de protocolo como una unidad. Es importante destacar que el formato de búfer de protocolo admite la idea de ampliar el formato a lo largo del tiempo de tal forma que el código aún pueda leer datos codificados con el formato anterior.

¿Donde podemos encontrar código?
https://github.com/protocolbuffers/protobuf/blob/master/csharp/src/AddressBook/Program.cs
Ejemplos
https://github.com/protocolbuffers/protobuf/tree/master/examples
https://github.com/protocolbuffers/protobuf/tree/master/csharp/src/AddressBook

Definiendo su formato de protocolo
Para crear su aplicación de libreta de direcciones, deberá comenzar con un archivo .proto. Las definiciones en un archivo .proto son simples: agrega un mensaje para cada estructura de datos que desea serializar, luego especifica un nombre y un tipo para cada campo en el mensaje. En nuestro ejemplo, el archivo .proto que define los mensajes es addressbook.proto.

syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
En C #, las clases generadas se colocarán en un espacio de nombres que coincida con el nombre del paquete si no se especifica csharp_namespace. En nuestro ejemplo, se ha especificado la opción csharp_namespace para anular el valor predeterminado, por lo que el código generado utiliza un espacio de nombre de Google.Protobuf.Examples.AddressBook en lugar de Tutorial.

option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
Luego, tienes tus definiciones de mensaje. Un mensaje es simplemente un agregado que contiene un conjunto de campos tipados. Muchos tipos de datos estándar simples están disponibles como tipos de campo, incluidos bool, int32, float, double y string. También puede agregar más estructura a sus mensajes usando otros tipos de mensajes como tipos de campo.

message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

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

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}

En el ejemplo anterior, el mensaje de persona contiene mensajes de PhoneNumber, mientras que el de la libreta de direcciones contiene mensajes de persona. Incluso puede definir tipos de mensajes anidados dentro de otros mensajes; como puede ver, el tipo PhoneNumber se define dentro de Person. También puede definir tipos de enumeración si desea que uno de sus campos tenga una lista de valores predefinida; aquí debe especificar que un número de teléfono puede ser uno de MOBILE, HOME o WORK.

Los marcadores "= 1", "= 2" en cada elemento identifican la "etiqueta" única que ese campo utiliza en la codificación binaria. Los números de etiqueta 1-15 requieren un byte menos para codificar que los números más altos, por lo que, como optimización, puede decidir usar esas etiquetas para los elementos comúnmente utilizados o repetidos, dejando etiquetas 16 y superiores para los elementos opcionales menos utilizados. Cada elemento en un campo repetido requiere una nueva codificación del número de etiqueta, por lo que los campos repetidos son particularmente buenos candidatos para esta optimización.

Si no se establece un valor de campo, se usa un valor predeterminado: cero para los tipos numéricos, la cadena vacía para las cadenas, falso para los bools. Para mensajes incrustados, el valor predeterminado es siempre la "instancia predeterminada" o "prototipo" del mensaje, que no tiene ninguno de sus campos establecidos. Llamar al descriptor de acceso para obtener el valor de un campo que no se ha establecido explícitamente siempre devuelve el valor predeterminado de ese campo.

Si se repite un campo, el campo se puede repetir cualquier cantidad de veces (incluso cero). El orden de los valores repetidos se conservará en el búfer de protocolo. Piense en campos repetidos como matrices de tamaño dinámico.

Encontrará una guía completa para escribir archivos .proto, incluidos todos los tipos de campo posibles, en la Guía de idioma del búfer de protocolo. No busques instalaciones similares a la herencia de clase, sin embargo, los búferes de protocolo no hacen eso.
Compilando sus búfers de protocolo
Ahora que tienes un .proto, lo siguiente que debes hacer es generar las clases que necesitarás para leer y escribir los mensajes de AddressBook (y por lo tanto Person y PhoneNumber). Para hacer esto, necesita ejecutar el protocolo del compilador del buffer del protocolo en su .proto:

Si no ha instalado el compilador, descargue el paquete y siga las instrucciones en el archivo README.
Ahora ejecute el compilador, especificando el directorio de origen (donde vive el código fuente de su aplicación, el directorio actual se usa si no proporciona un valor), el directorio de destino (donde desea que vaya el código generado, a menudo lo mismo que $ SRC_DIR), y el camino a su .proto. En este caso, tú ...
protocolo -I = $ SRC_DIR --csharp_out = $ DST_DIR $ SRC_DIR / addressbook.proto
Como quiere las clases C #, usa la opción --csharp_out - se proporcionan opciones similares para otros lenguajes compatibles.
Esto genera Addressbook.cs en su directorio de destino especificado. Para compilar este código, necesitará un proyecto con una referencia al ensamblado Google.Protobuf.

Las clases de la libreta de direcciones
Generar Addressbook.cs le da cinco tipos útiles:

Una clase de libreta de direcciones estática que contiene metadatos sobre los mensajes del búfer de protocolo.
Una clase AddressBook con una propiedad People de solo lectura.
Una clase Person con propiedades para Name, Id, Email y Phones.
Una clase PhoneNumber, anidada en una clase Person.Types estática.
Una enumeración PhoneType, también anidada en Person.Types.
Puede leer más acerca de los detalles de lo que se generó exactamente en la guía C # Generated Code, pero en su mayor parte puede tratarlos como tipos de C # perfectamente normales. Un punto a destacar es que las propiedades correspondientes a los campos repetidos son de solo lectura. Puede agregar elementos a la colección o eliminar elementos de ella, pero no puede reemplazarla con una colección completamente separada. El tipo de colección para campos repetidos siempre es RepeatedField <T>. Este tipo es como List <T> pero con algunos métodos de conveniencia adicionales, como una sobrecarga Add que acepta una colección de elementos, para usar en los inicializadores de recopilación.

Aquí hay un ejemplo de cómo puede crear una instancia de Persona:

Person john = new Person
{
    Id = 1234,
    Name = "John Doe",
    Email = "jdoe@example.com",
    Phones = { new Person.Types.PhoneNumber { Number = "555-4321", Type = Person.Types.PhoneType.HOME } }
};
Note that with C# 6, you can use using static to remove the Person.Types ugliness:
// Add this to the other using directives using static Google.Protobuf.Examples.AddressBook.Person.Types; ... // The earlier Phones assignment can now be simplified to: Phones = { new PhoneNumber { Number = "555-4321", Type = PhoneType.HOME } }
Parseo y serialización
El propósito de utilizar memorias intermedias de protocolo es serializar sus datos para que puedan ser analizados en otro lugar. Cada clase generada tiene un método WriteTo (CodedOutputStream), donde CodedOutputStream es una clase en la biblioteca de tiempo de ejecución del búfer de protocolo. Sin embargo, generalmente usará uno de los métodos de extensión para escribir en un System.IO.Stream regular o convertir el mensaje a una matriz de bytes o ByteString. Estos mensajes de extensión están en la clase Google.Protobuf.MessageExtensions, por lo que cuando quiera serializar, generalmente querrá una directiva de uso para el espacio de nombres Google.Protobuf. Por ejemplo:

using Google.Protobuf;
...
Person john = ...; // Code as before
using (var output = File.Create("john.dat"))
{
    john.WriteTo(output);
}
El análisis también es simple. Cada clase generada tiene una propiedad de Analizador estática que devuelve un MessageParser <T> para ese tipo. Eso a su vez tiene métodos para analizar secuencias, matrices de bytes y ByteStrings. Entonces, para analizar el archivo que acabamos de crear, podemos usar:

Person john;
using (var input = File.OpenRead("john.dat"))
{
    john = Person.Parser.ParseFrom(input);
}
Un programa de ejemplo completo para mantener una libreta de direcciones (agregar nuevas entradas y enumerar las existentes) utilizando estos mensajes está disponible en el repositorio de Github.

Extendiendo un buffer de protocolo
Tarde o temprano, después de que libere el código que utiliza su búfer de protocolo, sin duda querrá "mejorar" la definición del búfer de protocolo. Si desea que sus nuevos almacenamientos intermedios sean compatibles con versiones anteriores, y sus almacenamientos intermedios antiguos sean compatibles con versiones anteriores (y es casi seguro que sí lo desea), existen algunas reglas que debe seguir. En la nueva versión del buffer de protocolo:

no debe cambiar los números de etiqueta de ningún campo existente.
usted puede eliminar campos.
puede agregar nuevos campos pero debe usar números de etiqueta nuevos (es decir, números de etiqueta que nunca se usaron en este búfer de protocolo, ni siquiera por campos eliminados).
(Hay algunas excepciones a estas reglas, pero rara vez se usan).

Si sigues estas reglas, el código antiguo leerá felizmente nuevos mensajes y simplemente ignorará cualquier campo nuevo. Para el código anterior, los campos singulares que se eliminaron simplemente tendrán su valor predeterminado, y los campos repetidos eliminados estarán vacíos. El nuevo código también leerá mensajes antiguos de manera transparente.

Sin embargo, tenga en cuenta que los nuevos campos no estarán presentes en los mensajes antiguos, por lo que deberá hacer algo razonable con el valor predeterminado. Se utiliza un valor predeterminado específico del tipo: para cadenas, el valor predeterminado es la cadena vacía. Para booleanos, el valor predeterminado es falso. Para los tipos numéricos, el valor predeterminado es cero.

Reflexión
Las descripciones de mensajes (la información en el archivo .proto) y las instancias de los mensajes se pueden examinar mediante programación utilizando la API de reflexión. Esto puede ser útil cuando se escribe un código genérico, como un formato de texto diferente o una herramienta de diferencia inteligente. Cada clase generada tiene una propiedad Descriptor estática, y el descriptor de cualquier instancia se puede recuperar utilizando la propiedad IMessage.Descriptor. Como un ejemplo rápido de cómo se pueden usar, aquí hay un método breve para imprimir los campos de nivel superior de cualquier mensaje.

public void PrintMessage(IMessage message)
{
    var descriptor = message.Descriptor;
    foreach (var field in descriptor.Fields.InDeclarationOrder())
    {
        Console.WriteLine(
            "Field {0} ({1}): {2}",
            field.FieldNumber,
            field.Name,
            field.Accessor.GetValue(message);
    }
}

Referencias:
https://developers.google.com/protocol-buffers/docs/csharptutorial?hl=es-419
https://github.com/protocolbuffers/protobuf/blob/master/csharp/src/AddressBook/Program.cs
https://github.com/protocolbuffers/protobuf/tree/master/examples
https://github.com/protocolbuffers/protobuf/tree/master/csharp/src/AddressBook

Comentarios

Entradas más populares de este blog

¿Cómo buscar tweets antiguos de una persona?

¿Qué es la Norma GAMP 5 y para que sirve?

¿Que tipos de Mensajes de HL7 hay?