這一系列文章主要是對protocol buffer這種編碼格式的使用方式、特點、使用技巧進行說明,並在原生protobuf的基礎上進行擴展和優化,使得它能更好地為我們服務。
1.什么是protobuf
protocol buffer是由google推出一種數據編碼格式,不依賴平台和語言,類似於xml和json。然而與xml和json最大的不同之處在於,protobuf並非是一種可以完全自解釋的編碼格式,這點在之后會有說明。
2.為什么要使用protobuf
和json或者xml相比,protocol buffer的解析速度更快,編碼后的字節數更少。
其中解析速度的相關比較可以參看相關文章,這並不是本系列關心的重點,而字節數的減少將會是后續擴展和優化的重點。
另外,比json和xml更便利的是,開發者只需要編寫一份.proto的描述文件,就可以通過google提供的編譯器生成不同平台的模型代碼,包括java、C#等等,而不需要手動進行模型編寫。
本文后續的示例都是采用java進行展示。
3.如何使用protobuf
首先我們需要下載一個google提供的編譯器,下載地址:
https://github.com/protocolbuffers/protobuf/releases/tag/v3.12.1
選擇自己的系統下載相應的zip包
解壓后就能看到看到一個protoc的執行文件,即是我們所需要的編譯器。
接着我們需要定義一份BasicUsage.proto的描述文件,其結構和我們定義普通的類十分類似。
syntax = "proto3";
option java_package = "cn.tera.protobuf.model"; option java_outer_classname = "BasicUsage"; message Person { string name = 1; int32 id = 2; string email = 3; }
第一行表示所使用的的語法版本,這里選擇的是最新的proto3版本。
syntax = "proto3";
第三、四行表示最終生成的java的package名和外部class的類名(這里外部class的意思之后會有代碼解釋)
option java_package = "cn.tera.protobuf.model";
option java_outer_classname = "BasicUsage";
之后緊接着的就是我們所定義的模型,其中大部分都是我們所熟悉的內容。
這里需要特別注意,特別注意,特別注意的是,在字段的后面都跟着一個"= X",這里並不是指這個字段的值,而是表示這個字段的“序號”,和正確地編碼與解碼息息相關,在我看來是protocol buffer的靈魂,之后會有詳細的說明
message Person { string name = 1; int32 id = 2; string email = 3; }
有了編譯器和.poto描述文件,我們就可以生成java模型文件了
編譯指令
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/BasicUsage.proto
-I :表示工作目錄,如果不指定,則就是當前目錄
--java_out:表示輸出.java文件的目錄
這里我比較習慣將.proto文件放到java項目中,並且將.java文件直接生成到相應的package文件夾中,即前文的java_package參數,這樣在使用的時候就可以不用再手動復制文件了
protoc -I=/protocol_buffer/protobuf/proto --java_out=/protocol_buffer/protobuf/src/main/java/ /protocol_buffer/protobuf/proto/BasicUsage.proto
項目的目錄結構如下圖,其中BasicUsage的class文件就是生成出來的
以上都是准備工作,接着我們就要進入代碼相關部分
引入maven依賴
<!--這部分是protobuf的基本庫-->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.9.1</version>
</dependency>
<!--這部分是protobuf和json相關的庫,這里一並導入,后面會用到-->
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.9.1</version>
</dependency>
接着我們創建一個Test方法
/** * protobuf的基礎使用 */ @Test void basicUse() { //創建一個Person對象 BasicUsage.Person person = BasicUsage.Person.newBuilder() .setId(5) .setName("tera") .setEmail("tera@google.com") .build(); System.out.println("Person's name is " + person.getName()); //編碼 //此時我們就可以通過我們想要的方式傳遞該byte數組了 byte[] bytes = person.toByteArray(); //將編碼重新轉換回Person對象 BasicUsage.Person clone = null; try { //解碼 clone = BasicUsage.Person.parseFrom(bytes); System.out.println("The clone's name is " + clone.getName()); } catch (InvalidProtocolBufferException e) { } //引用是不同的 System.out.println("==:" + (person == clone)); //equals方法經過了重寫,所以equals是相同的 System.out.println("equals:" + person.equals(clone)); //修改clone中的值 clone = clone.toBuilder().setName("clone").build(); System.out.println("The clone's new name is " + clone.getName()); }
在Test方法中,我們可以看到,訪問Person類是需要通過BasicUsage.Person進行訪問,這就是我們前面在定義.proto文件時指定的java_outer_classname參數
因為在一個.proto文件中,我們可以定義多個類,而多個.proto文件也可以定義相同的類名,因此用這個java_outer_classname進行區分,可以認為是.proto的package名
這里需要注意幾個點:
protobuf的對象的實例化和賦值必須通過newBuilder()返回的Builder對象進行,實例化最終對象需要通過build()方法。
BasicUsage.Person person = BasicUsage.Person.newBuilder() .setId(5) .setName("tera") .setEmail("tera@google.com") .build();
對象實例化完成之后就只能調用get方法而無法set,如果需要set值,則必須將其轉換回Builder對象才行。
clone = clone.toBuilder().setName("clone").build();
而對象的編碼和解碼,則分別通過toByteArray()方法和parseFrom()方法 。
byte[] bytes = person.toByteArray();
... BasicUsage.Person.parseFrom(bytes);
以上就是protocol buffer的基本使用方式,其實除了賦值比較麻煩意外,其他操作都很方便(如果我們需要在普通的模型中實現.setXX().setYY()這種連續操作,還得另外加個注解呢),特別是對於需要深度clone的對象,protocol buffer也是一個很好的選擇,可以避免很多clone引用的問題。
4.protocol buffer模型解析
當然,了解了基礎使用,源碼的研究自然也是不能少的,不過遵照着循序漸進的原則,我們先看下生成的模型文件中有些什么
查看Person的類,此時的你是不是嚇了一跳,這么簡單的一個類的代碼竟然有這么多!為了不湊字數,我這里就不貼全了,有興趣的同學自己去生成一個看看全貌,總計836行代碼
下面主要看下幾個主要部分
1).BasicUsage
主類名是BasicUsage,其余所有的類都作為了該主類的內部類,所以訪問Person時,需要通過BasicUsage.Person訪問
public final class BasicUsage { ... }
2).PersonOrBuilder接口
PersonOrBuilder接口,定義了Person對象所有字段的get方法以及其對應的字節的get方法
public interface PersonOrBuilder extends // @@protoc_insertion_point(interface_extends:Person) com.google.protobuf.MessageOrBuilder { java.lang.String getName(); com.google.protobuf.ByteString getNameBytes();
int getId();
java.lang.String getEmail(); com.google.protobuf.ByteString getEmailBytes(); }
3).Person類
Person對象是實現了PersonOrBuilder接口的,因此Person只能get而不能set了
public static final class Person extends com.google.protobuf.GeneratedMessageV3 implements PersonOrBuilder {
... }
Person類沒有public的構造函數,只有3個private的構造函數,因此在外部代碼中是不能直接創建Person對象的
3個構造函數分為接受Builder對象、構造空對象、接受CodeInputStream對象
其中Builder對象正是之前提到過的,用於通過Builder創建Person
而CodeInputStream則是指字節數組,則是用於從byte[]中解碼出對象
這2個構造函數在后文中都可以看到使用場景
private Person(com.google.protobuf.GeneratedMessageV3.Builder<?> builder) { super(builder); } private Person() { name_ = ""; email_ = ""; } private Person( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws com.google.protobuf.InvalidProtocolBufferException { ... }
查看Person的getName方法,可以看到在這里,name_是一個Object而不是String,在取值的時候需要做一個類型判斷
這么實現的原因在於,因為對象是可以通過byte[]數組解碼的,而byte[]數組的內容是不可控的、靈活可變的,為了盡量兼容這些情況,所以才會如此處理,這個問題后文會給出一些示例
@java.lang.Override public java.lang.String getName() { java.lang.Object ref = name_; if (ref instanceof java.lang.String) { return (java.lang.String) ref; } else { com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; java.lang.String s = bs.toStringUtf8(); name_ = s; return s; } }
查看equals和hashcode方法,可以看到根據對象字段的內容進行了相應的重寫,因此在之前的基本使用示例中,equals方法會返回true
@java.lang.Override public boolean equals(final java.lang.Object obj) { if (obj == this) { return true; } if (!(obj instanceof cn.tera.protobuf.model.BasicUsage.Person)) { return super.equals(obj); } cn.tera.protobuf.model.BasicUsage.Person other = (cn.tera.protobuf.model.BasicUsage.Person) obj; if (!getName() .equals(other.getName())) return false; if (getId() != other.getId()) return false; if (!getEmail() .equals(other.getEmail())) return false; if (!unknownFields.equals(other.unknownFields)) return false; return true; } @java.lang.Override public int hashCode() { if (memoizedHashCode != 0) { return memoizedHashCode; } int hash = 41; hash = (19 * hash) + getDescriptor().hashCode(); hash = (37 * hash) + NAME_FIELD_NUMBER; hash = (53 * hash) + getName().hashCode(); hash = (37 * hash) + ID_FIELD_NUMBER; hash = (53 * hash) + getId(); hash = (37 * hash) + EMAIL_FIELD_NUMBER; hash = (53 * hash) + getEmail().hashCode(); hash = (29 * hash) + unknownFields.hashCode(); memoizedHashCode = hash; return hash; }
查看Person的toByteArray()方法,可以看到這個方法是在AbstractMessageLite的類中,這是所有Protobuf生成對象的父類中的方法
public byte[] toByteArray() { try { byte[] result = new byte[this.getSerializedSize()]; CodedOutputStream output = CodedOutputStream.newInstance(result); this.writeTo(output); output.checkNoSpaceLeft(); return result; } catch (IOException var3) { throw new RuntimeException(this.getSerializingExceptionMessage("byte array"), var3); } }
此時查看Person類中的this.writeTo方法,可以看到正是在這個方法中寫入了3個字段的數據,這些方法的細節我們需要放到之后的文章中詳細分析,因為涉及到了protobuf的編碼原理等內容
@java.lang.Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { if (!getNameBytes().isEmpty()) { com.google.protobuf.GeneratedMessageV3.writeString(output, 1, name_); } if (id_ != 0) { output.writeInt32(2, id_); } if (!getEmailBytes().isEmpty()) { com.google.protobuf.GeneratedMessageV3.writeString(output, 3, email_); } unknownFields.writeTo(output); }
對於Person類,我們最后再看一下parseFrom方法,這個方法有很多的重載,然而本質都是一樣的,通過PARSER去處理數據,這里我就不全貼出來了
public static cn.tera.protobuf.model.BasicUsage.Person parseFrom(byte[] data) throws com.google.protobuf.InvalidProtocolBufferException { return PARSER.parseFrom(data); }
查看PARSER對象,這里正是會調用Person的接受Stream參數的構造函數,和前文對應
private static final com.google.protobuf.Parser<Person> PARSER = new com.google.protobuf.AbstractParser<Person>() { @java.lang.Override public Person parsePartialFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws com.google.protobuf.InvalidProtocolBufferException { return new Person(input, extensionRegistry); } };
4).Builder類
Builder類為Person的內部類,一樣實現了PersonOrBuilder接口,不過額外定義了set的方法
public static final class Builder extends com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements // @@protoc_insertion_point(builder_implements:Person) cn.tera.protobuf.model.BasicUsage.PersonOrBuilder { ... }
這里的get方法的邏輯和Person類一樣,不過特別注意的是,這里的name_和Person的getName方法中的name_不是同一個對象,而是分別屬於Builder類和Person類的private字段
public java.lang.String getName() { java.lang.Object ref = name_; if (!(ref instanceof java.lang.String)) { com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; java.lang.String s = bs.toStringUtf8(); name_ = s; return s; } else { return (java.lang.String) ref; } }
查看set方法,比較簡單,就是一個直接的賦值操作
public Builder setName( java.lang.String value) { if (value == null) { throw new NullPointerException(); } name_ = value; onChanged(); return this; }
最后,我們來看下Builder的build方法,這里調用了buildPartial方法
@java.lang.Override public cn.tera.protobuf.model.BasicUsage.Person build() { cn.tera.protobuf.model.BasicUsage.Person result = buildPartial(); if (!result.isInitialized()) { throw newUninitializedMessageException(result); } return result; }
查看buildPartial方法,可以看到這里調用了Person獲取builder參數的構造函數,和前文對應
構造完成后,將Builder中的各種字段賦值給Person中的相應字段,即完成了構造
@java.lang.Override public cn.tera.protobuf.model.BasicUsage.Person buildPartial() { cn.tera.protobuf.model.BasicUsage.Person result = new cn.tera.protobuf.model.BasicUsage.Person(this); result.name_ = name_; result.id_ = id_; result.email_ = email_; onBuilt(); return result; }
總結一下:
1.protocol buffer需要定義.proto描述文件,然后通過google提供的編譯器生成特定的模型文件,之后就可以作為正常的java對象使用了
2.不可以直接創建對象,需要通過Builder進行
3.只有Builder才可以進行set
4.可以通過對象的toByteArray()和parseFrom()方法進行編碼和解碼
5.模型文件很大(至少在java這里是如此),其中所有的代碼都是定制的,這其實是它很大的缺點之一
這里留了幾個伏筆,在maven引用中提到了json,在.proto描述文件中提到了=X的序號很重要,在getName()方法中提到了靈活性,這些內容會在下一篇文章中繼續探究,本文主要是對protocol buffer進行初步了解