Ha pasado mucho tiempo desde el artículo anterior donde expliqué cómo compilar protobuf con soporte de zlib. En este artículo explicaré cómo usar flujos (streams) de C++ basados en zlib para serializar y deserealizar mensajes Protobuf. Usaremos un pequeño bitmap de la Mona Lisa para este ejemplo, ya que su formato descomprimido compactará muy bien con el algoritmo Deflate usado por zlib.
Echa un vistazo a Simverge/howto-protobuf-zlib en GitHub para una implementación más completa de este ejemplo.
Define un esquema de mensaje Protobuf
Primero, creemos un archivo llamado blob.proto que defina un simple mensaje Protobuf que almacene una collección arbitraria de datos además de una cadena de texto que identifique su origen:
1 2 3 4 5 6 7 8 9 |
syntax = "proto3"; package simverge; message Blob { string origen = 1; bytes datos = 2; } |
Compila blob.proto con el compilador de Protobuf (protoc) usando las instrucciones del tutorial oficial de Protobuf en C++. Por ejemplo, el siguiente comando generaría el archivo fuente para C++ llamado blob.pb.cc y la cabecera para C++ llamada blob.pb.h en el mismo directorio dónde reside blob.proto:
1 |
protoc -I=. --cpp_out=. blob.proto |
Otra opción es usar el módulo FindProtobuf si compilas con CMake, por ejemplo:
1 2 3 4 5 6 |
find_package(Protobuf REQUIRED) # Los archivos generados van a ser listados en PROTOBUF_FUENTES y PROTOBUF_CABECERAS protobuf_generate_cpp(PROTOBUF_FUENTES PROTOBUF_CABECERAS blob.proto) # Resto del script CMake ... |
Cuando blob.proto es compilado a archivos de C++ y blob.pb.cc es incluído en la compilación, puedes incluír la cabecera blob.pb.h para crear un mensaje Protobuf de tipo simverge::Blob, definir sus campos de origen y datos, y determinar el tamaño del mensaje Protobuf descomprimido:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include "blob.pb.h" #include <fstream> #include <iostream> #include <string> int main(int argc, char **argv) { GOOGLE_PROTOBUF_VERIFY_VERSION; std::string origen("Mona_Lisa.bmp"); std::ifstream entrada(origen, std::ios::binary); if (entrada) { simverge::Blob blob; *blob.mutable_origen() = origen; auto datos = blob.mutable_datos(); // Calcula el tamaño del flujo de entrada para reservar bytes en el campo de datos entrada.unsetf(std::ios::skipws); entrada.seekg(0, std::ios::end); datos->reserve(entrada.tellg()); entrada.seekg(0, std::ios::beg); datos->assign(std::ifstreambuf_iterator<char>(entrada), std::ifstreambuf_iterator<char>()); entrada.close(); std::cout << "Un mensaje Protobuf ha sido creado con " << datos->size() << " bytes que proceden de " << origen << std::endl; std::cout << "El mensaje Protobuf descomprimido contiene " << blob.ByteSizeLong() << " bytes" << std::endl; } return 0; } |
Al ejecutar este programa vemos el tamaño del bitmap de la Mona Lisa y del mensaje Protobuf que lo envuelve:
1 2 |
Un mensaje Protobuf ha sido creado con 366414 bytes que proceden de Mona_Lisa.bmp El mensaje Protobuf descomprimido contiene 366433 bytes |
Comprime y escribe el mensaje Protobuf al disco
Podemos luego usar un flujo de salida del tipo GzipOutputStream definido en el espacio de nombres (namespace) google::protobuf::io para serializar y comprimir el mensaje. El constructor de esta clase requiere un puntero a un subflujo de tipo ZeroCopyOutputStream para procesar los datos comprimidos. Usaremos un objeto ArrayOutputStream para determinar el tamaño de los datos comprimidos y después un objeto std::ofstream para escribir el arreglo de bytes al disco. También confirmaremos que no hayan errores de zlib anted de escribir el mensaje Protobuf comprimido al disco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
#include "blob.pb.h" #include <google/protobuf/io/zero_copy_stream_impl.h> #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <google/protobuf/io/gzip_stream.h> #include <fstream> #include <iostream> #include <memory> #include <string> int main(int argc, char **argv) { GOOGLE_PROTOBUF_VERIFY_VERSION; std::string origen("Mona_Lisa.bmp"); std::string destino("Mona_Lisa.pbz"); std::ifstream entrada(origen, std::ios::binary); if (entrada) { simverge::Blob blob; *blob.mutable_origen() = origen; auto datos = blob.mutable_datos(); // Calcula el tamaño del flujo de entrada para reservar bytes en el campo de datos entrada.unsetf(std::ios::skipws); entrada.seekg(0, std::ios::end); datos->reserve(entrada.tellg()); entrada.seekg(0, std::ios::beg); datos->assign(std::ifstreambuf_iterator<char>(entrada), std::ifstreambuf_iterator<char>()); entrada.close(); std::cout << "Un mensaje Protobuf ha sido creado con " << datos->size() << " bytes que proceden de " << origen << std::endl; auto bytesDescomprimidos = blob.ByteSizeLong(); std::cout << "El mensaje Protobuf descomprimido contiene " << bytesDescomprimidos << " bytes" << std::endl; std::unique_ptr<char[]> arreglo(new char[bytesDescomprimidos]); google::protobuf::io::ArrayOutputStream aos(arreglo.get(), (int) bytesDescomprimidos); google::protobuf::io::GzipOutputStream gos(&aos); if (blob.SerializeToZeroCopyStream(&gos)) { gos.Close(); if (gos.ZlibErrorCode() > 0) { auto bytesComprimidos = aos.ByteCount(); std::ofstream salida(destino, std::ios::binary); salida.write(arreglo.get(), bytesComprimidos); std::cout << "El mensaje Protobuf fue comprimido a " << bytesComprimidos << " bytes (" << (100.0 * (uncompressedBytes - compressedBytes) / uncompressedBytes) << "%): " << destino << std::endl; } } } return 0; } |
Al ejecutar este programa vemos que la configuración por defecto de GzipOutputStream brinda una tasa de compresión de aproximadamente 22.6% con el bitmap de la Mona Lisa. Puedes ajustar la configuración de compresión con zlib pasando un objeto de tipo GzipOutputStream::Options al constructor de GzipOutputStream.
1 2 3 4 5 |
Un mensaje Protobuf ha sido creado con 366414 bytes que proceden de Mona_Lisa.bmp El mensaje Protobuf descomprimido contiene 366433 bytes El mensaje Protobuf fue comprimido a 283638 bytes (22.5949%): Mona_Lisa.pbz |
Una forma más sencilla de serializar los datos comprimidos es usar como subflujo de salida un objeto de tipo OstreamOutputStream o FileOutputStream en vez de ArrayOutputStream.
Lee y descomprime el mensaje Protobuf
Usemos un flujo de entrada de tipo GzipInputStream definido en google::protobuf::io para leer y descomprimir el mensaje. El constructor de esta clase requiere un puntero a un subflujo de tipo ZeroCopyInputStream para leer los datos antes de la descompresión. Usaremos un objeto de tipo IstreamInputStream como subflujo (el cual requiere un puntero a un flujo de entrada de tipo std::istream) en contraste al subflujo basado en un arreglo de bytes que usamos en el ejemplo de salida. Tal cómo hicimos en el ejemplo de salida, revisaremos si hay errores de zlib antes de procesar los datos descomprimidos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include "blob.pb.h" #include <google/protobuf/io/zero_copy_stream_impl.h> #include <google/protobuf/io/zero_copy_stream_impl_lite.h> #include <google/protobuf/io/gzip_stream.h> #include <fstream> #include <iostream> #include <string> int main(int argc, char **argv) { std::string protobuf("Mona_Lisa.pbz"); std::string bitmap("Mona_Lisa_descomprimido.bmp"); std::ifstream entrada(protobuf, std::ios::binary); if (entrada) { entrada.unsetf(std::ios::skipws); entrada.seekg(0, std::ios::end); std::cout << "Lectura de archivo con" << entrada.tellg() << " bytes: " << protobuf << std::endl; entrada.seekg(0, std::ios::beg); google::protobuf::io::IstreamInputStream iss(&entrada); google::protobuf::io::GzipInputStream gis(&iss); simverge::Blob blob; if (blob.ParseFromZeroCopyStream(&gis) && gis.ZlibErrorCode() > 0) { std::cout << "Mensaje Protobuf descomprimido y analizado: " << blob.ByteSize() << " bytes" << std::endl; std::cout << "El mensaje contiene " << blob.data().size() << " bytes que proceden de " << blob.source() << std::endl; std::ofstream salida(bitmap, std::ios::binary); salida.write(blob.data().c_str(), blob.data().size());. std::cout << "Datos del mensaje Protobuf fueron escritos a " << bitmap << std::endl; } } return 0; } |
Al ejecutar este programa vemos que el mensaje Protobuf descomprimido y analizado desde el archivo corresponde al que fue creado por el ejemplo de salida, además de corresponder el tamaño de los datos con el del archivo bitmap.
1 2 3 4 5 6 |
Lectura de archivo con 283638 bytes: Mona_Lisa.pbz Mensaje Protobuf descomprimido y analizado: 366433 bytes El mensaje contiene 366414 bytes que proceden de Mona_Lisa.bmp Datos del mensaje Protobuf fueron escritos a Mona_Lisa_descomprimido.bmp |
Esta obra de Simverge Software LLC está bajo una Licencia Creative Commons Atribución 4.0 Internacional.