protobuf原理以及实例(Varint编码)

简介: protobuf原理以及实例(Varint编码)

protobuf定义

  • protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
  • protobuf(Protocol Buffers)是一款序列化编码框架,经常在一些RPC(远程调用)协议中出现。但其实protobuf可以理解成是一款序列化协议,和json、xml一样,使用该框架,需要在自己的结构上构建的数据。
  • 而且protobuf序列化的体积比xml、json小得多,所以protobuf经常在一些网络框架中使用。

protobuf特点:

  • ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台。并且客户端和服务端还可以使用不同的语言编写。
  • 使用protobuf序列化数据传输速度快,比XML数据快数10倍。主要得益于他的编码方式。
  • 扩展性、兼容性好。更新数据结构,不影响和破坏原有的旧程序。

protobuf实例

举一个登录信息网络传输的示例,熟悉一下如何使用protobuf将数据传输给服务端。

登录信息包括5个数据:

  • user_name
  • password
  • online_status
  • client_type
  • client_version
  1. 编写proto文件如下,将传输的数据结构放在message 字段。message 是一个关键字,后面跟上自定义的消息名称,如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;
}

注意:每个字段的编号,需要按照顺序从1开始规则定义,最小编号是 1,最大的是 2^29 -1即536,870,911,其中 19000 到 19999不能使用(内定为Protocol Buffers使用)。

  1. 编写完proto文件后,通过protoc进行编译
protoc --cpp_out=. login.proto

此处编译结果会生成两个文件,分别是login.pb.h和login.pb.cc的文件。生成的文件中有一些接口之后需要使用到。需要将生成的文件复制到客户端和服务端各一份供接口调用。

3.客户端设置

#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;
}

注意:set_user_name、set_password这些函数接口是通过protoc自动生成在.cc和.h文件中。

其中msg.SerializeToString(&buffer);用于序列化msg数据。这里序列化数据后通过通信协议传输可以节省传输的带宽。

4.服务端设置

在服务端将接收到客户端发送的字节流,解析为msg对象。

#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;
}

注意:msg.user_name、msg.password这些函数也是通过protoc编译器自动生成在.cc和.h文件中

示例中创建了一个socket并绑定到端口,然后接收客户端连接并接收数据。然后将接收到的字节流解析为msg对象,并打印出信息。

protobuf编码

protobuf目前支持6种编码类型

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited(长度分割) string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

其中Varint 编码最常用,可以看到int,bool,enum这类数据都使用的Varint 编码。Start group和End group 已经放弃使用了。

Varint 编码

Varint 编码有以下3个特点:

  • 在每个字节开头的 bit 设置了 msb(most significant bit),标识是否需要继续读取下一个字节
  • 存储数字对应的二进制补码
  • 补码的低位排在前面

用一个例子来理解这些特点:

比如存储一个int32类型的数据

int32 num = 1;

1的二进制为0000 0000 0000 0001(32位),因此在内存中保存这个1需要消耗2个字节。

而使用Varint 编码保存这个1,则是0000 0001,只需要8个字节。这其中包含了一些编码规则。

再用一个例子,保存

int32 num = 500;

500的二进制0000 0001 1111 0100(32位)。使用Varint 编码保存为以下形式

1111 0100 0000 0011。看起来没有规律,但其实很简单,如下

将0000 0001 1111 0100 从右到左按照7位分成0000011和1110100(把最前面的0去掉)。然后把低位1110100放在前面,并且最前面增加一个标志位1,即11110100;高位放在后面,并且最前面增加一个标志位0,即00000011。把两个拼在一起即1111010000000011。这就是按照Varint 三个特点实现的编码规则。

标志位:表示是否已经结束(是否还需要读取下一个字节),为1则表示还需要继续读下一个字节;为0表示这就是最后一个字节,不需要再读写一个字节了。

protobuf就是通过Varint 这样的编码方式,来减少序列化后的字节,下面是测试10万次序列化的对比

Varint 由于标志位占用了一位,那如果一个值为0xff ff ff ff那需要多少个字节存储?

答:0xff ff ff ff需要分配32个bit,使用Varints 编码需要的字节数:

32/7=4.57, 就是需要5个字节存储。 从这里看得出来,如果>=28bit的整数不适合使用变长Varint 编码,如果整数都是32bit>= 变量 >28bit可以考虑使用fixed32, sfixed32等固定4字节的类型。

在日常使用情况下,大部分的数据都会小于28bit的,所以说实际场景protobuf的效率仍然很高。

为什么补码的低位排在前面

我们调用序列化时,最终会调用底层的WriteVarint32ToArray 函数,这是是 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;
}

从代码中可以看出来我们是从最低的7位开始处理的,通过移位指令一直处理到高位,直到剩余高位小于0x80,从代码逻辑可以看出这样的编码形式的优雅。

protobuf不能完全代替json,就像这个登录的例子一样,通过json的话只需要把数据的格式传给服务端就好了。而protobuf还需要将proto文件,还需要protoc编译出.cc、.h文件;相对这种场景下操作更复杂了。


目录
相关文章
|
4月前
|
存储 安全 数据安全/隐私保护
Base64编码的优缺点
Base64 是一种常用于将二进制数据编码为 ASCII 字符串的编码方法。下面是 Base64 编码的一些优点和缺点: ### 优点: 1. **可读性**:Base64 编码后的数据是纯文本形式的,这使得它可以轻松地在各种文本传输协议和环境中传输,而不会被误解或破坏。 2. **平台无关性**:Base64 编码是基于 ASCII 字符集的,这使得它在几乎所有的操作系统和编程语言中都是可用的。 3. **数据完整性**:Base64 可以确保数据的完整性,因为它是基于 ASCII 字符进行传输的,不会受到二进制数据在不同系统中解释方式的影响。 4. **简单易用**:许多编程语言
|
5月前
|
存储 XML JSON
日常小知识点之序列化结构(protobuf使用及简单原理)
日常小知识点之序列化结构(protobuf使用及简单原理)
71 0
|
9月前
|
XML 存储 JSON
数据序列化工具 Protobuf 编码&避坑指南
我们现在所有的协议、配置、数据库的表达都是以 protobuf 来进行承载的,所以我想深入总结一下 protobuf 这个协议,以免踩坑。 先简单介绍一下 Protocol Buffers(protobuf),它是 Google 开发的一种数据序列化协议(与 XML、JSON 类似)。它具有很多优点,但也有一些需要注意的缺点: 优点: 效率高:Protobuf 以二进制格式存储数据,比如 XML 和 JSON 等文本格式更紧凑,也更快。序列化和反序列化的速度也很快。 跨语言支持:Protobuf 支持多种编程语言,包括 C++、Java、Python 等。 清晰的结构定义:使用 prot
|
9月前
|
数据采集 算法 安全
Base64 编码原理 && 实现
Base64 编码原理 && 实现
|
编解码
一文搞懂Base64编解码
一文搞懂Base64编解码
206 0
|
编解码 JSON 安全
IM通讯协议专题学习(四):从Base64到Protobuf,详解Protobuf的数据编码原理
本篇将从Base64再到Base128编码,带你一起从底层来理解Protobuf的数据编码原理。 本文结构总体与 Protobuf 官方文档相似,不少内容也来自官方文档,并在官方文档的基础上添加作者理解的内容(确保不那么枯燥),如有出入请以官方文档为准。
331 0
IM通讯协议专题学习(四):从Base64到Protobuf,详解Protobuf的数据编码原理
|
Dubbo 算法 安全
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(二)
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(二)
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(二)
|
SQL 存储 Java
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(一)
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(一)
Java序列化案例demo(包含Kryo、JDK原生、Protobuf、ProtoStuff以及hessian)(一)
|
编解码 Go 区块链
Go语言实现Base64、Base58编码与解码
目录 1. 常见的编码 2. go语言实现base64的编码与解码 2.1 Base64编码原理 2.2 实现Base64的编码与解码 3. go语言实现base58编码与解码 3.1 base58的编码过程 3.2 base58编解码具体实现 4. 最后
340 0
Go语言实现Base64、Base58编码与解码
|
算法 Android开发 索引
一文详解 Base64编码原理
Base64 是一种 编码方式,最早出现在电子邮件传输协议中。 电子邮件问世之初,传递消息时只支持 ASCII 字符,后来随着电子邮件的广泛使用,传递非ASCII字符内容的需求增加,例如:传输中文、传输文件(图片、视频)。
123 0
一文详解 Base64编码原理