背景
Protocol Buffers,是Google公司开发的一种数据描述语言,也是一种语言中立,平台无关,可扩展的序列化数据的格式 ,类似于XML、json能够将结构化数据序列化,可用于数据存储、通信协议等方面。
为什么发明protobuf
大家可能会觉得 Google 发明 protocol buffers 是为了解决序列化速度的,其实真实的原因并不是这样的。
protocol buffers 最先开始是 google 用来解决索引服务器 request/response 协议的。没有 protocol buffers 之前,google 已经存在了一种 request/response 格式,用于手动处理 request/response 的编组和反编组。它也能支持多版本协议,不过代码比较丑陋:
if (version == 3) {
...
} else if (version > 4) {
if (version == 5) {
...
}
...
}
如果非常明确的格式化协议,会使新协议变得非常复杂。因为开发人员必须确保请求发起者与处理请求的实际服务器之间的所有服务器都能理解新协议,然后才能切换开关以开始使用新协议。
这也就是每个服务器开发人员都遇到过的低版本兼容、新旧协议兼容相关的问题。
protocol buffers 为了解决这些问题,于是就诞生了。protocol buffers 被寄予一下 2 个特点:
- 可以很容易地引入新的字段,并且不需要检查数据的中间服务器可以简单地解析并传递数据,而无需了解所有字段。
- 数据格式更加具有自我描述性,可以用各种语言来处理(C++, Java 等各种语言)
这个版本的 protocol buffers 仍需要自己手写解析的代码。
不过随着系统慢慢发展,演进,protocol buffers 目前具有了更多的特性:
- 自动生成的序列化和反序列化代码(也类似QT中的控件生成代码)避免了手动解析的需要。(官方提供自动生成代码工具,各个语言平台的基本都有)
- 除了用于 RPC(远程过程调用)请求之外,人们开始将 protocol buffers 用作持久存储数据的便捷自描述格式(例如,在Bigtable中)。
- 服务器的 RPC 接口可以先声明为协议的一部分,然后用 protocol compiler 生成基类,用户可以使用服务器接口的实际实现来覆盖它们。
protobuf编译安装
地址链接 (cpp版本)我目前的最新版本是3.19.1
tar zxvf protobuf-cpp-3.19.1.tar.gz
cd protobuf-3.19.1/
./configure
make (这步时间有点长)
make install
ldconfig
protoc --version
将proto文件生成对应的.cc 和 .h 文件
addressbook.proto 文件
// See README.txt for information and build instructions.
//
// Note: START and END tags are used in comments to define sections used in
// tutorials. They are not part of the syntax for Protocol Buffers.
//
// To get an in-depth walkthrough of this file and the related examples, see:
// https://developers.google.com/protocol-buffers/docs/tutorials
// [START declaration]
syntax = "proto3"; //proto2
package tutorial; // package 类似C++命令空间
// 可以引用本地,也可以引用include里面的
import "google/protobuf/timestamp.proto"; // 已经写好的proto文件是可以引用
// [END declaration]
// [START java_declaration]
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
// [END java_declaration]
// [START csharp_declaration]
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
// [END csharp_declaration]
// [START messages]
message Person { // message 类似C++ 的class
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType { // 枚举类型
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1; // 字符串 电话号码
PhoneType type = 2;
}
repeated PhoneNumber phones = 4; // 重复多个,一个人有多个电话
google.protobuf.Timestamp last_updated = 5; // google/protobuf/timestamp.proto
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1; // 电话薄有多人电话
}
// [END messages]
生成对应的.cc 和 .h文件
protoc -I=/路径1 --cpp_out=./路径2 /路径1/addressbook.proto
- 路径1为.proto所在的路径
- 路径2为.cc和.h⽣成的位置
实例:
protoc -I=./ --cpp_out=./ addressbook.proto (将指定文件)
protoc -I=./ --cpp_out=./ *.proto
添加数据(上面是编写文件格式)
add_person.cc (添加一个person)
// See README.txt for information and build instructions.
#include <ctime>
#include <fstream>
#include <google/protobuf/util/time_util.h>
#include <iostream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
using google::protobuf::util::TimeUtil;
// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person* person) {
cout << "Enter person ID number: ";
int id;
cin >> id;
person->set_id(id);
cin.ignore(256, '\n');
cout << "Enter name: ";
getline(cin, *person->mutable_name());
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if (!email.empty()) {
person->set_email(email);
}
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
string number;
getline(cin, number);
if (number.empty()) {
break;
}
tutorial::Person::PhoneNumber* phone_number = person->add_phones();
phone_number->set_number(number);
cout << "Is this a mobile, home, or work phone? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
// 设置时间
*person->mutable_last_updated() = TimeUtil::SecondsToTimestamp(time(NULL));
}
// 正确方式 g++ -std=c++11 -o add_person add_person.cc addressbook.pb.cc -lprotobuf -std=c++11 -lpthread
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Read the existing address book. 本地文件保存电话本的数据
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file." << endl;
} else if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
// Add an address. 新建一个对象
PromptForAddress(address_book.add_people());
{
// Write the new address book back to disk. 保存电话本
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
g++ -std=c++11 -o add_person add_person.cc addressbook.pb.cc -lprotobuf -std=c++11 -lpthread (生成执行程序)
./add_person book (执行)
读取数据
list_people.cc (读取一个people)
// See README.txt for information and build instructions.
#include <fstream>
#include <google/protobuf/util/time_util.h>
#include <iostream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
using google::protobuf::util::TimeUtil;
// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
for (int i = 0; i < address_book.people_size(); i++) {
const tutorial::Person& person = address_book.people(i);
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if (person.email() != "") {
cout << " E-mail address: " << person.email() << endl;
}
for (int j = 0; j < person.phones_size(); j++) {
const tutorial::Person::PhoneNumber& phone_number = person.phones(j);
switch (phone_number.type()) {
case tutorial::Person::MOBILE:
cout << " Mobile phone #: ";
break;
case tutorial::Person::HOME:
cout << " Home phone #: ";
break;
case tutorial::Person::WORK:
cout << " Work phone #: ";
break;
default:
cout << " Unknown phone #: ";
break;
}
cout << phone_number.number() << endl;
}
if (person.has_last_updated()) {
cout << " Updated: " << TimeUtil::ToString(person.last_updated()) << endl;
}
}
}
// protoc -I=./ --cpp_out=./ *.proto
// 错误方式 g++ -o list_people list_people.cc addressbook.pb.cc -lpthread -lprotobuf -std=c++11
// 正确方式 g++ -o list_people list_people.cc addressbook.pb.cc -lprotobuf -std=c++11 -lpthread
// Main function: Reads the entire address book from a file and prints all
// the information inside.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
ListPeople(address_book);
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
g++ -o list_people list_people.cc addressbook.pb.cc -lprotobuf -std=c++11 -lpthread
./list_people book
标量数值类型
⼀个标量消息字段可以含有⼀个如下的类型——该表格展示了定义于.proto⽂件中的类型,以及与之对应 的、在⾃动⽣成的访问类中定义的类型:
注: 上面的红色标注的地方后面还涉及到编码方式的不同,后面有讲
protobuf 协议的工作流程
IDL是Interface description language的缩写,指接⼝描述语⾔。
可以看到,对于序列化协议来说,使⽤⽅只需要关注业务对象本身,即 idl 定义(.proto),序列化和反序 列化的代码只需要通过⼯具⽣成即可(上面有写)。
protobuf 使用实例(IM项目)
- pb协议设计
- 登陆
- 发送消息
- 代码验证
protobuf 工程经验
- proto文件命名规则
- proto 命名空间
- 引用文件
- 多个平台使用同一份proto文件
扩展(协议设计)
首先要明白协议设计这个用在哪里。
在网络传输中一般要用将包分界,一般的做法有以下几种
- 以固定大小自己数目传输,如没包100字节,就是每收齐100字节,就当成一个包来解析;(现在几乎不用,不够灵活)
- 以特定符号来分界:如每个包都以特定的字符来结尾(如\r\n),当在字节流中读取到该字符时,则表示上一个包到此为止。(例如http的包头)
- 固定包头+ 包体结构:这种结构中⼀般包头部分是⼀个固定字节⻓度的结构,并且包头中会有⼀个特定 的字段指定包体的⼤⼩。收包时,先接收固定字节数的头部,解出这个包完整⻓度,按此⻓度接收包体。这是⽬前各种⽹络应⽤⽤的最多的⼀种包格式;header + body
- 在序列化后的buffer前⾯增加⼀个字符流的头部,其中有个字段存储包总⻓度,根据特殊字符(⽐如根 据\n或者\0)判断头部的完整性。这样通常⽐3要麻烦⼀些,HTTP和REDIS采⽤的是这种⽅式。收包的时候,先判断已收到的数据中是否包含结束符,收到结束符后解析包头,解出这个包完整⻓度,按此⻓度接收包体。
IM即时通讯
云平台节点服务器
nginx协议
HTTP协议
注:HTTP协议是我们最常⻅的协议,我们是否可以采⽤HTTP协议作为互联⽹后台的协议呢?
这个⼀般是不适当的,主要是考虑到以下2个原因:
- HTTP协议只是⼀个框架,没有指定包体的序列化⽅式,所以还需要配合其他序列化的⽅式使⽤才能传递业务逻辑数据。
- HTTP协议解析效率低,⽽且⽐较复杂(不知道有没有⼈觉得HTTP协议简单,其实不是http协议简单, ⽽是HTTP⼤家⽐较熟悉⽽已)
有些情况下是可以使⽤HTTP协议的:
- 对公⽹⽤户api,HTTP协议的穿透性最好,所以最适合;
- 效率要求没那么⾼的场景;
- 希望提供更多⼈熟悉的接⼝,⽐如新浪微、腾讯博提供的开放接⼝;
有一个面试题是这样的:http 的body 是二进制还是文本?
答案是:有文本如html,有二进制,如图片信息,可根据type来判断,http里面有很多格式,可以根据不同格式来判断不同的数据。
protobuf 的编码原理
的协议呢?
这个⼀般是不适当的,主要是考虑到以下2个原因:
- HTTP协议只是⼀个框架,没有指定包体的序列化⽅式,所以还需要配合其他序列化的⽅式使⽤才能传递业务逻辑数据。
- HTTP协议解析效率低,⽽且⽐较复杂(不知道有没有⼈觉得HTTP协议简单,其实不是http协议简单, ⽽是HTTP⼤家⽐较熟悉⽽已)
有些情况下是可以使⽤HTTP协议的:
- 对公⽹⽤户api,HTTP协议的穿透性最好,所以最适合;
- 效率要求没那么⾼的场景;
- 希望提供更多⼈熟悉的接⼝,⽐如新浪微、腾讯博提供的开放接⼝;
有一个面试题是这样的:http 的body 是二进制还是文本?
答案是:有文本如html,有二进制,如图片信息,可根据type来判断,http里面有很多格式,可以根据不同格式来判断不同的数据。
protobuf 的编码原理
下一章介绍