1. 程式人生 > >google protocol buffer——protobuf的問題及改進一

google protocol buffer——protobuf的問題及改進一

這一系列文章主要是對protocol buffer這種編碼格式的使用方式、特點、使用技巧進行說明,並在原生protobuf的基礎上進行擴充套件和優化,使得它能更好地為我們服務。 在上一篇文章中,我們完整了解了protobuf的編碼原理,那麼在這篇文章中,我將會展示在使用過程中遇到的問題,以及解決方案。並在此基礎上根據我們實際的使用場景進行改進。 本文主要涉及以下2個部分 1.protobuf的使用背景及所遇到的問題 2.自己完成一個protobuf的編碼、解碼類庫,相容官方的編碼過程 ### protobuf的使用背景 我在日常工作中是進行APP服務端開發的,服務端與客戶端的資料互動格式使用的是最常用的json。 眾所周知,在移動網際網路的使用場景下,單次請求耗時對於使用者來說是一個非常敏感的資料指標,而影響單次請求耗時的因素有很多,其中最重要的自然是服務端的資料處理能力與網路訊號的狀態。服務端的處理資料處理能力是完全在我們自己的掌控之中,可以有很多方法提高響應速度。然而使用者的網路訊號狀態是我們無法控制的,也許是3G訊號,也許是4G訊號,也許正在經過一個隧道,也許正在地下商場等等。如果我們能降低每一次網路請求的資料量,那麼也算是在我們所能掌控的範圍內去優化請求響應時長的問題了。 在我接觸到protobuf之後,瞭解到其編碼後的位元組數量會比json小許多,就開始思考有沒有可能在移動網際網路場景下使用protobuf代替json格式。網上搜索了一下之後發現並沒有相關內容,於是就著手以自己工作中的APP為基礎進行protobuf的實際應用探索。(當然grpc也是一種選項,不過改造成本比較大,我這裡只考慮對編碼方式進行改進) ### 使用階段一:直接使用原生類庫 在第一階段中,自然是考慮直接使用google提供的各版本類庫。在服務端和android端使用的是java版本的類庫,而ios端使用的是swift類庫。 在系列的第一篇文章中,已經展示java類庫的使用流程。在此過程中我們會發現,我們定義好.proto檔案後,需要使用google提供的編譯器來生成相應的.java模型檔案。而即使是一個簡單的模型都會生成一個龐大的.java檔案,原因在之前編碼原理的文章中都有提及,即protobuf為了減少編碼後的位元組數,拋棄了很多資料相關的資訊(因此protobuf是一個不可以自解釋的編碼方式),**因此為了實現資訊的正確編碼和解碼,資訊的傳送方和接收方都必須擁有同一個定義好的.java檔案,該java檔案需要包含完整的編碼解碼邏輯** 對於服務端來說,模型檔案的大小並不是一個大的問題,然而對於android客戶端來說,這卻是非常致命的。在移動網際網路場景下,單次請求的時長對於使用者來說很敏感,而客戶端的大小對於使用者來說也是一個不可忽略的問題。特別在很多線下業務推廣場景下,需要客戶當場下載APP,此時客戶端的下載速度將會極大地影響推廣的成功率(想象一下,如果一個app有200MB,在非wifi情況下,很多使用者應該都會猶豫的吧。即使在wifi情況下,1分鐘下載完畢和2分鐘下載完畢對於使用者的體驗上也是天壤之別)。 在我的實際使用中,僅僅一個略複雜的.java模型檔案會達到800kb!!而整個APP包含的模型檔案何止百個,如果完全使用原生類庫,android客戶端的大小將成為一個災難。 而對於ios客戶端來說,情況相對好一些,不過類庫本身的大小也達到了10MB,基於同樣的原因,這也並不是一個可以接受的方案。 因此需要解決的第一個問題就是原生類庫大小的問題。 ### 原生類庫大小解決方案 首先,我們需要分析protobuf官方.java檔案巨大的原因。 正如之前提到的,因為protobuf是一個不可自解釋的資料格式,特別是不同的資料內容編碼後的結果可以是完全相同的(參見上一篇文章最後的例子),所以需要在編譯器生成的.java檔案中包含定製的編碼、解碼邏輯,以將相同的編碼結果對應到不同的java型別上。 我們摘取一段protobuf生成的.java檔案中的分支程式碼,其中的tag正是表示序號和型別的位元組,所以在編碼與解碼的時候就是根據這個位元組的值進入不同的case分支,進行資料的讀取和寫入。所以對於protobuf的官方類庫而言,表示序號和型別的位元組是靈魂,因為這個位元組一旦發生了變化,編碼的結果將完全不同。 ```java ... int tag = input.readTag(); switch (tag) { case 0: done = true; break; case 8: { age_ = input.readInt32(); break; } case 16: { hairCount_ = input.readInt64(); break; } case 24: { isMale_ = input.readBool(); break; } case 34: { java.lang.String s = input.readStringRequireUtf8(); name_ = s; break; } ... } ... ``` 並且為了實現跨平臺、跨語言地使用,protobuf所依賴的模型定義是.proto檔案,而.java檔案僅僅是根據.proto定義所生成的,並非是模型的原始定義。為了擺脫.proto的束縛,我們還必須將模型的定義直接放到.java檔案中。 例如我們原先定義.proto檔案如下 ```protobuf 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; } ``` 現在直接將其定義到.java檔案中,且拋棄了outer_classname ```java package cn.tera.protobuf.model; public class Person { String name; int id; String email; } ``` 接著就需要考慮這樣一個問題,之前一直在強調,.proto中定義的欄位的序號和型別是protobuf的靈魂,然而此時我們同時拋棄了.proto的定義和編譯器生成的定製化.java檔案,那又該如何去確定欄位的序號和型別呢? **答案是依賴定義的java模型本身。** **java語言自身其實就是一個強型別的語言,它在編碼和解碼的過程中,完全可以知曉每一個欄位的資料型別,而不需要根據.proto檔案生成各種定製的邏輯。** **而序號問題我們可以通過一些約定,例如欄位名的小寫字母順序進行排序。** 既然解決了protobuf的核心依賴問題,那麼接著就可以著手編寫編碼和解碼的類庫了 先看編碼部分的功能,我們將其定義為BasicEncoder。 ```java public class BasicEncoder { } ``` 在使用的時候為了簡化和直觀,我們定義入口方法的形式如下 ```java public class BasicEncoder { public