簡單來講, 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= SRCDIR−−cppout=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;
public class ProtoTest {
public static void main(String[] args) {
try {
// personTest 構造器
PersonTestProtos.PersonTest.Builder personBuilder = PersonTestProtos.PersonTest.newBuilder();
// personTest 指派
personBuilder.setName(“Jet Chen”);
personBuilder.setEmail(“[email protected]”);
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