天天看點

深入 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= SRCD​IR−−cppo​ut=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

繼續閱讀