![](http://static.zybuluo.com/Yano/6tikg8w6ppn8k16zh1ee325t/image_1b40hs8rvbdpouuseu4q51b4s9.png)
protobuf 簡介
protobuf是什么
protobuf
(Protocol Buffers)是Google推出的一個(gè)結(jié)構(gòu)化數(shù)據(jù)交換協(xié)議劲室,用于傳遞自定義的消息格式,可用于同一臺機(jī)器的進(jìn)程間、不同設(shè)備進(jìn)程間的數(shù)據(jù)傳遞泡一。protobuf是一種語言無關(guān)兰吟、平臺無關(guān)、高效仙逻、擴(kuò)展性良好的語言驰吓,提供了一種將結(jié)構(gòu)化數(shù)據(jù)進(jìn)行序列化和反序列化
的方法。
相對于XML系奉,protobuf的體積更小檬贰、速度更快、使用更簡單缺亮。我們僅需要定義一次數(shù)據(jù)結(jié)構(gòu)翁涤,就可以很輕松地使用生成的代碼讀/寫數(shù)據(jù),而且這些數(shù)據(jù)結(jié)構(gòu)是向后兼容
的萌踱。
官方網(wǎng)站
https://developers.google.com/protocol-buffers/
protobuf的優(yōu)劣
為什么不使用XML葵礼?
相對于XML來說,Protocol buffers在序列化結(jié)構(gòu)化數(shù)據(jù)上虫蝶,具有非常明顯的優(yōu)勢:
- 更加簡單
- 體積減小3~10倍
- 速度提高20~100倍
- 更清晰
- 生成的數(shù)據(jù)結(jié)構(gòu)代碼章咧,更容易使用
如果要生成一個(gè)具有name
和email
的person
實(shí)例,XML做法如下:
<person>
<name>John Doe</name>
<email>jdoe@example.com</email>
</person>
使用protocol:
# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
name: "John Doe"
email: "jdoe@example.com"
}
當(dāng)此消息被編碼為二進(jìn)制格式時(shí)能真,長度大概是28字節(jié)赁严,解析時(shí)間為100200ns。在去除所有空格后粉铐,XML版本也至少為69字節(jié)疼约,解析時(shí)間長達(dá)500010000ns。
同時(shí)蝙泼,使用protocol buffer更簡單:
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
使用XML的代碼:
cout << "Name: "
<< person.getElementsByTagName("name")->item(0)->innerText()
<< endl;
cout << "E-mail: "
<< person.getElementsByTagName("email")->item(0)->innerText()
<< endl;
相對于JSON來說程剥,protobuf也有明顯的優(yōu)勢:
- 更好的前后數(shù)據(jù)版本兼容性
- 提供了驗(yàn)證機(jī)制,更容易被擴(kuò)展
- 不同語言間互操作性更好
protobuf也有一些缺點(diǎn),并不是適合所有場景织鲸。
- 二進(jìn)制編碼和傳輸舔腾,可讀性差
- 編碼和解碼依賴額外的庫,不能在瀏覽器搂擦、JS中直接使用
- 缺乏自描述
如何使用protobuf
- 定義
.proto
文件 - 編譯protocol buffer
- 使用Java protocol buffer API 讀寫數(shù)據(jù)
下面是通過Java使用protobuf的官方示例:https://developers.google.com/protocol-buffers/docs/javatutorial 稳诚,并在此基礎(chǔ)上進(jìn)行了簡化。
定義.proto
文件
定義需要序列化的數(shù)據(jù)結(jié)構(gòu)瀑踢,為message
中的每一個(gè)變量設(shè)置名稱和類型扳还。下面
package tutorial;
option java_package = "yano";
option java_outer_classname = "AddressBookProtos";
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}
- package:防止不同工程中的命名沖突;生成Java的proto時(shí)橱夭,如果沒有指定java_package氨距,包名默認(rèn)為package
- java_package:包名
- java_outer_classname:定義生成Java代碼的文件名(類名);如果沒有指定棘劣,會將proto文件變成駝峰形式:默認(rèn)會將
my_proto.proto
生成MyProto
的類文件俏让。
定義字段時(shí),我們使用了required茬暇、optional舆驶、repeated三個(gè)關(guān)鍵字。這些關(guān)鍵字表示對字段的約束而钞,分別表示:
- required-非空約束。如果字段值為空拘荡,會被認(rèn)為是uninitialized臼节,并拋出異常。
- optional-可選珊皿。表示字段可以賦值网缝,也可以不賦值。不賦值時(shí)蟋定,將會使用默認(rèn)值粉臊。
- repeated-可重復(fù)次數(shù)。表示字段可以重復(fù)使用的次數(shù)驶兜,重復(fù)順序會被保存在protobuf中扼仲,可以將其理解為一個(gè)數(shù)組。
proto文件中的其它格式抄淑,在此不作介紹屠凶,詳細(xì)內(nèi)容可以參考官方文檔。
編譯protocol buffer
現(xiàn)在我們有了一個(gè).proto
文件肆资,接下來就需要將生成class文件矗愧,我們可以通過這個(gè)class文件讀寫AddressBook的消息。
- proto編譯器下載地址:https://developers.google.com/protocol-buffers/docs/downloads
- 運(yùn)行編譯器郑原,指定proto路徑唉韭、生成路徑夜涕、
.proto
文件。
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
Protocol Buffer API
通過proto編譯器属愤,addressbook.proto
生成了Java類AddressBookProtos.java
女器。每個(gè)class都有自己的Builder
,用以生成該類的實(shí)例春塌。
// required string name = 1;
public boolean hasName();
public String getName();
// required int32 id = 2;
public boolean hasId();
public int getId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
Person.Builder 除了getters晓避,還有setters方法:
// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();
// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();
// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<PhoneNumber> value);
public Builder clearPhone();
![](http://static.zybuluo.com/Yano/ovb3tfk71lwhynyplfocxh3k/image_1b403efmqj1d17me178b1aft7njm.png)
創(chuàng)建一個(gè) Person 實(shí)例:
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhone(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.HOME))
.build();
標(biāo)準(zhǔn)消息方法
以下方法能夠讓我們檢查和操作整個(gè)message:
- isInitialized():檢查required字段是否全部設(shè)置
- toString():返回可閱讀的格式,在debug時(shí)非常有用
- mergeFrom(Message other):將
other
的內(nèi)容合并到該message中只壳,會覆蓋相同的字段俏拱,對repeated
字段會添加 - clear():重置所有字段
解析和序列化
所有的protocol buffer類都有讀寫二進(jìn)制的方法:
- byte[] toByteArray():序列化消息并返回包含其原始字節(jié)的字節(jié)數(shù)組
- static Person parseFrom(byte[] data):通過給定的字節(jié)數(shù)組,解析message
- void writeTo(OutputStream output):序列化消息吼句,并將其寫到
OutputStream
- static Person parseFrom(InputStream input):從
InputStream
中讀取并解析message
擴(kuò)展protobuf
在擴(kuò)展proto文件時(shí)锅必,需要注意以下事項(xiàng):
- 絕對不能改變已經(jīng)存在的字段的
tag numbers
- 絕對不能添加或刪除
required
字段 - 可以刪除
optional
和repeated
字段 - 可以添加新的
optional
和repeated
字段,但是必須使用新的tag numbers
結(jié)束語
我的博客:http://www.reibang.com/users/6835c29fc12a/latest_articles
我的知乎:https://www.zhihu.com/people/liu-jia-yu-58/activities