ProtoBuf原理概述与工程实践

1. protobuf概述

Protocol Buffers(也被称为protobuf)是Google开发的一种跨语言、跨平台、可扩展的用于序列化数据的机制,相比较XML和json格式而言,google protobuf更小、更快捷也更简单。你只需对想要的数据结构化定义一次,然后就可以用编译器(protoc)生成特定编程语言的代码来简单的实现对结构化数据的读写。当前protobuf支持多种语言:C++、Java、Python、C#、Go、Objective-C、JavaScript、Ruby、PHP、Dart。

1.1 protobuf工作原理

在protocol buffer消息类型文件.proto中,我们可以定义需要实现序列化的结构化数据信息。每一条protocol buffer消息都是一个小的逻辑记录,包含了一系列的name-value键值对。如下是一个基础的.proto文件示例,定义了一个person对象包含的一些信息:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

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

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

如上所示,每一条消息类型都有一个或多个编号唯一的field,并且每一个field都具有名称和类型。其中类型可以是:

  • numbers(integer or floating-point)
  • booleans
  • strings
  • raw bytes
  • protobuf message类型(这使得结构化数据可以具有层次性)

另外,你可以用optionalrequiredrepeated等关键字来修饰某个field。更多关于.proto文件的信息我们后面会详细介绍。

一旦定义好了上述消息类型,就可以使用protobuf编译器(protoc)生成对应语言的数据访问类(class),这些类会提供简单的方法来访问这些数据成员(如name()、set_name()),并且还会提供方法来将这种消息序列化成二进制数据,或者将二进制数据解析为消息类型。因此,假如你选择使用C++语言的话,对上述示例运行protoc编译器将会生成一个Person类,之后你就可以在应用程序中使用该类来分发、序列化以及获取Person消息。你可以编写类似如下的代码:

Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);

之后,你也可以将消息重新从文件中读回:

fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

你可以向上述消息格式中添加新的field,但仍可保持消息的向下兼容性: 当解析到原先老的二进制数据时,会将后面新添加的field直接忽略。因此,假如你采用protobuf作为数据通信协议的话,你就可以很好的扩展协议,而不必担心会破坏原有的代码。

1.2 为什么不采用XML?

在序列化结构性数据时,protobuf对比XML具有很多优势:

  • 更简单
  • 比XML所产生的数据量小3至10倍
  • 比XML快20到100倍
  • 较少产生歧义性
  • 所产生的数据访问类对象在编程中更容易使用

例如,假如我们想要表示一个具有name与email属性的Person对象,用XML的话需要如下:

<person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
  </person>

而当用protobuf消息来传递时格式如下(注: 这里是protobuf消息的文本格式表示):

# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
  name: "John Doe"
  email: "jdoe@example.com"
}

当这样一条消息被编码成protobuf二进制格式后,它也许只需要占用28字节并且只需花费100200纳秒来进行解析。如果采用XML格式至少需要69个字节(假如把空格之类的全移除),并且需要花费500010000纳秒来进行解析。

另外,当我们要操纵protobuf时,也更为简便:

 cout << "Name: " << person.name() << endl;
  cout << "E-mail: " << person.email() << endl;

而如果用XML的话,我们需要通过如下方式操作:

 cout << "Name: "
       << person.getElementsByTagName("name")->item(0)->innerText()
       << endl;
  cout << "E-mail: "
       << person.getElementsByTagName("email")->item(0)->innerText()
       << endl;

值得指出的是,protobuf并不是在所有方面都比XML好,比如protobuf并不适合用来处理一些文本标记语言(HTML等),因为如果用protobuf的话你并不能很好的将文本与相应的结构进行分离。另外,XML具有更好的(人类)可读性与编辑性,而二进制protobuf通常不适合人类阅读。

1.3 proto3简介

当前protobuf的最新版本是版本3(也被称为proto3),其提供了一些新的特性。proto3简化了protobuf语言,使其更容易使用,也使得其可被用于更多的编程语言:当前的proto3已经支持Java, C++, Python, Java Lite, Ruby, JavaScript, Objective-C, and C#。另外,你也可以使用go protoc编译器为Go语言生成proto3代码,请参看golang/protobuf

值得注意的是,protobuf的这两个版本(proto2、proto3)的API并不完全兼容。为了避免对现有用户造成不便,在新发布的protobuf版本中仍加持续支持proto2。

后续相关章节我们会更为详细的介绍proto3。

1.4 protobuf的一点历史

Google最先开发protobuf的时候主要是为了处理索引服务器(index server)请求与响应。在开发protobuf之前,我们可能通常需要编写类似如下的代码来处理对应的请求与响应:

if (version == 3) {
   ...
 } else if (version > 4) {
   if (version == 5) {
     ...
   }
   ...
 }

同样如上面这种明确格式化的协议也使得要发布新的协议版本变得十分复杂,因为开发者必须要确定发出请求(request)的服务器与实际处理请求的服务器必须要双方都能理解新的协议版本。

protobuf就是被设计用来解决很多有关这方面问题的:

  • 可以很容易的引入新的field,并且中间的服务器并不需要被另行通知即可解析所有的字段;
  • protobuf数据格式具有很好的自我描述性,并且可以被更多的编程语言所处理

然而,假如仅仅只是这样的话,用户仍必须手写代码来解析相应的数据。因此,protobuf还提供了如下一些特性:

  • 可以自动化产生序列化与反序列化代码,以避免手写代码来解析
  • 为了支持短生命周期的RPC请求,在数据持久化存储方面人们也开始使用protobuf来作为一种方便的数据自我描述格式(如bigtable)
  • 服务器RPC接口也作为protobuf文件的一部分被声明,然后通过protoc编译器产生相应的stub类,之后用户可以重写相应的方法来实现服务器接口

1.5 为什么是protbuf?

大家可能会觉得 Google 发明 ProtoBuf 是为了解决序列化速度的,其实真实的原因并不是这样的。

ProtoBuf最先开始是 Google用来解决索引服务器 request/response 协议的。没有ProtoBuf之前,Google 已经存在了一种 request/response 格式,用于手动处理 request/response 的编解码。它也能支持多版本协议,不过代码不够优雅:

if (protocolVersion=1) {
    doSomething();
} else if (protocolVersion=2) {
    doOtherThing();
} ...

如果是非常明确的格式化协议,会使新协议变得非常复杂。因为开发人员必须确保请求发起者与处理请求的实际服务器之间的所有服务器都能理解新协议,然后才能切换开关以开始使用新协议。

这也就是每个服务器开发人员都遇到过的低版本兼容、新旧协议兼容相关的问题。

为了解决这些问题,于是ProtoBuf就诞生了。

ProtoBuf 最初被寄予以下 2 个特点:

  • 更容易引入新的字段,并且不需要检查数据的中间服务器可以简单地解析并传递数据,而无需了解所有字段。
  • 数据格式更加具有自我描述性,可以用各种语言来处理(C++, Java 等各种语言)。

这个版本的 ProtoBuf 仍需要自己手写解析的代码。

不过随着系统慢慢发展,演进,ProtoBuf具有了更多的特性:

  • 自动生成的序列化和反序列化代码避免了手动解析的需要。(官方提供自动生成代码工具,各个语言平台的基本都有)。
  • 除了用于数据交换之外,ProtoBuf被用作持久化数据的便捷自描述格式。

ProtoBuf 现在是 Google 用于数据交换和存储的通用语言。谷歌代码树中定义了 48162 种不同的消息类型,包括 12183 个 .proto 文件。它们既用于 RPC 系统,也用于在各种存储系统中持久存储数据。

ProtoBuf 诞生之初是为了解决服务器端新旧协议(高低版本)兼容性问题,名字也很体贴,“协议缓冲区”。只不过后期慢慢发展成用于传输数据。

Protocol Buffers 命名由来:

Why the name “Protocol Buffers”?

The name originates from the early days of the format, before we had the protocol buffer compiler to generate classes for us. At the time, there was a class called ProtocolBuffer which actually acted as a buffer for an individual method. Users would add tag/value pairs to this buffer individually by calling methods like AddValue(tag, value). The raw bytes were stored in a buffer which could then be written out once the message had been constructed.

Since that time, the “buffers” part of the name has lost its meaning, but it is still the name we use. Today, people usually use the term “protocol message” to refer to a message in an abstract sense, “protocol buffer” to refer to a serialized copy of a message, and “protocol message object” to refer to an in-memory object representing the parsed message.

2. protobuf的安装使用

1)准备必要安装环境

在Centos下通过源代码安装protobuf,通常要求系统已经安装有如下一些工具:

  • autoconf
  • automake
  • libtool
  • make
  • g++
  • unzip

我们可以执行如下命令来检查相关的工具是否安装:

# yum list installed | grep autoconf
Repodata is over 2 weeks old. Install yum-cron? Or run: yum makecache fast
autoconf.noarch                        2.69-11.el7                     @base 

或者直接运行相应的命令,看是否可以执行:

# autoconf -v
autoconf: error: no input file
  1. 下载安装包并解压
# mkdir protobuf-inst
# cd protobuf-inst
# wget https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protobuf-all-3.8.0.tar.gz
# ls
protobuf-all-3.8.0.tar.gz
# tar -zxvf protobuf-all-3.8.0.tar.gz 
# cd protobuf-3.8.0
  1. 编译并安装protobuf
# ./configure --prefix=/usr/local/protobuf
# make
# make check
PASS: protobuf-test
PASS: protobuf-lazy-descriptor-test
PASS: protobuf-lite-test
PASS: google/protobuf/compiler/zip_output_unittest.sh
PASS: google/protobuf/io/gzip_stream_unittest.sh
PASS: protobuf-lite-arena-test
PASS: no-warning-test
============================================================================
Testsuite summary for Protocol Buffers 3.8.0
============================================================================
# TOTAL: 7
# PASS:  7
# SKIP:  0
# XFAIL: 0
# FAIL:  0
# XPASS: 0
# ERROR: 0
============================================================================

# make install
# ls /usr/local/protobuf/
bin  include  lib

# pkg-config --cflags protobuf
-pthread -I/usr/local/protobuf/include  
# pkg-config --libs protobuf
-L/usr/local/protobuf/lib -lprotobuf 

至此,安装已经完成。

如果我们想以后使用方便,可以导出如下环境变量到/etc/profile中(记得执行source /etc/profile):

export PATH=$PATH:/usr/local/protobuf/bin/
export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/

另外如果想要我们以后的程序在运行时可以直接找到protobuf相关的动态库,也可以在protobuf库路径添加到*/etc/ld.so.conf*中。这里我们可以如下操作:

# echo "/usr/local/protobuf/lib" >> /etc/ld.so.conf.d/protobuf-x86_64.conf
# ls /etc/ld.so.conf.d
dyninst-x86_64.conf  kernel-3.10.0-514.el7.x86_64.conf  libiscsi-x86_64.conf  mysql-x86_64.conf  protobuf-x86_64.conf

注: 这里我们为了较为详细的演示protobuf的相关编译、运行步骤,暂时都未设置这些环境变量

3. protobuf程序示例(C++版)

该程序的大致功能是: 定义一个Person结构体,以及存放Person信息的AddressBook,然后一个写程序向一个文件写入该结构体信息,另一个程序从文件中读出该信息并打印到输出中.

  1. 编写address.proto文件

这里我们采用proto2语法格式:

syntax = "proto2";
package tutorial;

message Person {
    required string name = 1;
    required int32 age = 2;
}

message AddressBook {
    repeated Person person = 1;
}

执行如下命令对address.proto文件进行编译:

# /usr/local/protobuf/bin/protoc --cpp_out=./ ./address.proto
# ls
address.pb.cc  address.pb.h  address.proto

之后我们可以大体看一下生成的这两个文件,其结构大体如下:

//address.pb.h
namespace tutorial{
class Person{
};

class AddressBook{
};

}

其会为Person以及AddressBook生成很多方法。

2) 编写addressbook_write.cpp,向文件中写入AddressBook信息

这里我们编写addressbook_write.cpp,然后以二进制方式向文件中写入AddressBook信息:

#include <iostream>
#include <fstream>
#include <string>
#include "address.pb.h"


void PromptForAddress(tutorial::Person *person){

        std::string name;
        int age;

        std::cout<<"Enter person name: ";
        std::cin >> name;
        person->set_name(name);

        std::cout<<"Enter person age: ";
        std::cin >> age;
        person->set_age(age);
}

int main(int arc, char *argv[])
{
        if (arc != 2){
                std::cerr << "Usage: addressbook_write <bookfile>" <<  std::endl;
                return -1;
        }

        tutorial::AddressBook address_book;

        PromptForAddress(address_book.add_person());

        //write to file
        {
                std::fstream output(argv[1], std::ios::out | std::ios::trunc | std::ios::binary);
                if (!address_book.SerializeToOstream(&output)){
                        std::cerr << "Failed to write address book." << std::endl;
                        return -1;
                }
        }

        // Optional: Delete all global objects allocated by libprotobuf.
        //google::protobuf::ShutdownProtobufLibrary();

        return 0x0;
}

执行如下命令进行编译(注: 这里需要加上-std=c++11):

# gcc -o addressbook_write address.pb.cc addressbook_write.cpp -std=c++11 -I/usr/local/protobuf/include \
-L/usr/local/protobuf/lib -lprotobuf -lpthread -lstdc++

或者
# export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/
# g++ -o addressbook_write address.pb.cc addressbook_write.cpp -std=c++11 `pkg-config --cflags --libs protobuf` 
# ls
addressbook_write  addressbook_write.cpp  address.pb.cc  address.pb.h  address.proto

这里建议采用上面第2种方法编译,比较不容易出错,否则后续可能遇到各种奇怪的问题。编译成功之后运行:

# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib
# ./addressbook_write addressbook.bin
Enter person name: ivanzz1001
Enter person age: 18
# ls
addressbook.bin  addressbook_write  addressbook_write.cpp  address.pb.cc  address.pb.h  address.proto

3) 编写addressbook_read.cpp,从文件中读入AddressBook信息

这里我们编写addressbook_read.cpp,然后从二进制方式向文件中读入AddressBook信息:

#include <iostream>
#include <fstream>
#include <string>
#include "address.pb.h"


void ListPerson(const tutorial::AddressBook& address_book){
	for(int i = 0; i < address_book.person_size(); i++){
		const tutorial::Person &person = address_book.person(i);
		
		std::cout<<person.name() << "  " << person.age() << std::endl;
	}
}


int main(int argc, char *argv[])
{
        //GOOGLE_PROTOBUF_VERIFY_VERSION;

        if (argc != 2){
                std::cerr << "Usage: addressbook_write <bookfile>" << std::endl;
                return -1;
        }

        tutorial::AddressBook address_book;

        //read from file
        {
                std::fstream input(argv[1], std::ios::in | std::ios::binary);
                if (!address_book.ParseFromIstream(&input)){
                        std::cerr << "Failed to parse addressbook." << std::endl;
                        return -1;
                }
        }

        ListPerson(address_book);


    // Optional: Delete all global objects allocated by libprotobuf.
    //google::protobuf::ShutdownProtobufLibrary();

        return 0;
}

执行如下命令进行编译(注: 这里需要加上-std=c++11):

# gcc -o addressbook_read address.pb.cc addressbook_read.cpp -std=c++11 -I/usr/local/protobuf/include \
-L/usr/local/protobuf/lib -lprotobuf -lpthread -lstdc++

或者
# export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/
# g++ -o addressbook_read address.pb.cc addressbook_read.cpp -std=c++11 `pkg-config --cflags --libs protobuf` 
# ls
addressbook.bin  addressbook_read  addressbook_read.cpp  addressbook_write  addressbook_write.cpp  address.pb.cc  address.pb.h  address.proto

这里建议采用上面第2种方法编译,比较不容易出错,否则后续可能遇到各种奇怪的问题。编译成功之后运行:

# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib
# ./addressbook_read addressbook.bin
ivanzz1001  18

这里我们可以看到已经成功的读取出了前面写入的信息。

4. 总结

优点:

1. 效率高

从序列化后的数据体积角度,与XML、JSON这类文本协议相比,ProtoBuf通过T-(L)-V(TAG-LENGTH-VALUE)方式编码,不需要", {, }, :等分隔符来结构化信息,同时在编码层面使用varint压缩,所以描述同样的信息,ProtoBuf序列化后的体积要小很多,在网络中传输消耗的网络流量更少,进而对于网络资源紧张、性能要求非常高的场景,ProtoBuf协议是不错的选择。

// 我们简单做个对比
// 要描述如下JSON数据
{"id":1,"firstName":"Chris","lastName":"Richardson","email":[{"type":"PROFESSIONAL","email":"crichardson@email.com"}]}
# 使用JSON序列化后的数据大小为118byte
7b226964223a312c2266697273744e616d65223a224368726973222c226c6173744e616d65223a2252696368617264736f6e222c22656d61696c223a5b7b22747970<br>65223a2250524f46455353494f4e414c222c22656d61696c223a226372696368617264736f6e40656d61696c2e636f6d227d5d7d
# 而使用ProtoBuf序列化后的数据大小为48byte
0801120543687269731a0a52696368617264736f6e2a190a156372696368617264736f6e40656d61696c2e636f6d1001

从序列化/反序列化速度角度,与XML、JSON相比,ProtoBuf序列化/反序列化的速度更快,比XML要快20-100倍。

2. 支持跨平台、多语言

ProtoBuf是平台无关的,无论是Android与PC,还是C#与Java都可以利用ProtoBuf进行无障碍通讯。

proto3支持C++, Java, Python, Go, Ruby, Objective-C, C#。

3. 扩展性、兼容性好

具有向后兼容的特性,更新数据结构以后,老版本依旧可以兼容,这也是ProtoBuf诞生之初被寄予解决的问题。因为编译器对不识别的新增字段会跳过不处理。

4. 使用简单

ProtoBuf 提供了一套编译工具,可以自动生成序列化、反序列化的样板代码,这样开发者只要关注业务数据idl,简化了编码解码工作以及多语言交互的复杂度。

缺点:

可读性差,缺乏自描述

XML,JSON是自描述的,而ProtoBuf则不是。

ProtoBuf是二进制协议,编码后的数据可读性差,如果没有idl文件,就无法理解二进制数据流,对调试不友好。

不过Charles已经支持ProtoBuf协议,导入数据的描述文件即可,详情可参考Charles Protocol Buffers

此外,由于没有idl文件无法解析二进制数据流,ProtoBuf在一定程度上可以保护数据,提升核心数据被破解的门槛,降低核心数据被盗爬的风险。

  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值