Principio y ejemplos de Protobuf (codificación Varint)

definicion de protobuf

  • Los búferes de protocolo son una forma extensible e independiente del idioma y de la plataforma para serializar datos estructurados, que se pueden usar para protocolos de comunicación (de datos), almacenamiento de datos y más.
  • protobuf (Protocol Buffers) es un marco de codificación de serialización que a menudo aparece en algunos protocolos RPC (llamada remota). Pero, de hecho, protobuf puede entenderse como un protocolo de serialización. Al igual que json y xml, el uso de este marco requiere datos construidos en su propia estructura.
  • Además, el volumen de serialización de protobuf es mucho más pequeño que el de xml y json, por lo que protobuf se usa a menudo en algunos marcos de red.

características de protobuf:

  • ProtoBuf es compatible con Java, C++, Python y otros lenguajes, y es compatible con múltiples plataformas. Y el cliente y el servidor también se pueden escribir en diferentes idiomas.
  • El uso de la velocidad de transmisión de datos serializados protobuf es rápido, varias veces más rápido que los datos XML. Principalmente gracias a la forma en que lo codificó.
  • Buena escalabilidad y compatibilidad. Actualice la estructura de datos sin afectar o destruir el antiguo programa original.

instancia protobuf

Proporcione un ejemplo de transmisión de red de información de inicio de sesión y familiarícese con el uso de protobuf para transmitir datos al servidor.

La información de inicio de sesión incluye 5 datos:

  • nombre de usuario
  • contraseña
  • Estado en línea
  • Tipo de cliente
  • versión_cliente
  1. Escriba el archivo proto de la siguiente manera y coloque la estructura de datos transmitida en el campo del mensaje. mensaje es una palabra clave seguida de un nombre de mensaje personalizado, como IMLoginReq:
syntax = "proto3"; // 版本指定,包括proto2和proto3 版本
package IM.Login;	//IM::Login -> package IM.Login   类似于命名空间
import "IM.BaseDefine.proto";	// 引用文件 引用其他的proto文件

option optimize_for = LITE_RUNTIME;  //编译优化
//IMLoginReq:描述的一个类
message IMLoginReq{
    
    
	string user_name = 1;
	string password = 2;
	IM.BaseDefine.UserStatType online_status = 3;
	IM.BaseDefine.ClientType client_type = 4;
	string client_version = 5;
}

Nota: El número de cada campo debe definirse en orden a partir de 1. El número mínimo es 1 y el máximo es 2^29 -1, que es 536,870,911, de los cuales no se pueden usar de 19000 a 19999 (el valor predeterminado es para Búferes de protocolo).

  1. Después de escribir el archivo proto, compílelo a través de protoc
protoc --cpp_out=. login.proto

El resultado de la compilación aquí generará dos archivos, a saber, login.pb.h y login.pb.cc. Hay algunas interfaces en el archivo generado que deben usarse más adelante. Los archivos generados deben copiarse en el cliente y el servidor, respectivamente, para las llamadas de interfaz.

3. Configuración del cliente

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "login.pb.h"

using namespace std;

int main() {
    
    
  // 创建一个msg对象
  // 设置登录信息
	IM::Login::IMLoginReq msg;
	msg.set_user_name("aries");
	msg.set_password("123456");	
	msg.set_online_status(IM::BaseDefine::USER_STATUS_ONLINE);
	msg.set_client_type(IM::BaseDefine::CLIENT_TYPE_WINDOWS);
	msg.set_client_version("1.0");

  // 将Person对象序列化为字节流
  string buffer;
  msg.SerializeToString(&buffer);

  // 创建socket并连接到服务器
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  struct sockaddr_in servaddr;
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
  servaddr.sin_port = htons(8080);
  connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));

  // 发送字节流到服务器
  send(sockfd, buffer.c_str(), buffer.size(), 0);

  // 关闭socket
  close(sockfd);

  return 0;
}

Nota: Las interfaces de función de set_user_name y set_password se generan automáticamente en archivos .cc y .h a través de protoc.

Donde msg.SerializeToString(&buffer);se usa para serializar datos de mensajes. Aquí los datos serializados se pueden transmitir a través del protocolo de comunicación para ahorrar ancho de banda de transmisión.

4. Configuración del servidor

El servidor recibirá el flujo de bytes enviado por el cliente y lo analizará en un objeto de mensaje.

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "login.pb.h"

using namespace std;

int main() {
    
    
  // 创建socket并绑定到端口
  int listenfd = socket(AF_INET, SOCK_STREAM, 0);
  struct sockaddr_in servaddr;
  servaddr.sin_family = AF_INET;
  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  servaddr.sin_port = htons(8080);
  bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
  listen(listenfd, 1);

  // 接收客户端连接并接收数据
  struct sockaddr_in cliaddr;
  socklen_t clilen = sizeof(cliaddr);
  int connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
  char buffer[1024];
  int n = recv(connfd, buffer, sizeof(buffer), 0);

  // 将字节流解析为msg对象
  IM::Login::IMLoginReq msg;
  msg.ParseFromArray(buffer, n);

  // 打印Person对象
  cout << "Name: " << msg.user_name() << endl;
  cout << "password: " << msg.password() << endl;
	std::string client_version = msg.client_version();
	IM::BaseDefine::ClientType client_type = msg.client_type();
  // 关闭socket
  close(connfd);
  close(listenfd);

  return 0;
}

Nota: El compilador protoc también genera automáticamente las funciones msg.user_name y msg.password en los archivos .cc y .h

En el ejemplo, se crea un socket y se vincula al puerto, luego se recibe la conexión del cliente y se reciben los datos. Luego analice el flujo de bytes recibido en un objeto msg e imprima la información.

codificación protobuf

protobuf actualmente admite 6 tipos de codificación

Tipo Significado Usado para
0 Variedad int32, int64, uint32, uint64, sint32, sint64, bool, enumeración
1 64 bits fijo64, fijo64, doble
2 Longitud delimitada (división de longitud) cadena, bytes, mensajes incrustados, campos repetidos empaquetados
3 grupo de inicio grupos (en desuso)
4 Grupo final grupos (en desuso)
5 32 bits fijo32, fijo32, flotante

Entre ellos, la codificación Varint es la más utilizada, y puede ver la codificación Varint utilizada por datos como int, bool y enum. El grupo de inicio y el grupo final se han abandonado.

Codificación variante

La codificación Varint tiene las siguientes tres características:

  • El bit al comienzo de cada byte se establece con msb (bit más significativo) para identificar si continuar leyendo el siguiente byte
  • Almacena el complemento a dos del número correspondiente a
  • El bit bajo del código del complemento viene primero

Use un ejemplo para entender estas características:

Por ejemplo, almacenar un tipo de datos int32

int32 num = 1;

El valor binario de 1 es 0000 0000 0000 0001 (32 bits), por lo que se necesitan 2 bytes para almacenar este 1 en la memoria.

Y usar la codificación Varint para guardar este 1 es 0 000 0001, solo se necesitan 8 bytes. Esto incluye algunas reglas de codificación.

Usando otro ejemplo, guarde

int32 num = 500;

El binario de 500 es 0000 0001 1111 0100 (32 bits). Use la codificación Varint para guardar como el siguiente formulario

1 111 0100 0 000 0011. Parece irregular, pero en realidad es muy simple, así

Divida 0000 0001 1111 0100 en 0000011 y 1110100 según 7 bits de derecha a izquierda (elimine el 0 inicial). Luego, coloque el bit bajo 1110100 al frente y agregue un bit indicador 1 al frente, es decir, 11110100; coloque el bit alto atrás y agregue un bit indicador 0 al frente, es decir, 00000011. Poner los dos juntos es 1111010000000011. Esta es la regla de codificación implementada según las tres características de Varint.

Bit de bandera : indica si ha terminado (si se debe leer el siguiente byte), si es 1, significa que debe continuar leyendo el siguiente byte; si es 0, significa que este es el último byte, y no hay necesidad de leer o escribir otro byte.

protobuf utiliza el método de codificación de Varint para reducir los bytes serializados. La siguiente es una comparación de las pruebas de 100 000 serializaciones

inserte la descripción de la imagen aquí

Dado que Varint ocupa un bit de bandera, si un valor es 0xff ff ff ff, ¿cuántos bytes se necesitan para el almacenamiento?
Respuesta: 0xff ff ff ff necesita asignar 32 bits y la cantidad de bytes necesarios para usar la codificación Varints:
32/7=4,57, es decir, se requieren 5 bytes para el almacenamiento. Se puede ver a partir de esto que si el número entero >=28 bits no es adecuado para la codificación Varint de longitud variable, si el número entero es 32 bits>=variable>28 bits, puede considerar usar tipos fijos de 4 bytes como fixed32 y sfixed32.

En el uso diario, la mayoría de los datos serán de menos de 28 bits, por lo que la eficiencia de protobuf en escenarios reales sigue siendo muy alta.

¿Por qué el bit bajo del número del complemento está al frente?

Cuando llamemos a la serialización, eventualmente llamaremos a la función subyacente WriteVarint32ToArray, que es una característica de la codificación Varint.

inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
    
    
  // 0x80 -> 1000 0000
  // 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节
  // 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回
  // 如果至少需要两个字节
  while (value >= 0x80) {
    
    
    // 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位
    *target = static_cast<uint8>(value | 0x80);
    // 处理完七位,后移,继续处理下一个七位
    value >>= 7;
    // 指针加一,(数组后移一位)  相当于后移了8位 
    ++target;
  }
  // 跳出循环,则表示已无后续字节,但还有最后一个字节
  // 把最后一个字节放入数组
  *target = static_cast<uint8>(value);
  // 结束地址指向数组最后一个元素的末尾
  return target + 1;
}

Se puede ver en el código que comenzamos a procesar desde los 7 bits más bajos y procesamos a los bits altos a través de la instrucción de cambio hasta que los bits altos restantes son menos de 0 x 80. Desde la lógica del código, podemos ver la elegancia de esta codificación. forma.

Protobuf no puede reemplazar completamente a json, al igual que este ejemplo de inicio de sesión, si pasa json, solo necesita pasar el formato de los datos al servidor. Sin embargo, protobuf también necesita compilar archivos proto y protoc para compilar archivos .cc y .h, en comparación con este escenario, la operación es más complicada.

Para obtener más información, consulte https://www.jianshu.com/p/a24c88c0526a

Supongo que te gusta

Origin blog.csdn.net/weixin_44477424/article/details/131796087
Recomendado
Clasificación