游戲開發(fā)-協(xié)議-protobuf原理詳解

如果你有看過上一篇《游戲開發(fā)-協(xié)議設(shè)計-protobuf》,就會了解到prtobuf的what和how,那么這一篇主要分析一下why的問題,protobuf為何解析速度快,占用空間小,以及兼容性好,它是如何做到的,我們將從 占用空間,解析速度,兼容三個問題著手進(jìn)行分析。

上一篇發(fā)出后也有同學(xué)留言,說直接二進(jìn)制read和write比protobuf會節(jié)省空間,某種程度上他說的對,但是需要在極端條件下才成立,一般情況下protobuf 還是比二進(jìn)制序列化節(jié)省空間的,具體為何,以下會詳細(xì)介紹。

占用空間

一條消息數(shù)據(jù),用protobuf序列化后的大小是json的10分之一,xml格式的20分之一,是二進(jìn)制序列化的10分之一(極端情況下,會大于等于直接序列化),總體看來ProtoBuf的優(yōu)勢還是很明顯的。那么protobuf是如何做到的,我們主要從以下4個方面進(jìn)行分析。

1、數(shù)據(jù)緊湊

相對于json或者xml,protobuf沒有定義標(biāo)簽,直接生成的二進(jìn)制,令消息非常緊湊,這個和我們直接定義消息,然后順序解析二進(jìn)制數(shù)據(jù)相似。期間沒有多余的數(shù)據(jù)。

一個message的信息結(jié)構(gòu)如下:

每個field由一個tag和value組成,每個field之間在字節(jié)流中緊密相連,這意味著消息的信息沒用冗余,保持最緊湊的樣子。

2、剔除無效字段

剔除無值字段一般json和xml這種標(biāo)簽式結(jié)構(gòu)數(shù)據(jù)在序列化的時候也會處理,同樣protobuf也做了處理。我們先看一下一個常規(guī)定義的的二進(jìn)制信息:


此種常規(guī)定義,可以對數(shù)據(jù)順序?qū)懭?,然后再順序讀取,也支持?jǐn)?shù)據(jù)的序列化。但會帶來一個問題,某些字段沒有賦值的情況下,不得不傳一個默認(rèn)值。比如field3的值是一個int,占位4個字節(jié)。如果client的field2沒有賦值,而不寫入一個默認(rèn)值(比如0),那么server解包就會偏移量就會出錯,最終整個包的數(shù)據(jù)讀不出。

protobuf 是如何解決這個問題,因為它引入了tag。

我們看下tag的組成。

tag是由:fieldNumber和wireType組成,fieldNumber定義字段的標(biāo)識位,以此來處理寫入和讀取順序,wireType定義字段類型,以此來定義單個field的占用字節(jié)大小。

wireType 可以支持的類型如下:

我們看下每個的field解析方式:

讀取field的時候,先讀取tag,然后基于tag知道value的數(shù)據(jù)類型,獲取value。write也一樣,單個field的寫入也是先寫入tag再寫入value。

因為每個field都定義了tag,如若field沒有賦值,編碼的時候它的tag不會被寫入流中,相應(yīng)也不會有它的value,如此解析期間因為沒有此字段的tag,可以直接無視,讀取其他field。如此去除無效字段之后,可以有效的節(jié)省空間。

比如上述的常規(guī)定義的的二進(jìn)制信息,在field2沒有賦值的情況下,protobuf可以如此處理。

3、Varints &?Zigzag

Varints

我們在上面的wireType中也看到有一種Varint類型的field定義,這是要說第三個特點(diǎn)。

Varint 是一種緊湊的表示數(shù)字的方法。它用一個或多個字節(jié)來表示一個數(shù)字,值越小的數(shù)字使用越少的字節(jié)數(shù)。這能減少用來表示數(shù)字的字節(jié)數(shù)。我們看下它的算法:

我們知道,?int32數(shù)據(jù)類型?,一般需要4 個 字節(jié)?來表示,采用 Varint編碼之后,對于很小的 int32 類型的數(shù)字,比如小于127的,則可以用 1 個 字節(jié)?來存儲,小于255的2個字節(jié)來存儲,依次類推,最優(yōu)占用字節(jié)的大小。當(dāng)然這也有不好的一面,采用 Varint 表示法,太大的數(shù)字則需要 5 個 byte 來表示。不過一般不會所有的消息中的數(shù)字都是大數(shù),而且大數(shù)的概率比較低,所以大多數(shù)情況下,采用 Varint 后,可以用更少的字節(jié)數(shù)來表示數(shù)字信息。

ZigZag

我們知道有符號的整型數(shù)值,因為采用的是補(bǔ)碼,所以一個負(fù)數(shù)會比正數(shù)占用的字節(jié)多,比如-1,二進(jìn)制結(jié)構(gòu)是11111111 11111111 11111111 11111111,如果我們還是采用 Varint 表示一個負(fù)數(shù),那么需要 5 個 byte。為此 protobuf?定義了 sint32 ,采用 zigzag 編碼。

我們看下zigzag的算法:

Zigzag 編碼用無符號數(shù)來表示有符號數(shù)字,正數(shù)和負(fù)數(shù)交錯,無論正負(fù)都可以采用較少的 byte 來表示。

4、字符串

一般單個字符串的定義,需要兩個部分組成:leg+value ,如下圖所示:

leg 表示字符串?dāng)?shù)據(jù)的長度,value是字符串真實數(shù)據(jù),protobuf里面這個leg 采用Varint定義,一般情況下一個字節(jié)足夠了。

解析速度

解析速度快,主要?dú)w功于protobuf對message 沒有動態(tài)解析,沒有了動態(tài)解析的處理序列化速度自然快了。就比如xml ,獲取文件之后,還需要解析標(biāo)簽、節(jié)點(diǎn)、字段,每一個都需要遍歷,而protobuf不需要,直接將field裝入流。

我們知道.proto文件定義了整個message的結(jié)構(gòu),但這只是一個定義的配置文件,結(jié)合compiler的使用,單個message的read和write代碼已經(jīng)被生成,無需再基于配置文件解析,直接操作field到二進(jìn)制流里面,這個速度就好比你直接操作IO一樣快,沒有其他代價。

我們看下生成的message代碼(還是LoginMsg)

write

read

我們看到,read的代碼中tag的生成(轉(zhuǎn)換為int)也已經(jīng)幫你處理,所以都是基于流直接操作。

兼容性

兼容性什么意思,就是說message需要支持向上兼容,不能說單個message的升級,就會導(dǎo)致old message解析出錯,這是我們不能忍受的,開發(fā)過程中,需求總變,誰都無法保證協(xié)議不會有變更。

比如這種場景下:message 需要增加一個字段,如若client沒有升級,sever升級了,此時client 請求的message格式必定是old 格式,server 采用的new message來解析,此時會出現(xiàn)找不到新字段的問題,流數(shù)據(jù)錯亂之后,后續(xù)的數(shù)據(jù)都會亂。

那么protobuf是如何處理?這時候fieldNumber就派上用途了,看似可又可無的設(shè)計,其實包含很大用處。

fieldNumber 為每個field定義一個編號,其一保證不重復(fù),其二保證其在流中的位置。如若當(dāng)前數(shù)據(jù)流中有某個字段,而解析方?jīng)]有相關(guān)的解析代碼,解析放會直接skip 吊這個field,而且讀數(shù)據(jù)的position也會后移,保證后續(xù)讀取不出問題。

如上,線讀取tag,某個字段沒有被賦值,就沒有這個字段的tag,解析方不處理,如若有某個字段,而沒有解析方法,就skip了,不影響消息的處理。老數(shù)據(jù)依然被獲取。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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