深入 ProtoBuf - 简介Protobuf 使用指南

简单来讲, ProtoBuf 是结构数据序列化[1] 方法,可简单类比于 XML[2],其具有以下特点:

语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序
序列化[1]:将结构数据或对象转换成能够被存储和传输(例如网络传输)的格式,同时应当要保证这个序列化结果在之后(可能在另一个计算环境中)能够被重建回原来的结构数据或对象。
更为详尽的介绍可参阅 维基百科。
类比于 XML[2]:这里主要指在数据通信和数据存储应用场景中序列化方面的类比,但个人认为 XML 作为一种扩展标记语言和 ProtoBuf 还是有着本质区别的。

使用 ProtoBuf
对 ProtoBuf 的基本概念有了一定了解之后,我们来看看具体该如何使用 ProtoBuf。
第一步,创建 .proto 文件,定义数据结构,如下例1所示:

// 例1: 在 xxx.proto 文件中定义 Example1 message
message Example1 {
optional string stringVal = 1;
optional bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
optional EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}
我们在上例中定义了一个名为 Example1 的 消息,语法很简单,message 关键字后跟上消息名称:

message xxx {

}
之后我们在其中定义了 message 具有的字段,形式为:

message xxx {
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit …
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
字段规则 类型 名称 = 字段编号;
}
在上例中,我们定义了:

类型 string,名为 stringVal 的 optional 可选字段,字段编号为 1,此字段可出现 0 或 1 次
类型 bytes,名为 bytesVal 的 optional 可选字段,字段编号为 2,此字段可出现 0 或 1 次
类型 EmbeddedMessage(自定义的内嵌 message 类型),名为 embeddedExample1 的 optional 可选字段,字段编号为 3,此字段可出现 0 或 1 次
类型 int32,名为 repeatedInt32Val 的 repeated 可重复字段,字段编号为 4,此字段可出现 任意多次(包括 0)
类型 string,名为 repeatedStringVal 的 repeated 可重复字段,字段编号为 5,此字段可出现 任意多次(包括 0)
关于 proto2 定义 message 消息的更多语法细节,例如具有支持哪些类型,字段编号分配、import
导入定义,reserved 保留字段等知识请参阅 [翻译] ProtoBuf 官方文档(二)- 语法指引(proto2)。

关于定义时的一些规范请参阅 [翻译] ProtoBuf 官方文档(四)- 规范指引

第二步,protoc 编译 .proto 文件生成读写接口

我们在 .proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。

当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。那么如何实现呢?不用担心, ProtoBuf 将会为我们提供相应的接口代码。如何提供?答案就是通过 protoc 这个编译器。

可通过如下命令生成相应的接口代码:

// $SRC_DIR: .proto 所在的源目录
// --cpp_out: 生成 c++ 代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码

protoc -I= S R C D I R − − c p p o u t = SRC_DIR --cpp_out= SRCDIRcppout=DST_DIR $SRC_DIR/xxx.proto
最终生成的代码将提供类似如下的接口:

例子-序列化和解析接口.png
例子-protoc 生成接口.png
第三步,调用接口实现序列化、反序列化以及读写
针对第一步中例1定义的 message,我们可以调用第二步中生成的接口,实现测试代码如下:

//
// Created by yue on 18-7-21.
//
#include
#include
#include
#include “single_length_delimited_all.pb.h”

int main() {
Example1 example1;
example1.set_stringval(“hello,world”);
example1.set_bytesval(“are you ok?”);

Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();

embeddedExample2->set_int32val(1);
embeddedExample2->set_stringval("embeddedInfo");
example1.set_allocated_embeddedexample1(embeddedExample2);

example1.add_repeatedint32val(2);
example1.add_repeatedint32val(3);
example1.add_repeatedstringval("repeated1");
example1.add_repeatedstringval("repeated2");

std::string filename = "single_length_delimited_all_example1_val_result";
std::fstream output(filename, std::ios::out | std::ios::trunc | std::ios::binary);
if (!example1.SerializeToOstream(&output)) {
    std::cerr << "Failed to write example1." << std::endl;
    exit(-1);
}

return 0;

}
关于 protoc 的使用以及接口调用的更多信息可参阅 [翻译] ProtoBuf 官方文档(九)- (C++开发)教程

关于例1的完整代码请参阅 源码:protobuf 例1。其中的 single_length_delimited_all.* 为例子相关代码和文件。

因为此系列文章重点在于深入 ProtoBuf 的编码、序列化、反射等原理,关于 ProtoBuf 的语法、使用等只做简单介绍,更为详见的使用教程可参阅我翻译的系列官方文档。

作者:404_89_117_101
链接:https://www.jianshu.com/p/a24c88c0526a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
https://www.jianshu.com/p/a24c88c0526a

一、简介
最近在手撸 IM 系统,关于数据传输格式的选择,犹豫了下,对比了 JSON 和 XML,最后选择了 Protobuf 作为数据传输格式。

毕竟 Google 出品,必属精品😂,[官网地址]。
好了,舔狗环节结束,关于技术选择,都是需要根据实际的应用场景的,否则都是耍流氓,下文会进行简单的对比,先来看看官网的介绍:

他是一种与语言无关、与平台无关,是一种可扩展的用于序列化和结构化数据的方法,常用于用于通信协议,数据存储等。
他是一种灵活,高效,自动化的机制,用于序列化结构化数据,对比于 XML,他更小(310倍),更快(20100倍),更简单。

当然,最简单粗暴的理解方式,就是结合 JSON 和 XML 来理解,你可以暂时将他们仨理解成同一种类型的事物,但是呢,Protobuf 对比于他们两个,拥有着体量更小,解析速度更快的优势,所以,在 IM 这种通信应用中,非常适合将 Protobuf 作为数据传输格式。

二、关于 proto3
Protobuf 有两个大版本,proto2 和 proto3,同比 python 的 2.x 和 3.x 版本,如果是新接触的话,同样建议直接入手 proto3 版本。所以下文的描述都是基于 proto3 的。

proto3 相对 proto2 而言,简言之就是支持更多的语言(Ruby、C#等)、删除了一些复杂的语法和特性、引入了更多的约定等。

为什么要关注语言,因为它不像 JSON 一样开箱即用,它依赖工具包来进行编译成 java 文件或 go 文件等。

正如硬币的两面性一样,凡事皆有双面性,Protobuf 数据的体量更小,所以自然失去了人类的直接可读性, JSON 数据结构是可以很直观地阅读的,但是 Protobuf 我们需要借助工具来进行更友好地使用,所以,我们需要自定义一个 schema 来定义数据结构的描述,即下面的 message。

Message
举个很简单的栗子,摘自官网:

syntax = “proto3”; // proto3 必须加此注解

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
上面便是定义好的一个 message,里面包含:

String 类型的 query,编号是 1 (注:字段必须有编号且编号不允许重复)
int 类型的 page_number,编号是 2
枚举类型的 corpus (注:枚举内部的编号也不允许重复,并且第一个编号必须为0)
三、对比 JSON 和 XML
对比图
四、应用
此处以 Windows 为例,其他的都差不多。

windows 安装
protoc 下载:[官方下载地址],然后将 bin 路径添加到 path 环境变量下去
查看是否安装成功:控制台输入 protoc --version ,控制台输出版本信息代表成功,如: libprotoc 3.7.1
ideal 安装插件
ideal 插件库搜索安装 Protobuf Support 即可
此插件可以不用安装,但是这有助于一些源码阅读的便利性和一些编码提示
IDE 最大的作用不就是快速编码嘛

image
编写 proto 文件
定义一个 JetProtos.proto 文件
syntax = “proto3”; // PB协议版本

import “google/protobuf/any.proto”; // 引用外部的message,可以是本地的,也可以是此处比较特殊的 Any

package jet.protobuf; // 包名,其他 proto 在引用此 proto 的时候,就可以使用 test.protobuf.PersonTest 来使用,
// 注意:和下面的 java_package 是两种易混淆概念,同时定义的时候,java_package 具有较高的优先级

option java_package = “com.jet.protobuf”; // 生成类的包名,注意:会在指定路径下按照该包名的定义来生成文件夹
option java_outer_classname=“PersonTestProtos”; // 生成类的类名,注意:下划线的命名会在编译的时候被自动改为驼峰命名

message PersonTest {
int32 id = 1; // int 类型
string name = 2; // string 类型
string email = 3;
Sex sex = 4; // 枚举类型
repeated PhoneNumber phone = 5; // 引用下面定义的 PhoneNumber 类型的 message
map<string, string> tags = 6; // map 类型
repeated google.protobuf.Any details = 7; // 使用 google 的 any 类型

// 定义一个枚举  
enum Sex {      
    DEFAULT = 0;      
    MALE = 1;      
    Female = 2;  
}  

// 定义一个 message  
message PhoneNumber {    
    string number = 1;    
    PhoneType type = 2;    
    
    enum PhoneType {      
        MOBILE = 0;      
        HOME = 1;      
        WORK = 2;    
    }  
    
}

}
编译成 java 文件
进入 proto 文件所在路径,输入下面 protoc 命令(后面有三部分参数),然后将编译得出的 java 文件拷贝到项目中即可(此 java 文件可以理解成使用的数据对象):
protoc -I=./ --java_out=./ ./JetProtos.proto

protoc -proto_path=./ --java_out=./ ./JetProtos.proto
参数说明:

-I 等价于 -proto_path:指定 .proto 文件所在的路径
–java_out:编译成 java 文件时,标明输出目标路径
./JetProtos.proto:指定需要编译的 .proto 文件
使用
maven 引入指定包

com.google.protobuf protobuf-java 3.7.1 使用 序列化和反序列化有多种方式,可以是 byte[],也可以是 inputStream 等, package com.jet.mini.protobuf;

import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**

  • @ClassName: ProtoTest

  • @Description: ProtoBuf 测试

  • @Author: Jet.Chen

  • @Date: 2019/5/8 9:55

  • @Version: 1.0
    **/
    public class ProtoTest {

    public static void main(String[] args) {
    try {
    /** Step1:生成 personTest 对象 */
    // personTest 构造器
    PersonTestProtos.PersonTest.Builder personBuilder = PersonTestProtos.PersonTest.newBuilder();
    // personTest 赋值
    personBuilder.setName(“Jet Chen”);
    personBuilder.setEmail(“ckk505214992@gmail.com”);
    personBuilder.setSex(PersonTestProtos.PersonTest.Sex.MALE);

         // 内部的 PhoneNumber 构造器
         PersonTestProtos.PersonTest.PhoneNumber.Builder phoneNumberBuilder = PersonTestProtos.PersonTest.PhoneNumber.newBuilder();
         // PhoneNumber 赋值
         phoneNumberBuilder.setType(PersonTestProtos.PersonTest.PhoneNumber.PhoneType.MOBILE);
         phoneNumberBuilder.setNumber("17717037257");
    
         // personTest 设置 PhoneNumber
         personBuilder.addPhone(phoneNumberBuilder);
    
         // 生成 personTest 对象
         PersonTestProtos.PersonTest personTest = personBuilder.build();
    
         /** Step2:序列化和反序列化 */
         // 方式一 byte[]:
         // 序列化
    

// byte[] bytes = personTest.toByteArray();
// 反序列化
// PersonTestProtos.PersonTest personTestResult = PersonTestProtos.PersonTest.parseFrom(bytes);
// System.out.println(String.format(“反序列化得到的信息,姓名:%s,性别:%d,手机号:%s”, personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));

        // 方式二 ByteString:
        // 序列化

// ByteString byteString = personTest.toByteString();
// System.out.println(byteString.toString());
// 反序列化
// PersonTestProtos.PersonTest personTestResult = PersonTestProtos.PersonTest.parseFrom(byteString);
// System.out.println(String.format(“反序列化得到的信息,姓名:%s,性别:%d,手机号:%s”, personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));

        // 方式三 InputStream
        // 粘包,将一个或者多个protobuf 对象字节写入 stream
        // 序列化
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        personTest.writeDelimitedTo(byteArrayOutputStream);
        // 反序列化,从 steam 中读取一个或者多个 protobuf 字节对象
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
        PersonTestProtos.PersonTest personTestResult = PersonTestProtos.PersonTest.parseDelimitedFrom(byteArrayInputStream);
        System.out.println(String.format("反序列化得到的信息,姓名:%s,性别:%d,手机号:%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));

    } catch (InvalidProtocolBufferException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }

}

}
五、message 部分语法说明
在 proto3 中,枚举的第一个常量名的编号必须为 0
在 proto3 中,由于默认值的规则进行了调整,而枚举的默认值为第一个,所以必须将第一个常量的编号置为 0,但是这与我们的业务有时候是有冲突的,所以,我们常将第一个常量设为:xx_UNSPECIFIED = 0,如:ENUM_TYPE_UNSPECIFIED = 0;,当然这不是我们自己约定的,这是 Google API Guilder 中建议的。

同一个 proto 文件中,多个枚举之间不允许定义相同的常量名
如下面的 message 在编译的时候就会报错 IDEA is already defined in “xxx”:

enum IDE1 {
IDEA = 0;
ECLIPSE = 1;
}

enum IDE2 {
IDEA = 7;
ECLIPSE = 8;
}
关于数据类型匹配
见下图,摘自官网:

Protobuf 数据类型参考图
关于默认值
proto3 中,数据的默认值不再支持自定义,而是由程序自行推倒:

string:默认值为空
bytes:默认值为空
bools:默认值为 false
数字类型:默认值为 0
枚举类型: 默认为定义的第一个元素,并且编号必须为 0
message 类型:默认值为 DEFAULT_INSTANCE,其值相当于空的 message
六、总结
XML、JSON、Protobuf 都具有数据结构化和数据序列化的能力
XML、JSON 更注重 数据结构化,关注人类可读性和语义表达能力。Protobuf 更注重 数据序列化,关注效率、空间、速度,人类可读性差,语义表达能力不足
Protobuf 的应用场景更为明确,XML、JSON 的应用场景更为丰富

作者:goldenJetty
链接:https://www.jianshu.com/p/cae40f8faf1e
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
https://www.jianshu.com/p/cae40f8faf1e

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值