Protocol Buffers簡(jiǎn)明教程

隨著微服務(wù)架構(gòu)的流行,RPC框架漸漸地成為服務(wù)框架的一個(gè)重要部分世杀。在很多RPC的設(shè)計(jì)中阀参,都采用了高性能的編解碼技術(shù),Protocol Buffers就屬于其中的佼佼者瞻坝。Protocol Buffers是Google開源的一個(gè)語(yǔ)言無(wú)關(guān)蛛壳、平臺(tái)無(wú)關(guān)的通信協(xié)議,其小巧所刀、高效和友好的兼容性設(shè)計(jì)衙荐,使其被廣泛使用。

概述

protobuf是什么浮创?

Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

  • Google良心企業(yè)出廠的忧吟;
  • 是一種序列化對(duì)象框架(或者說(shuō)是編解碼框架),其他功能相似的有Java自帶的序列化斩披、Facebook的Thrift和JBoss Marshalling等溜族;
  • 通過(guò)proto文件定義結(jié)構(gòu)化數(shù)據(jù),其他功能相似的比如XML垦沉、JSON等煌抒;
  • 自帶代碼生成器,支持多種語(yǔ)言厕倍;

為什么叫“Protocol Buffers”寡壮?

官方如是說(shuō):

The name originates from the early days of the format, before we had the protocol buffer compiler to generate classes for us. At the time, there was a class called ProtocolBuffer which actually acted as a buffer for an individual method. Users would add tag/value pairs to this buffer individually by calling methods like AddValue(tag, value). The raw bytes were stored in a buffer which could then be written out once the message had been constructed.

Since that time, the "buffers" part of the name has lost its meaning, but it is still the name we use. Today, people usually use the term "protocol message" to refer to a message in an abstract sense, "protocol buffer" to refer to a serialized copy of a message, and "protocol message object" to refer to an in-memory object representing the parsed message.

核心特點(diǎn)

  • 語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)
  • 簡(jiǎn)潔
  • 高性能
  • 良好的兼容性

“變態(tài)的”性能表現(xiàn)

有位網(wǎng)友曾經(jīng)做過(guò)各種通用序列化協(xié)議技術(shù)的對(duì)比,我這里直接拿來(lái)給大家感受一下:

序列化響應(yīng)時(shí)間對(duì)比

序列化響應(yīng)時(shí)間對(duì)比

序列化bytes對(duì)比

序列化bytes對(duì)比

具體的數(shù)字

具體的數(shù)字

快速開始

以下示例源碼已上傳至github:https://github.com/ginobefun/learning_projects/tree/master/learning-protobuf

新建一個(gè)maven項(xiàng)目并添加依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ginobefunny.learning</groupId>
    <artifactId>leanring-protobuf</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.2.0</version>
        </dependency>
    </dependencies>
</project>

新建protobuf的消息定義文件addressbook.proto

syntax = "proto3"; // 聲明為protobuf 3定義文件
package tutorial;

option java_package = "com.ginobefunny.learning.protobuf.message"; // 聲明生成消息類的java包路徑
option java_outer_classname = "AddressBookProtos";  // 聲明生成消息類的類名

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

使用protoc工具生成消息對(duì)應(yīng)的Java類

  • 已發(fā)布版本中下載protoc工具况既,比如protoc-3.2.0-win32屋群;
  • 解壓后將bin目錄添加到path路徑;
  • 執(zhí)行以下protoc命令生成Java類:
protoc -I=. --java_out=src/main/java addressbook.proto

編寫測(cè)試類寫入和讀取序列化文件

  • AddPerson類通過(guò)用戶每次添加一個(gè)聯(lián)系人坏挠,并序列化保存到指定文件中芍躏。
public class AddPerson {

    // 通過(guò)用戶輸入構(gòu)建一個(gè)Person對(duì)象
    static AddressBookProtos.Person promptForAddress(BufferedReader stdin,
                                                     PrintStream stdout) throws IOException {
        AddressBookProtos.Person.Builder person = AddressBookProtos.Person.newBuilder();

        stdout.print("Enter person ID: ");
        person.setId(Integer.valueOf(stdin.readLine()));

        stdout.print("Enter name: ");
        person.setName(stdin.readLine());

        stdout.print("Enter email address (blank for none): ");
        String email = stdin.readLine();
        if (email.length() > 0) {
            person.setEmail(email);
        }

        while (true) {
            stdout.print("Enter a phone number (or leave blank to finish): ");
            String number = stdin.readLine();
            if (number.length() == 0) {
                break;
            }

            AddressBookProtos.Person.PhoneNumber.Builder phoneNumber =
                    AddressBookProtos.Person.PhoneNumber.newBuilder().setNumber(number);

            stdout.print("Is this a mobile, home, or work phone? ");
            String type = stdin.readLine();
            if (type.equals("mobile")) {
                phoneNumber.setType(AddressBookProtos.Person.PhoneType.MOBILE);
            } else if (type.equals("home")) {
                phoneNumber.setType(AddressBookProtos.Person.PhoneType.HOME);
            } else if (type.equals("work")) {
                phoneNumber.setType(AddressBookProtos.Person.PhoneType.WORK);
            } else {
                stdout.println("Unknown phone type.  Using default.");
            }

            person.addPhones(phoneNumber);
        }

        return person.build();
    }

    // 加載指定的序列化文件(如不存在則創(chuàng)建一個(gè)新的),再通過(guò)用戶輸入增加一個(gè)新的聯(lián)系人到地址簿降狠,最后序列化到文件中
    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");
            System.exit(-1);
        }

        AddressBookProtos.AddressBook.Builder addressBook = AddressBookProtos.AddressBook.newBuilder();

        // Read the existing address book.
        try {
            addressBook.mergeFrom(new FileInputStream(args[0]));
        } catch (FileNotFoundException e) {
            System.out.println(args[0] + ": File not found.  Creating a new file.");
        }

        // Add an address.
        addressBook.addPeople(promptForAddress(new BufferedReader(new InputStreamReader(System.in)),
                        System.out));

        // Write the new address book back to disk.
        FileOutputStream output = new FileOutputStream(args[0]);
        addressBook.build().writeTo(output);
        output.close();
    }
}
  • ListPeople類讀取序列化文件并輸出所有聯(lián)系人信息对竣。
public class ListPeople {

    // 打印地址簿中所有聯(lián)系人信息
    static void print(AddressBookProtos.AddressBook addressBook) {
        for (AddressBookProtos.Person person: addressBook.getPeopleList()) {
            System.out.println("Person ID: " + person.getId());
            System.out.println("  Name: " + person.getName());
            if (!person.getPhonesList().isEmpty()) {
                System.out.println("  E-mail address: " + person.getEmail());
            }

            for (AddressBookProtos.Person.PhoneNumber phoneNumber : person.getPhonesList()) {
                switch (phoneNumber.getType()) {
                    case MOBILE:
                        System.out.print("  Mobile phone #: ");
                        break;
                    case HOME:
                        System.out.print("  Home phone #: ");
                        break;
                    case WORK:
                        System.out.print("  Work phone #: ");
                        break;
                }
                System.out.println(phoneNumber.getNumber());
            }
        }
    }

    // 加載指定的序列化文件,并輸出所有聯(lián)系人信息
    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");
            System.exit(-1);
        }

        // Read the existing address book.
        AddressBookProtos.AddressBook addressBook =
                AddressBookProtos.AddressBook.parseFrom(new FileInputStream(args[0]));

        print(addressBook);
    }
}

驗(yàn)證效果

先添加一個(gè)聯(lián)系人Gino

添加一個(gè)聯(lián)系人Gino

再添加一個(gè)聯(lián)系人Slightly

添加一個(gè)聯(lián)系人Gino

最后顯示所有聯(lián)系人信息

添加一個(gè)聯(lián)系人Gino

實(shí)例小結(jié)

  • 通過(guò)以上的例子我們能大概感受到開發(fā)protobuf序列化的大致步驟:定義proto文件榜配、生成對(duì)應(yīng)的Java類文件否纬、通過(guò)消息類的構(gòu)造器構(gòu)造對(duì)象并通過(guò)writeTo序列化、通過(guò)parseFrom反序列化對(duì)象蛋褥;
  • 如果查看中間序列化的文件临燃,我們可以發(fā)現(xiàn)protobuf序列化的二進(jìn)制文件非常緊湊,因此文件更小烙心,傳輸性能更好膜廊。

深入學(xué)習(xí)

關(guān)于proto文件

protobuf版本

  • protobuf現(xiàn)在主流的有2.X和3.X版本,兩者之間相差比較大淫茵,對(duì)于剛采用的建議使用3.X版本爪瓜;
  • 如果采用3.X版本,需要再proto文件第一個(gè)非注釋行聲明(就像我們上面的例子那樣)匙瘪,因?yàn)閜rotobuf默認(rèn)認(rèn)為是2.X版本铆铆;

message結(jié)構(gòu)

  • 在一個(gè)proto文件中可以包含多個(gè)message定義,message之間可以互相引用丹喻,message還可以嵌套message和枚舉類薄货;
  • 一個(gè)message通常包含一至多個(gè)字段;
  • 每個(gè)字段包含以下幾個(gè)部分:字段描述符(可選)碍论、字段類型谅猾、字段名稱和字段對(duì)應(yīng)的Tag;

字段描述符

字段描述符用于描述字段出現(xiàn)的頻率骑冗,有以下兩個(gè)可選值:

  • singular:表示出現(xiàn)0次或1次赊瞬;如果沒有聲明描述符先煎,默認(rèn)為singular贼涩;
  • repeated:表示出現(xiàn)0次或多次;

字段類型

  • 基本數(shù)據(jù)類型:包括double薯蝎、float遥倦、bool、string、bytes袒哥、int32缩筛、int64、uint32堡称、uint64瞎抛、sint32、sint64却紧、fixed32桐臊、fixed64、sfixed32晓殊、sfixed64断凶;
  • 引用其他message類型:這個(gè)就有點(diǎn)像我們Java里面的對(duì)象引用的方式;
  • 枚舉類型:對(duì)于枚舉類型巫俺,protobuf有個(gè)約束:枚舉的第一項(xiàng)對(duì)應(yīng)的值必須為0认烁;下面是一個(gè)包含枚舉類型的消息定義:
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;
}

字段對(duì)應(yīng)的Tag

  • 對(duì)應(yīng)同一個(gè)message里面的字段,每個(gè)字段的Tag是必須唯一數(shù)字介汹;
  • Tag主要用于說(shuō)明字段在二進(jìn)制文件的對(duì)應(yīng)關(guān)系却嗡,一旦指定字段為對(duì)應(yīng)的Tag,不應(yīng)該在后續(xù)進(jìn)行變更嘹承;
  • 對(duì)于Tag的分配稽穆,115只用一個(gè)byte進(jìn)行編碼(因此應(yīng)該留給那些常用的字段),162047用兩個(gè)byte進(jìn)行編碼赶撰,最大支持到536870911舌镶,但是中間有一段(19000~19999)是protobuf內(nèi)部使用的;
  • 可以通過(guò)reserved關(guān)鍵字來(lái)預(yù)留Tag和字段名豪娜,還有一種場(chǎng)景是如果某個(gè)字段已經(jīng)被廢棄了不希望后續(xù)被采用餐胀,也可以用reserved關(guān)鍵字聲明;

字段的默認(rèn)值

protobuf 2.X版本是支持在字段中聲明默認(rèn)值的瘤载,但是在3.X版本中去掉了默認(rèn)值的定義否灾,主要是為了區(qū)別用戶是否設(shè)置了一個(gè)和默認(rèn)值一樣的值的情況。對(duì)于3.X版本鸣奔,protobuf采用以下規(guī)則處理默認(rèn)值:

  • 對(duì)應(yīng)string類型墨技,默認(rèn)值為一個(gè)空字符串;
  • 對(duì)于bytes類型挎狸,默認(rèn)值為一個(gè)空的byte數(shù)組扣汪;
  • 對(duì)于bool類型,默認(rèn)值為false锨匆;
  • 對(duì)于數(shù)值類型崭别,默認(rèn)值為0;
  • 對(duì)于枚舉類型,默認(rèn)值為第一項(xiàng)茅主,也即值為0的那個(gè)枚舉值舞痰;
  • 對(duì)于引用其他message類型:其默認(rèn)值和對(duì)應(yīng)的語(yǔ)言是相關(guān)的;

Map字段類型

  • protobuf也支持定義Map類型的字段诀姚,但是對(duì)于Map的key的類型只能是整數(shù)型(包括各種int32和int64)和string類型响牛;
  • Map類型不能定義為repeated;
  • Map類型的數(shù)據(jù)是無(wú)序的赫段;
  • 以下是一個(gè)Map類型的字段定義示例:
map<string, Project> projects = 3;

導(dǎo)入其他proto文件

  • 可以通過(guò)import關(guān)鍵字導(dǎo)入其他proto文件娃善,從而重用message類型;下面是一個(gè)import的示例:
import "myproject/other_protos.proto";

如果proto中的message要擴(kuò)展怎么辦瑞佩?

proto具有很好的擴(kuò)展性聚磺,但是也要遵循以下原則:

  • 不能修改原有字段的Tag;
  • 如果新增一個(gè)字段炬丸,對(duì)于老的二進(jìn)制序列化文件處理時(shí)會(huì)給這個(gè)字段增加默認(rèn)值瘫寝;如果是升級(jí)了proto文件而沒有升級(jí)對(duì)應(yīng)的代碼,則新的字段會(huì)被忽略稠炬;
  • 可以刪除字段焕阿,但是對(duì)應(yīng)的Tag不應(yīng)該再被使用,否則對(duì)于之前的二進(jìn)制序列化消息處理時(shí)對(duì)應(yīng)關(guān)系出現(xiàn)問(wèn)題首启;
  • int32暮屡、uint32、int64毅桃、uint64和bool類型是相互兼容的褒纲,這意味著你可以在他們之間修改類型而不會(huì)有兼容性問(wèn)題;

Any消息類型

  • protobuf內(nèi)置了一些通用的消息類型钥飞,Any就是其他的一種莺掠,通過(guò)查看它的proto文件可以看到它包含了一個(gè)URL標(biāo)識(shí)符和一個(gè)byte數(shù)組;
  • 在使用Any消息類型之前读宙,需要通過(guò)import "google/protobuf/any.proto";導(dǎo)入proto文件定義彻秆;

Oneof關(guān)鍵字

  • oneof關(guān)鍵字用于聲明一組字段中,必須要有一個(gè)字段被賦值结闸;通常比如我們?cè)诘顷懙臅r(shí)候,可以用手機(jī)號(hào)桦锄、郵箱和用戶名登陸扎附,這種時(shí)候就可以使用oneof來(lái)定義察纯;
  • 當(dāng)我們對(duì)oneof其中一個(gè)字段賦值時(shí)即纲,其他字段的值將會(huì)被清空膊畴;所以只有最后一次賦值是有效的;
  • 下面是一個(gè)oneof的示例:
message LoginMessage {
  oneof user_identifier {
    string user_name = 4;
    string phone_num = 5;
    string user_email = 6;
  }
  
  string password = 10;
}

定義服務(wù)

  • 在proto文件中還允許定義RPC服務(wù)病游,以下是一個(gè)示例:
service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

小結(jié)

  • 隨著微服務(wù)架構(gòu)的流行唇跨,RPC框架漸漸地成為服務(wù)框架的一個(gè)重要部分。在很多RPC的設(shè)計(jì)中衬衬,都采用了高性能的編解碼技術(shù)买猖,protobuf就屬于其中的佼佼者;
  • protobuf相對(duì)于其他編解碼框架滋尉,有著非常驚人的性能表現(xiàn)玉控;
  • 通過(guò)一個(gè)簡(jiǎn)單的實(shí)例,我們了解如果使用protobuf進(jìn)行序列化和數(shù)據(jù)交互狮惜;
  • 最后高诺,我們列舉了一些重要的特性和配置說(shuō)明,這些在我們使用protobuf中都會(huì)給頻繁使用碾篡;
  • 后續(xù)學(xué)習(xí):后面我會(huì)根據(jù)所學(xué)的Netty和protobuf知識(shí)懒叛,開發(fā)一個(gè)簡(jiǎn)單的RPC框架。

參考資料

掃一掃 關(guān)注我的微信公眾號(hào)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末耽梅,一起剝皮案震驚了整個(gè)濱河市薛窥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌眼姐,老刑警劉巖诅迷,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異众旗,居然都是意外死亡罢杉,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門滩租,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)赋秀,“玉大人,你說(shuō)我怎么就攤上這事律想×粤” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵技即,是天一觀的道長(zhǎng)著洼。 經(jīng)常有香客問(wèn)我,道長(zhǎng)而叼,這世上最難降的妖魔是什么身笤? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮葵陵,結(jié)果婚禮上液荸,老公的妹妹穿的比我還像新娘。我一直安慰自己脱篙,他們只是感情好莹弊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著涡尘,像睡著了一般忍弛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上考抄,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天细疚,我揣著相機(jī)與錄音,去河邊找鬼川梅。 笑死疯兼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贫途。 我是一名探鬼主播吧彪,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼丢早!你這毒婦竟也來(lái)了姨裸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤怨酝,失蹤者是張志新(化名)和其女友劉穎傀缩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體农猬,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赡艰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了斤葱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慷垮。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡揖闸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出料身,到底是詐尸還是另有隱情汤纸,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布惯驼,位于F島的核電站蹲嚣,受9級(jí)特大地震影響递瑰,放射性物質(zhì)發(fā)生泄漏祟牲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一抖部、第九天 我趴在偏房一處隱蔽的房頂上張望说贝。 院中可真熱鬧,春花似錦慎颗、人聲如沸乡恕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)傲宜。三九已至,卻和暖如春夫啊,著一層夾襖步出監(jiān)牢的瞬間函卒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工撇眯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留报嵌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓熊榛,卻偏偏與公主長(zhǎng)得像锚国,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子玄坦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容