如果你有看過上一篇《游戲開發(fā)-協(xié)議設(shè)計(jì)-protobuf》诱贿,就會(huì)了解到prtobuf的what和how,那么這一篇主要分析一下why的問題,protobuf為何解析速度快茴厉,占用空間小旅掂,以及兼容性好赏胚,它是如何做到的,我們將從 占用空間商虐,解析速度觉阅,兼容三個(gè)問題著手進(jìn)行分析崖疤。
上一篇發(fā)出后也有同學(xué)留言,說直接二進(jìn)制read和write比protobuf會(huì)節(jié)省空間典勇,某種程度上他說的對(duì)劫哼,但是需要在極端條件下才成立,一般情況下protobuf 還是比二進(jìn)制序列化節(jié)省空間的割笙,具體為何权烧,以下會(huì)詳細(xì)介紹。
占用空間
一條消息數(shù)據(jù)伤溉,用protobuf序列化后的大小是json的10分之一般码,xml格式的20分之一,是二進(jìn)制序列化的10分之一(極端情況下乱顾,會(huì)大于等于直接序列化)侈询,總體看來ProtoBuf的優(yōu)勢(shì)還是很明顯的。那么protobuf是如何做到的糯耍,我們主要從以下4個(gè)方面進(jìn)行分析扔字。
1、數(shù)據(jù)緊湊
相對(duì)于json或者xml温技,protobuf沒有定義標(biāo)簽革为,直接生成的二進(jìn)制,令消息非常緊湊舵鳞,這個(gè)和我們直接定義消息震檩,然后順序解析二進(jìn)制數(shù)據(jù)相似。期間沒有多余的數(shù)據(jù)蜓堕。
一個(gè)message的信息結(jié)構(gòu)如下:
每個(gè)field由一個(gè)tag和value組成抛虏,每個(gè)field之間在字節(jié)流中緊密相連,這意味著消息的信息沒用冗余套才,保持最緊湊的樣子迂猴。
2、剔除無效字段
剔除無值字段一般json和xml這種標(biāo)簽式結(jié)構(gòu)數(shù)據(jù)在序列化的時(shí)候也會(huì)處理背伴,同樣protobuf也做了處理沸毁。我們先看一下一個(gè)常規(guī)定義的的二進(jìn)制信息:
此種常規(guī)定義,可以對(duì)數(shù)據(jù)順序?qū)懭肷导牛缓笤夙樞蜃x取息尺,也支持?jǐn)?shù)據(jù)的序列化。但會(huì)帶來一個(gè)問題疾掰,某些字段沒有賦值的情況下搂誉,不得不傳一個(gè)默認(rèn)值。比如field3的值是一個(gè)int静檬,占位4個(gè)字節(jié)炭懊。如果client的field2沒有賦值浪汪,而不寫入一個(gè)默認(rèn)值(比如0),那么server解包就會(huì)偏移量就會(huì)出錯(cuò)凛虽,最終整個(gè)包的數(shù)據(jù)讀不出死遭。
protobuf 是如何解決這個(gè)問題,因?yàn)樗肓藅ag凯旋。
我們看下tag的組成呀潭。
tag是由:fieldNumber和wireType組成,fieldNumber定義字段的標(biāo)識(shí)位至非,以此來處理寫入和讀取順序钠署,wireType定義字段類型,以此來定義單個(gè)field的占用字節(jié)大小荒椭。
wireType 可以支持的類型如下:
我們看下每個(gè)的field解析方式:
讀取field的時(shí)候谐鼎,先讀取tag,然后基于tag知道value的數(shù)據(jù)類型趣惠,獲取value狸棍。write也一樣,單個(gè)field的寫入也是先寫入tag再寫入value味悄。
因?yàn)槊總€(gè)field都定義了tag草戈,如若field沒有賦值,編碼的時(shí)候它的tag不會(huì)被寫入流中侍瑟,相應(yīng)也不會(huì)有它的value,如此解析期間因?yàn)闆]有此字段的tag唐片,可以直接無視,讀取其他field涨颜。如此去除無效字段之后费韭,可以有效的節(jié)省空間。
比如上述的常規(guī)定義的的二進(jìn)制信息,在field2沒有賦值的情況下庭瑰,protobuf可以如此處理星持。
3、Varints &?Zigzag
Varints
我們?cè)谏厦娴膚ireType中也看到有一種Varint類型的field定義见擦,這是要說第三個(gè)特點(diǎn)钉汗。
Varint 是一種緊湊的表示數(shù)字的方法羹令。它用一個(gè)或多個(gè)字節(jié)來表示一個(gè)數(shù)字鲤屡,值越小的數(shù)字使用越少的字節(jié)數(shù)。這能減少用來表示數(shù)字的字節(jié)數(shù)福侈。我們看下它的算法:
我們知道酒来,?int32數(shù)據(jù)類型?,一般需要4 個(gè) 字節(jié)?來表示肪凛,采用 Varint編碼之后堰汉,對(duì)于很小的 int32 類型的數(shù)字辽社,比如小于127的,則可以用 1 個(gè) 字節(jié)?來存儲(chǔ)翘鸭,小于255的2個(gè)字節(jié)來存儲(chǔ)滴铅,依次類推,最優(yōu)占用字節(jié)的大小就乓。當(dāng)然這也有不好的一面汉匙,采用 Varint 表示法,太大的數(shù)字則需要 5 個(gè) byte 來表示生蚁。不過一般不會(huì)所有的消息中的數(shù)字都是大數(shù)噩翠,而且大數(shù)的概率比較低,所以大多數(shù)情況下邦投,采用 Varint 后伤锚,可以用更少的字節(jié)數(shù)來表示數(shù)字信息。
ZigZag
我們知道有符號(hào)的整型數(shù)值志衣,因?yàn)椴捎玫氖茄a(bǔ)碼屯援,所以一個(gè)負(fù)數(shù)會(huì)比正數(shù)占用的字節(jié)多,比如-1,二進(jìn)制結(jié)構(gòu)是11111111 11111111 11111111 11111111,如果我們還是采用 Varint 表示一個(gè)負(fù)數(shù)念脯,那么需要 5 個(gè) byte玄呛。為此 protobuf?定義了 sint32 ,采用 zigzag 編碼和二。
我們看下zigzag的算法:
Zigzag 編碼用無符號(hào)數(shù)來表示有符號(hào)數(shù)字徘铝,正數(shù)和負(fù)數(shù)交錯(cuò),無論正負(fù)都可以采用較少的 byte 來表示惯吕。
4惕它、字符串
一般單個(gè)字符串的定義,需要兩個(gè)部分組成:leg+value ,如下圖所示:
leg 表示字符串?dāng)?shù)據(jù)的長(zhǎng)度废登,value是字符串真實(shí)數(shù)據(jù)淹魄,protobuf里面這個(gè)leg 采用Varint定義,一般情況下一個(gè)字節(jié)足夠了堡距。
解析速度
解析速度快甲锡,主要?dú)w功于protobuf對(duì)message 沒有動(dòng)態(tài)解析,沒有了動(dòng)態(tài)解析的處理序列化速度自然快了羽戒。就比如xml 缤沦,獲取文件之后,還需要解析標(biāo)簽易稠、節(jié)點(diǎn)缸废、字段,每一個(gè)都需要遍歷,而protobuf不需要企量,直接將field裝入流测萎。
我們知道.proto文件定義了整個(gè)message的結(jié)構(gòu),但這只是一個(gè)定義的配置文件届巩,結(jié)合compiler的使用硅瞧,單個(gè)message的read和write代碼已經(jīng)被生成,無需再基于配置文件解析恕汇,直接操作field到二進(jìn)制流里面零酪,這個(gè)速度就好比你直接操作IO一樣快,沒有其他代價(jià)拇勃。
我們看下生成的message代碼(還是LoginMsg)
write
read
我們看到四苇,read的代碼中tag的生成(轉(zhuǎn)換為int)也已經(jīng)幫你處理,所以都是基于流直接操作方咆。
兼容性
兼容性什么意思月腋,就是說message需要支持向上兼容,不能說單個(gè)message的升級(jí)瓣赂,就會(huì)導(dǎo)致old message解析出錯(cuò)榆骚,這是我們不能忍受的,開發(fā)過程中煌集,需求總變妓肢,誰都無法保證協(xié)議不會(huì)有變更。
比如這種場(chǎng)景下:message 需要增加一個(gè)字段苫纤,如若client沒有升級(jí)碉钠,sever升級(jí)了,此時(shí)client 請(qǐng)求的message格式必定是old 格式卷拘,server 采用的new message來解析喊废,此時(shí)會(huì)出現(xiàn)找不到新字段的問題,流數(shù)據(jù)錯(cuò)亂之后栗弟,后續(xù)的數(shù)據(jù)都會(huì)亂污筷。
那么protobuf是如何處理?這時(shí)候fieldNumber就派上用途了乍赫,看似可又可無的設(shè)計(jì)瓣蛀,其實(shí)包含很大用處。
fieldNumber 為每個(gè)field定義一個(gè)編號(hào)雷厂,其一保證不重復(fù)惋增,其二保證其在流中的位置。如若當(dāng)前數(shù)據(jù)流中有某個(gè)字段罗侯,而解析方?jīng)]有相關(guān)的解析代碼器腋,解析放會(huì)直接skip 吊這個(gè)field,而且讀數(shù)據(jù)的position也會(huì)后移钩杰,保證后續(xù)讀取不出問題纫塌。
如上,線讀取tag讲弄,某個(gè)字段沒有被賦值措左,就沒有這個(gè)字段的tag,解析方不處理避除,如若有某個(gè)字段怎披,而沒有解析方法,就skip了瓶摆,不影響消息的處理凉逛。老數(shù)據(jù)依然被獲取。