Protocol Buffer - 初学者入门






4.89/5 (16投票s)
本文将介绍第三种数据序列化方案。这是 Google Protocol Buffer 的入门指南。让我们跳出 XML 和 JSON 吧。
Protocol buffer 是一种由 Google 开发的、与语言无关的二进制数据格式,用于在不同服务之间序列化结构化数据。如果你一开始没听懂这些术语,没关系。请允许我一步步讲解,直到一切都清晰明了。所以,本文我们将讨论:
- Protocol Buffer 到底是什么?
- 为什么选择 Protocol Buffer?
- 它们是如何工作的?
- 总体结构
- 演示
Protocol Buffer
要完全理解 protobuf 的概念,我们首先需要理解什么是序列化,以及我们面临的需要解决的问题。正如 维基百科 所解释的,序列化是:
“序列化是将数据结构或对象状态转换为可以存储(例如,在文件或内存缓冲区中)或传输(例如,通过网络连接)并在以后(可能在不同的计算机环境中)重新构造的格式的过程。”
简单来说,这意味着我们需要对数据进行序列化,以便存储或传输。但数据应该以什么格式进行序列化呢?以下是一些解决方法:
- 原始的内存数据结构可以以二进制形式发送或保存。但如果我想在不同的内存布局上检索这些数据怎么办?直接不行!我们不能那样做。代码必须以相同的内存布局和字节序等进行编译。而且,扩展这种格式非常困难。
- 将数据序列化为 XML(可扩展标记语言)或 JSON(JavaScript 对象表示法)。这种方法对前端来说很棒,因为它可读性强,但后端服务器之间的通信呢?这些格式占用空间很大。此外,编码和解码会给应用程序带来巨大的性能开销。
那么现在怎么办?是的,你当然可以继续发明自己的序列化技术。这会给你灵活性,但当你有大量数据时,这不是一个好主意。你需要特定的协议来处理巨大的流量。这正是 protobuf 出现的地方。它们就像 XML,但更 **灵活、高效、自动化、体积更小、速度更快、更简单**。你只需要定义一次数据结构,然后使用特殊的生成源代码来读取和写入各种数据流和语言中的结构化数据。
为什么选择 Protobuf?
我基本上已经为为什么我们应该考虑 Protocol Buffer 打下了基础,但如果这还不够有说服力,让我们多讨论一些,以使其使用更合理。
数据存储和传输通常涉及两种类型的考虑。
- 大小
- 效率
XML 和 JSON 被设计为人类可读且自描述的,这意味着它们是基于文本的。现在这有一个代价,因为你需要编码数据来传输消息,然后在另一端解码。因此,它增加了消息的大小,因为为了让消息有意义,需要将一些模式信息与消息一起包含。
另一方面,Protocol buffer 不具有自描述性,而是通过二进制序列化工作,这意味着它们将数据编码并压缩为二进制流,该流非常轻量且易于传输。据报道,与 XML 相比,它们的体积大约是 XML 的 1/3,是 JSON 的 1/2。较小的消息也需要更少的时间来传输,这有助于提高效率。据报道,Protobuf 的速度是 JSON 的 6 倍。
除了上述优点,还有一些其他的:
- 验证
- 易于扩展
- 保证类型安全
- 向后兼容
- 语言独立
- 更快的序列化/反序列化
为什么现在?
现在你可能想知道为什么以前没听说过它,如果听说了,为什么**现在**才讨论它?这是给你的额外福利。
是的!尽管它们已经存在了十年,但大多数人不知道它们,因为它们最初是由 Google“内部”使用的。而现在是因为“Pokémon Go”游戏。
它由 Niantic 开发,并使用 protobuf 进行数据传输。这款游戏的成功为 protobuf 的公开使用带来了热潮。
有什么缺点吗?
嗯,我不会真的称它们为缺点,但有些情况下 protobuf 可能不是那么有用。正如我们已经讨论过的,它们并不针对人类可读性。所以,如果你希望你的数据是人类可读的,那么 protobuf 就不是一个好的选择。如果你的浏览器直接消费来自服务的数据,那么选择其他序列化技术可能是更好的选择。此外,人们倾向于更频繁地使用 XML 或 JSON,因为它们拥有良好的社区支持,而 protobuf 在这方面有所欠缺。不要期望非常详细的文档,也不要期望很多针对使用 protobuf 进行开发的博客文章或文章。但是,这个项目是开源的,所以你当然可以大胆尝试。
Protobuf 如何工作?
你需要通过在 *.proto* 文件中定义 `message` 类型来指定数据结构以及你要序列化的服务。将此消息视为逻辑信息记录,其中你指定带有值的消息。然后,该代码会进入编译器,由 `protoc` 进行编译。使用预定的模式来编码和解码消息。
现在工作原理已经说通了,让我给你一个非常基本的 `.proto` 消息示例来详细说明结构。
message Movie {
required string title = 1;
required string genre = 2;
}
我们在这里所做的是在名为 `Movie` 的 `message` 中定义了上下文,它有两个字段:`title` 和 `genre`,标识符分别为 `1` 和 `2`。你可以将字段指定为 `optional`、`required` 和 `repeated`。请记住,这是实际执行的二进制的 `string` 表示。
演示
所以,我相信现在术语已经说得通了,话不多说,让我们动手实践。
环境设置
注意:虽然 Protobuf 支持几乎所有语言并面向所有主要平台,例如 Linux 和 Windows,但我将仅介绍 Windows 上的 C++ 安装。如果你有兴趣使用其他语言或平台,请访问 protobuf 文档 此处。
要在 Windows 上使用 MSVC 构建 protobuf,你需要以下工具:
- CMake
- Visual Studio
- Git (可选)
继续安装上述工具以便跟上。
安装好一切后,打开 Visual Studio 命令提示符,导航到你的工作目录。进入该目录后,执行以下命令:
$ mkdir install
这将仅创建一个文件夹,protobuf 构建后将安装在该文件夹中。在继续之前,请确保 `cmake` 和 `git` 已添加到系统的 `PATH` 变量中。如果未添加,可以通过执行以下命令将其添加到 `PATH`:
$ set PATH=%PATH%;C:\Program Files (x86)\CMake\bin
$ set PATH=%PATH%;C:\Program Files\Git\cmd
现在你可以将 `protobuf` 克隆到本地了。
$ git clone https://github.com/protocolbuffers/protobuf.git
如果你选择不使用 git,则可以直接从位于 https://github.com/protocolbuffers/protobuf/releases/latest 的 git 仓库下载包。
将仓库克隆到本地后,导航到项目文件夹 `protobuf`,然后进入 `cmake` 文件夹。
$ cd protobuf
$ cd cmake
现在我们需要配置 `CMake`。为此,请按照以下命令执行:
$ mkdir build & cd build
如果你使用 git clone,则需要更新任何 `submodule`。
$ git submodule update --init --recursive
Makefile 生成器一次只能构建一个配置,因此每个配置都需要一个单独的文件夹。
对于 Release 配置:
$ mkdir release & cd release
$ cmake -G "NMake Makefiles" ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_INSTALL_PREFIX=../../../../install ^
../..
对于 Debug 配置:
$ mkdir debug & cd debug
$ cmake -G "NMake Makefiles" ^
-DCMAKE_BUILD_TYPE=Debug ^
-DCMAKE_INSTALL_PREFIX=../../../../install ^
../..
以上任何命令都会在当前目录中生成 `nmake Makefile`。之后,你可以转到 Visual Studio。导航回 `build` 文件夹并执行以下命令。请记住指定你正在使用的 Visual Studio 版本。我使用的是 VS 2017 社区版。
$ mkdir solution & cd solution
$ cmake -G "Visual Studio 15 2017 Win64" ^
-DCMAKE_INSTALL_PREFIX=../../../../install ^
../..
现在该编译 `protobuf` 了。记住你之前指定的配置,并在此时选择相应的配置。导航到 `build`/`release` 文件夹并执行:
$ nmake
编译完成后,你可以运行单元测试:
$ nmake check
如果所有测试都通过,则进行安装:
$ nmake install
现在我们可以创建我们的项目了。创建一个新文件夹,你想要将你的项目保存在其中。为了使用 `protobuf`,你首先需要定义 `.proto` 文件。让我们以一个简单的 `Project` 管理系统为例。使用你选择的任何文本编辑器创建一个名为 `projectmanagement.proto` 的文件。请记住文件名扩展名为 `.proto`。
将以下内容添加到文件中:
//projectmanagement.proto
package projectmanagement;
message Developer
{
required string first_name = 1;
required string last_name = 2;
required string email = 3;
}
message Project
{
required string title= 1;
optional string url = 2;
repeated Developer developer= 3;
}
上面的代码指定了两个 `messages` 和字段。这现在应该很熟悉了,因为我们已经讨论过消息的结构。
你需要 `protoc` 编译器来编译上述代码。`release`/`debug` 文件夹包含在执行配置时生成的 `protoc.exe` 文件。现在,你可以将该文件添加到系统 `PATH`,或者将其复制到当前工作目录并执行以下命令来编译代码:
$ protoc --cpp_out=. projectmanagement.proto
命令成功执行后,你将看到生成了两个文件:
projectmanagement.pb.h
projectmanagement.pb.cc
让我们看一下头文件中的一些生成代码。如果你向下滚动,你会看到为你定义的访问器。
accessors -------------------------------------------------------
// required string first_name = 1;
bool has_first_name() const;
void clear_first_name();
static const int kFirstNameFieldNumber = 1;
const ::std::string& first_name() const;
void set_first_name(const ::std::string& value);
#if LANG_CXX11
void set_first_name(::std::string&& value);
#endif
void set_first_name(const char* value);
void set_first_name(const char* value, size_t size);
::std::string* mutable_first_name();
::std::string* release_first_name();
void set_allocated_first_name(::std::string* first_name);
// required string last_name = 2;
bool has_last_name() const;
void clear_last_name();
static const int kLastNameFieldNumber = 2;
const ::std::string& last_name() const;
void set_last_name(const ::std::string& value);
#if LANG_CXX11
void set_last_name(::std::string&& value);
#endif
void set_last_name(const char* value);
void set_last_name(const char* value, size_t size);
::std::string* mutable_last_name();
::std::string* release_last_name();
void set_allocated_last_name(::std::string* last_name);
// required string email = 3;
bool has_email() const;
void clear_email();
static const int kEmailFieldNumber = 3;
const ::std::string& email() const;
void set_email(const ::std::string& value);
#if LANG_CXX11
void set_email(::std::string&& value);
#endif
void set_email(const char* value);
void set_email(const char* value, size_t size);
::std::string* mutable_email();
::std::string* release_email();
void set_allocated_email(::std::string* email);
对于我们的第二个消息也是如此。这是 `protobuf` 的另一个伟大特性。你可以像平常一样轻松地使用这些设置器和获取器。下面是一个小程序,仅用于说明 `protobuf` 在访问器方面的便捷性。
//protobuf_sample.cc
#include <iostream>
#include <fstream>
#include "projectmanagement.pb.h"
using namespace std;
int main()
{
projectmanagement::Project project;
project.set_name("Sample");
project.set_url("http://www.sample.com");
projectmanagement::Developer *developer = project.add_developer();
developer->set_first_name("ABC");
developer->set_last_name("XYZ");
developer->set_email("someone@example.com");
cout << "Project: " << project.name() << endl;
cout << "URL: " << (company.has_url() ? company.url() : "N/A") << endl;
cout << "Developers: " endl;
cout << "First name: " << developer.first_name() << endl;
cout << "Last name: " << developer.last_name() << endl;
cout << "Email: " << developer.email() << endl;
return 0;
}
上面的代码是自解释的。我们只是通过设置器为字段分配一些值,然后获取输出。
// output:
// Project: Sample
// URL: http://www.sample.com
//
// developers:
//
// First name: ABC
// Last name: XYZ
// Email: someone@example.com
最后的话
这还不是 `protobuf` 的全部。你可以将上述数据编码为二进制,或者将数据从二进制转储回人类可读的文本格式,这非常棒。我强烈鼓励你进行实验,看看会发生什么。此外,`protobuf` 的文档还包含其他语言的教程。你可以在这里访问它。继续尝试一下。