Protobuf 使用指南

一、簡(jiǎn)介

最近在手?jǐn)] IM 系統(tǒng),關(guān)于數(shù)據(jù)傳輸格式的選擇,猶豫了下,對(duì)比了 JSON 和 XML,最后選擇了 Protobuf 作為數(shù)據(jù)傳輸格式。

畢竟 Google 出品,必屬精品??,[官網(wǎng)地址]。
好了,舔狗環(huán)節(jié)結(jié)束,關(guān)于技術(shù)選擇,都是需要根據(jù)實(shí)際的應(yīng)用場(chǎng)景的,否則都是耍流氓,下文會(huì)進(jìn)行簡(jiǎn)單的對(duì)比,先來(lái)看看官網(wǎng)的介紹:

他是一種與語(yǔ)言無(wú)關(guān)、與平臺(tái)無(wú)關(guān),是一種可擴(kuò)展的用于序列化和結(jié)構(gòu)化數(shù)據(jù)的方法,常用于用于通信協(xié)議,數(shù)據(jù)存儲(chǔ)等。
他是一種靈活,高效,自動(dòng)化的機(jī)制,用于序列化結(jié)構(gòu)化數(shù)據(jù),對(duì)比于 XML,他更?。?10倍),更快(20100倍),更簡(jiǎn)單。

當(dāng)然,最簡(jiǎn)單粗暴的理解方式,就是結(jié)合 JSON 和 XML 來(lái)理解,你可以暫時(shí)將他們仨理解成同一種類型的事物,但是呢,Protobuf 對(duì)比于他們兩個(gè),擁有著體量更小,解析速度更快的優(yōu)勢(shì),所以,在 IM 這種通信應(yīng)用中,非常適合將 Protobuf 作為數(shù)據(jù)傳輸格式。

二、關(guān)于 proto3

Protobuf 有兩個(gè)大版本,proto2 和 proto3,同比 python 的 2.x 和 3.x 版本,如果是新接觸的話,同樣建議直接入手 proto3 版本。所以下文的描述都是基于 proto3 的。

proto3 相對(duì) proto2 而言,簡(jiǎn)言之就是支持更多的語(yǔ)言(Ruby、C#等)、刪除了一些復(fù)雜的語(yǔ)法和特性、引入了更多的約定等。

為什么要關(guān)注語(yǔ)言,因?yàn)樗幌?JSON 一樣開箱即用,它依賴工具包來(lái)進(jìn)行編譯成 java 文件或 go 文件等。

正如硬幣的兩面性一樣,凡事皆有雙面性,Protobuf 數(shù)據(jù)的體量更小,所以自然失去了人類的直接可讀性, JSON 數(shù)據(jù)結(jié)構(gòu)是可以很直觀地閱讀的,但是 Protobuf 我們需要借助工具來(lái)進(jìn)行更友好地使用,所以,我們需要自定義一個(gè) schema 來(lái)定義數(shù)據(jù)結(jié)構(gòu)的描述,即下面的 message。

  • Message

舉個(gè)很簡(jiǎn)單的栗子,摘自官網(wǎng):

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;
}

上面便是定義好的一個(gè) message,里面包含:

  1. String 類型的 query,編號(hào)是 1 (注:字段必須有編號(hào)且編號(hào)不允許重復(fù))
  2. int 類型的 page_number,編號(hào)是 2
  3. 枚舉類型的 corpus (注:枚舉內(nèi)部的編號(hào)也不允許重復(fù),并且第一個(gè)編號(hào)必須為0)

三、對(duì)比 JSON 和 XML

對(duì)比圖

四、應(yīng)用

此處以 Windows 為例,其他的都差不多。

  1. windows 安裝
  • protoc 下載:[官方下載地址],然后將 bin 路徑添加到 path 環(huán)境變量下去
  • 查看是否安裝成功:控制臺(tái)輸入 protoc --version ,控制臺(tái)輸出版本信息代表成功,如: libprotoc 3.7.1
  1. ideal 安裝插件
  • ideal 插件庫(kù)搜索安裝 Protobuf Support 即可
  • 此插件可以不用安裝,但是這有助于一些源碼閱讀的便利性和一些編碼提示

IDE 最大的作用不就是快速編碼嘛

image
  1. 編寫 proto 文件
    定義一個(gè) JetProtos.proto 文件
syntax = "proto3"; // PB協(xié)議版本

import "google/protobuf/any.proto"; // 引用外部的message,可以是本地的,也可以是此處比較特殊的 Any

package jet.protobuf; // 包名,其他 proto 在引用此 proto 的時(shí)候,就可以使用 test.protobuf.PersonTest 來(lái)使用,
// 注意:和下面的 java_package 是兩種易混淆概念,同時(shí)定義的時(shí)候,java_package 具有較高的優(yōu)先級(jí)

option java_package = "com.jet.protobuf"; // 生成類的包名,注意:會(huì)在指定路徑下按照該包名的定義來(lái)生成文件夾
option java_outer_classname="PersonTestProtos"; // 生成類的類名,注意:下劃線的命名會(huì)在編譯的時(shí)候被自動(dòng)改為駝峰命名

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 類型  

    // 定義一個(gè)枚舉  
    enum Sex {      
        DEFAULT = 0;      
        MALE = 1;      
        Female = 2;  
    }  
    
    // 定義一個(gè) message  
    message PhoneNumber {    
        string number = 1;    
        PhoneType type = 2;    
        
        enum PhoneType {      
            MOBILE = 0;      
            HOME = 1;      
            WORK = 2;    
        }  
        
    }
    
}
  1. 編譯成 java 文件
    進(jìn)入 proto 文件所在路徑,輸入下面 protoc 命令(后面有三部分參數(shù)),然后將編譯得出的 java 文件拷貝到項(xiàng)目中即可(此 java 文件可以理解成使用的數(shù)據(jù)對(duì)象):
protoc -I=./ --java_out=./ ./JetProtos.proto
或
protoc -proto_path=./ --java_out=./ ./JetProtos.proto

參數(shù)說(shuō)明:

  1. -I 等價(jià)于 -proto_path:指定 .proto 文件所在的路徑
  2. --java_out:編譯成 java 文件時(shí),標(biāo)明輸出目標(biāo)路徑
  3. ./JetProtos.proto:指定需要編譯的 .proto 文件
  1. 使用
  • maven 引入指定包
<!-- protobuf -->
<dependency>     
    <groupId>com.google.protobuf</groupId>     
    <artifactId>protobuf-java</artifactId>     
    <version>3.7.1</version>
</dependency>
  • 使用
    序列化和反序列化有多種方式,可以是 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;

/**
 * @ClassName: ProtoTest
 * @Description: ProtoBuf 測(cè)試
 * @Author: Jet.Chen
 * @Date: 2019/5/8 9:55
 * @Version: 1.0
 **/
public class ProtoTest {

    public static void main(String[] args) {
        try {
            /** Step1:生成 personTest 對(duì)象 */
            // personTest 構(gòu)造器
            PersonTestProtos.PersonTest.Builder personBuilder = PersonTestProtos.PersonTest.newBuilder();
            // personTest 賦值
            personBuilder.setName("Jet Chen");
            personBuilder.setEmail("ckk505214992@gmail.com");
            personBuilder.setSex(PersonTestProtos.PersonTest.Sex.MALE);

            // 內(nèi)部的 PhoneNumber 構(gòu)造器
            PersonTestProtos.PersonTest.PhoneNumber.Builder phoneNumberBuilder = PersonTestProtos.PersonTest.PhoneNumber.newBuilder();
            // PhoneNumber 賦值
            phoneNumberBuilder.setType(PersonTestProtos.PersonTest.PhoneNumber.PhoneType.MOBILE);
            phoneNumberBuilder.setNumber("17717037257");

            // personTest 設(shè)置 PhoneNumber
            personBuilder.addPhone(phoneNumberBuilder);

            // 生成 personTest 對(duì)象
            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,手機(jī)號(hào):%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,手機(jī)號(hào):%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));



            // 方式三 InputStream
            // 粘包,將一個(gè)或者多個(gè)protobuf 對(duì)象字節(jié)寫入 stream
            // 序列化
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            personTest.writeDelimitedTo(byteArrayOutputStream);
            // 反序列化,從 steam 中讀取一個(gè)或者多個(gè) protobuf 字節(jié)對(duì)象
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            PersonTestProtos.PersonTest personTestResult = PersonTestProtos.PersonTest.parseDelimitedFrom(byteArrayInputStream);
            System.out.println(String.format("反序列化得到的信息,姓名:%s,性別:%d,手機(jī)號(hào):%s", personTestResult.getName(), personTest.getSexValue(), personTest.getPhone(0).getNumber()));

        } catch (InvalidProtocolBufferException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

五、message 部分語(yǔ)法說(shuō)明

  1. 在 proto3 中,枚舉的第一個(gè)常量名的編號(hào)必須為 0
    在 proto3 中,由于默認(rèn)值的規(guī)則進(jìn)行了調(diào)整,而枚舉的默認(rèn)值為第一個(gè),所以必須將第一個(gè)常量的編號(hào)置為 0,但是這與我們的業(yè)務(wù)有時(shí)候是有沖突的,所以,我們常將第一個(gè)常量設(shè)為:xx_UNSPECIFIED = 0,如:ENUM_TYPE_UNSPECIFIED = 0;,當(dāng)然這不是我們自己約定的,這是 Google API Guilder 中建議的。

  2. 同一個(gè) proto 文件中,多個(gè)枚舉之間不允許定義相同的常量名
    如下面的 message 在編譯的時(shí)候就會(huì)報(bào)錯(cuò) IDEA is already defined in "xxx"

enum IDE1 {
    IDEA = 0;
    ECLIPSE = 1;
}

enum IDE2 {
    IDEA = 7;
    ECLIPSE = 8;
}
  1. 關(guān)于數(shù)據(jù)類型匹配
    見下圖,摘自官網(wǎng):


    Protobuf 數(shù)據(jù)類型參考圖
  2. 關(guān)于默認(rèn)值
    proto3 中,數(shù)據(jù)的默認(rèn)值不再支持自定義,而是由程序自行推倒:

  • string:默認(rèn)值為空
  • bytes:默認(rèn)值為空
  • bools:默認(rèn)值為 false
  • 數(shù)字類型:默認(rèn)值為 0
  • 枚舉類型: 默認(rèn)為定義的第一個(gè)元素,并且編號(hào)必須為 0
  • message 類型:默認(rèn)值為 DEFAULT_INSTANCE,其值相當(dāng)于空的 message

六、總結(jié)

  1. XML、JSON、Protobuf 都具有數(shù)據(jù)結(jié)構(gòu)化和數(shù)據(jù)序列化的能力
  2. XML、JSON 更注重 數(shù)據(jù)結(jié)構(gòu)化,關(guān)注人類可讀性和語(yǔ)義表達(dá)能力。Protobuf 更注重 數(shù)據(jù)序列化,關(guān)注效率、空間、速度,人類可讀性差,語(yǔ)義表達(dá)能力不足
  3. Protobuf 的應(yīng)用場(chǎng)景更為明確,XML、JSON 的應(yīng)用場(chǎng)景更為豐富

七、其它

  1. 文檔:官網(wǎng)
  2. 上文使用的案例源碼:[源碼]
  3. 當(dāng)然了,除了 Google 的 Protobuf,還有 Facebook 的 thrift,也值得研究一下哦,暫不進(jìn)行贅述
image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容