基本類型編碼
在前文有提到消息是一系列的基本類型以及其他消息類型的組合,因而基本類型是probobuf編碼實(shí)現(xiàn)的基礎(chǔ),這些基本類型有:
.proto Type | Java Type | C++ Type | Wire Type |
double | double | double | WIRETYPE_FIXED64(1) |
float | float | float | WIRETYPE_FIXED32(5) |
int64 | long | int64 | WIRETYPE_VARINT(0) |
int32 | int | int32 | WIRETYPE_VARINT(0) |
uint64 | long | unit64 | WIRETYPE_VARINT(0) |
uint32 | int | unit32 | WIRETYPE_VARINT(0) |
sint64 | long | int64 | WIRETYPE_VARINT(0) |
sint32 | int | int32 | WIRETYPE_VARINT(0) |
fixed64 | long | unit64 | WIRETYPE_FIXED64(1) |
fixed32 | int | unit32 | WIRETYPE_FIXED32(5) |
sfixed64 | long | int64 | WIRETYPE_FIXED64(1) |
sfixed32 | int | int32 | WIRETYPE_FIXED32(5) |
bool | boolean | bool | WIRETYPE_VARINT(0) |
string | String | string | WIRETYPE_LENGTH_DELIMITED(2) |
bytes | ByteString | string | WIRETYPE_LENGTH_DELIMITED(2) |
在Java種對(duì)不同類型的選擇,其他的類型區(qū)別很明顯,主要在與int32、uint32、sint32、fixed32中以及對(duì)應(yīng)的64位版本的選擇,因?yàn)樵?/span>Java中這些類型都用int(long)來(lái)表達(dá),但是protobuf內(nèi)部使用ZigZag編碼方式來(lái)處理多余的符號(hào)問(wèn)題,但是在編譯生成的代碼中并沒(méi)有驗(yàn)證邏輯,比如uint的字段不能傳入負(fù)數(shù)之類的。而從編碼效率上,對(duì)fixed32類型,如果字段值大于2^28,它的編碼效率比int32更加有效;而在負(fù)數(shù)編碼上sint32的效率比int32要高;uint32則用于字段值永遠(yuǎn)是正整數(shù)的情況。
在實(shí)現(xiàn)上,protobuf使用CodedOutputStream實(shí)現(xiàn)序列化邏輯、CodedInputStream實(shí)現(xiàn)反序列化邏輯,他們都包含write/read基本類型和Message類型的方法,write方法中同時(shí)包含fieldNumber和value參數(shù),在寫入時(shí)先寫入由fieldNumber和WireType組成的tag值(添加這個(gè)WireType類型信息是為了在對(duì)無(wú)法識(shí)別的字段編碼時(shí)可以通過(guò)這個(gè)類型信息判斷使用那種方式解析這個(gè)未知字段,所以這幾種類型值即可),這個(gè)tag值是一個(gè)可變長(zhǎng)int類型,所謂的可變長(zhǎng)類型就是一個(gè)字節(jié)的最高位(msb,most significant bit)用1表示后一個(gè)字節(jié)屬于當(dāng)前字段,而最高位0表示當(dāng)前字段編碼結(jié)束。在寫入tag值后,再寫入字段值value,對(duì)不同的字段類型采用不同的編碼方式:
1. 對(duì)int32/int64類型,如果值大于等于0,直接采用可變長(zhǎng)編碼,否則,采用64位的可變長(zhǎng)編碼,因而其編碼結(jié)果永遠(yuǎn)是10個(gè)字節(jié),所有說(shuō)它int32/int64類型在編碼負(fù)數(shù)效率很低(然而這里我一直木有想明白對(duì)int32類型為什么需要做64位的符號(hào)擴(kuò)展,不擴(kuò)展,5個(gè)字節(jié)就可以了啊,而且對(duì)64位的負(fù)數(shù)也不需要用符號(hào)擴(kuò)展,或者無(wú)法符號(hào)擴(kuò)展,google上也沒(méi)有找到具體原因)。
2. 對(duì)uint32/uint64類型,也采用變長(zhǎng)編碼,不對(duì)負(fù)數(shù)做驗(yàn)證。
3. 對(duì)sint32/sint64類型,首先對(duì)該值做ZigZag編碼,以保留,然后將編碼后的值采用變長(zhǎng)編碼。所謂ZigZag編碼即將負(fù)數(shù)轉(zhuǎn)換成正數(shù),而所有正數(shù)都乘2,如0編碼成0,-1編碼成1,1編碼成2,-2編碼成3,以此類推,因而它對(duì)負(fù)數(shù)的編碼依然保持比較高的效率。
4. 對(duì)fixed32/sfixed32/fixed64/sfixed64類型,直接將該值以小端模式的固定長(zhǎng)度編碼。
5. 對(duì)double類型,先將double轉(zhuǎn)換成long類型,然后以8個(gè)字節(jié)固定長(zhǎng)度小端模式寫入。
6. 對(duì)float類型,先將float類型轉(zhuǎn)換成int類型,然后以4個(gè)字節(jié)固定長(zhǎng)度小端模式寫入。
7. 對(duì)bool類型,寫0或1的一個(gè)字節(jié)。
8. 對(duì)string類型,使用UTF-8編碼獲取字節(jié)數(shù)組,然后先用變長(zhǎng)編碼寫入字節(jié)數(shù)組長(zhǎng)度,然后寫入所有的字節(jié)數(shù)組。
Tag | msgByteSize | msgByte |
9. 對(duì)bytes類型(ByteString),先用變長(zhǎng)編碼寫入長(zhǎng)度,然后寫入整個(gè)字節(jié)數(shù)組。
Tag | msgByteSize | msgByte |
10. 對(duì)枚舉類型(類型值WIRETYPE_VARINT),用int32編碼方式寫入定義枚舉項(xiàng)時(shí)給定的值(因而在給枚舉類型項(xiàng)賦值時(shí)不推薦使用負(fù)數(shù),因?yàn)?/span>int32編碼方式對(duì)負(fù)數(shù)編碼效率太低)。
11. 對(duì)內(nèi)嵌Message類型(類型值WIRETYPE_LENGTH_DELIMITED),先寫入整個(gè)Message序列化后字節(jié)長(zhǎng)度,然后寫入整個(gè)Message。
Tag | msgByteSize | msgByte |
注:ZigZag編碼實(shí)現(xiàn):(n << 1) ^ (n >> 31) / (n << 1) ^ (n >> 63);在CodedOutputStream中還存在一些用于計(jì)算某個(gè)字段可能占用的字節(jié)數(shù)的compute靜態(tài)方法,這里不再詳述。
在protobuf的序列化中,所有的類型最終都會(huì)轉(zhuǎn)換成一個(gè)可變長(zhǎng)int/long類型、固定長(zhǎng)度的int/long類型、byte類型以及byte數(shù)組。對(duì)byte類型的寫只是簡(jiǎn)單的對(duì)內(nèi)部buffer的賦值:
if (position == limit) {
refreshBuffer();
}
buffer[position++] = value;
}
對(duì)32位可變長(zhǎng)整形實(shí)現(xiàn)為:
while (true) {
if ((value & ~0x7F) == 0) {
writeRawByte(value);
return;
} else {
writeRawByte((value & 0x7F) | 0x80);
value >>>= 7;
}
}
}
writeRawByte((value ) & 0xFF);
writeRawByte((value >> 8) & 0xFF);
writeRawByte((value >> 16) & 0xFF);
writeRawByte((value >> 24) & 0xFF);
}
對(duì)byte數(shù)組,可以簡(jiǎn)單理解為依次調(diào)用writeRawByte()方法,只是CodedOutputStream在實(shí)現(xiàn)時(shí)做了部分性能優(yōu)化。這里不詳細(xì)介紹。
對(duì)CodedInputStream則是根據(jù)CodedOutputStream的編碼方式進(jìn)行解碼,因而也不詳述,其中關(guān)于ZigZag的解碼:(n >>> 1) ^ -(n & 1)
repeated字段編碼
對(duì)于repeated字段,一般有兩種編碼方式:
1. 每個(gè)項(xiàng)都先寫入tag,然后寫入具體數(shù)據(jù)。如對(duì)基本類型:
Tag | Data | Tag | Data | … |
而對(duì)message類型:
Tag | Length | Data | Tag | Length | Data | … |
2. 先寫入tag,后count,再寫入count個(gè)項(xiàng),每個(gè)項(xiàng)包含length|data數(shù)據(jù)。即:
Tag | Count | Length | Data | Length | Data | … |
從編碼效率的角度來(lái)看,個(gè)人感覺(jué)第二中情況更加有效,然而不知道處于什么原因考慮,protobuf采用了第一種方式來(lái)編碼,個(gè)人能想到的一個(gè)理由是第一種情況下,每個(gè)消息項(xiàng)都是相對(duì)獨(dú)立的,因而在傳輸過(guò)程中接收端每接收到一個(gè)消息項(xiàng)就可以進(jìn)行解析,而不需要等待整個(gè)repeated字段的消息包。對(duì)于基本類型,protobuf也采用了第一種編碼方式,后來(lái)發(fā)現(xiàn)這種編碼方式效率太低,因而可以添加[packed = true]的描述將其轉(zhuǎn)換成第三種編碼方式(第二種方式的變種,對(duì)基本數(shù)據(jù)類型,比第二種方式更加有效):
3. 先寫入tag,后寫入字段的總字節(jié)數(shù),再寫入每個(gè)項(xiàng)數(shù)據(jù)。即:
Tag | dataByteSize | Data | Data | … |
目前protobuf只支持基本類型的packed修飾,因而如果將packed添加到非repeated字段或非基本類型的repeated字段,編譯器在編譯.proto文件時(shí)會(huì)報(bào)錯(cuò)。
未識(shí)別字段編碼
在protobuf中,將所有未識(shí)別字段保存在UnknownFieldSet中,并且在每個(gè)由protobuf編譯生成的Message類以及GeneratedMessage.Builder中保存了UnknownFieldSet字段unknownFields;該字段可以從CodedInputStream中初始化(調(diào)用UnknownFieldSet.Builder的mergeFieldFrom()方法)或從用戶自己通過(guò)Builder設(shè)置;在序列化時(shí),調(diào)用UnknownFieldSet的writeTo()方法將自身內(nèi)容序列化到CodedOutputStream中。
UnknownFieldSet顧名思義是未知字段的集合,其內(nèi)部數(shù)據(jù)結(jié)構(gòu)是一個(gè)FieldNumber到Field的Map,而一個(gè)Field用于表達(dá)一個(gè)未知字段,它可以是任何值,因而它包含了所有5中類型的List字段,這里并沒(méi)有對(duì)一個(gè)Field驗(yàn)證,因而允許多個(gè)相同FieldNumber的未知字段,并且他們可以是任意類型值。UnknownFieldSet采用MessageLite編程模式,因而它實(shí)現(xiàn)了MessageLite接口,并且定義了一個(gè)Builder類實(shí)現(xiàn)MessageLite.Builder接口用于手動(dòng)或從CodedInputStream中構(gòu)建UnknownFieldSet。雖然Field本身沒(méi)有實(shí)現(xiàn)MessageLite接口,它依然實(shí)現(xiàn)了該接口的部分方法,如writeTo()、getSerializedSize()用于實(shí)現(xiàn)向CodedOutputStream中序列化自身,并且定義了Field.Builder類用于構(gòu)建Field實(shí)例。
在一個(gè)Message序列化時(shí)(writeTo()方法實(shí)現(xiàn)),在寫完所有可識(shí)別的字段以及擴(kuò)展字段,這個(gè)定義在Message中的UnknownFieldSet也會(huì)被寫入CodedOutputStream中;而在從CodedInputStream中解析時(shí),對(duì)任何未知字段也都會(huì)被寫入這個(gè)UnknownFieldSet中。

擴(kuò)展字段編碼
在寫框架代碼時(shí),經(jīng)常由擴(kuò)展性的需求,在Java中,只需要簡(jiǎn)單的定義一個(gè)父類或接口即可解決,如果框架本身還負(fù)責(zé)構(gòu)建實(shí)例本身,可以使用反射或暴露Factory類也可以順利實(shí)現(xiàn),然而對(duì)序列化來(lái)說(shuō),就很難提供這種動(dòng)態(tài)plugin機(jī)制了。然而protobuf還是提出來(lái)一個(gè)相對(duì)可以接受的機(jī)制(語(yǔ)法有點(diǎn)怪異,但是至少可以用):在一個(gè)message中定義它支持的可擴(kuò)展字段值的范圍,然后用戶可以使用extend關(guān)鍵字?jǐn)U展該message定義(具體參考相關(guān)章節(jié))。在實(shí)現(xiàn)中,所有這些支持字段擴(kuò)展的message類型繼承自ExtendableMessage類(它本身繼承自GeneratedMessage類)并實(shí)現(xiàn)ExtendableMessageOrBuilder接口,而它們的Builder類則繼承自ExtendableBuilder類并且同時(shí)也實(shí)現(xiàn)了ExtendableMessageOrBuilder接口。
ExtendableMessage和ExtendableBuilder類都包含FieldSet<FieldDescriptor>類型的字段用于保存該message所有的擴(kuò)展字段值。FieldSet中保存了FieldDescriptor到其Object值的Map,然而在ExtendableMessage和ExtendableBuilder中則使用GeneratedExtension來(lái)表識(shí)一個(gè)擴(kuò)展字段,這是因?yàn)?/span>GeneratedExtension除了包含對(duì)一個(gè)擴(kuò)展字段的描述信息FieldDescriptor外,還存儲(chǔ)了該擴(kuò)展字段的類型、默認(rèn)值等信息,在protobuf消息定義編譯器中會(huì)為每個(gè)擴(kuò)展字段生成相應(yīng)的GeneratedExtension實(shí)例以供用戶使用:
bar.internalInit(descriptor.getExtensions().get(0));

Base base = Base.newBuilder().setExtension(SearchRequestProtos.bar, 11).build();
registry.add(SearchRequestProtos.bar);
}
