天天看點

運用google-protobuf的IM消息應用開發(前端篇)

  公司原本使用了第三方提供的IM消息系統,随着業務發展需要,三方的服務有限,并且出現問題也很難處理和排查,是以這次新版本疊代,我們的server同僚嘔心瀝血做了一個新的IM消息系統,我們也是以配合做了一些事情。 對于前端來說,被告知需要用到protocol buffer,什麼gui?最開始我一直沒弄懂到底是個什麼東西,感覺和平時接觸的技術差别比較大。 還有二進制什麼的,以前感覺從來就沒在前端使用過。 久經波折,這次的旅途學到了很多東西,是以作此部落格。

  簡稱protobuf,google開源項目,是一種資料交換的格式,google 提供了多種語言的實作:php、JavaScript、java、c#、c++、go 和 python等。 由于它是一種二進制的格式,比使用 xml, json 進行資料交換快許多。以上描述太官方不好了解,通俗點來解釋一下,就是通過protobuf定義好資料結構生成一個工具類,這個工具類可以把資料封裝成二進制資料來進行傳輸,在另一端收到二進制資料再用工具類解析成正常的資料。

  1.json占用流量大,用了protobuf的二進制傳輸會幫助傳輸更輕量,節約使用者和服務端流量 。之前舊消息系統使用json的時候發現,當一台伺服器通路量很大的時候,cpu占用很低,但是帶寬已經滿了,伺服器承載量也就滿了。

  2.json太随意太靈活了,字段想加就加,PM提的需求後來維護的人不考慮什麼就加上去, 搞亂架構,用protobuf,一看到這東西,就會謹慎。

  

  3.生成代碼對于除了javascript來說的其它語言,真的就是福利,用json的話,後端要寫好多類,去把對象解析到類上,然後遇到有子對象,還要再解析,都是體力活,傳上來一個protobuf,後端拿到就可以decode了,不用再關心類型,不用再去挨個判斷做解析,很開心啊,另外,雖然php也是弱類型,拿到json也可以decode成數組或stdClass,但那個沒有意義,不是具體的業務實體, 代碼依舊很難維護。(對于JavaScript這樣的語言來說,确實不是福利,用json會更好操作,引入編譯和解析二進制資料不僅增加了工作量,還要對protobuf生成的js再次做一次封裝友善業務開發,加大了業務複雜度)

  4.有一定安全性, 傳輸過程中是二進制,抓包是看不出具體資料的,并且是自己定義protobuf生成的代碼,才能正确解析出資料(有點類似于對稱加密)。 前端代碼壓縮過,即便想通過前端來看懂,也比較費勁,隻能說比一般的json安全性高一些。

  1.可讀性比較差,需要通過工具來解析

  2.對于JavaScript,沒有json友好,會增加解析和封裝的工作量。

  對于web前端來說,用它主要是能為使用者節約流量,但是業務代碼變得多了一層,會複雜一點,但是确實對傳輸性能做了很大提升。下面我們來看看大緻的使用流程。使用protobuf,你需要安裝protobuf編譯器,然後通過.proto檔案配置自定義的資料格式,如下代碼(person.proto):

  定義了人的類,有三個描述變量。通過protobuf編譯器,把目前配置的類編譯成你所需要語言的代碼。 比如編譯成JavaScript,這個時候會生成一個js檔案,我們重命名就叫person.js吧,裡面的代碼依賴google-protobuf,是以我們要先npm google-protobuf,然後通過webpack或者browserify之類的打包工具把 google-protobuf 引入到目前 person.js 中,最後再引入到我們的工程中。

  定義的person,前端要使用的話大緻代碼如下:

  但是,這樣就顯得非常難以使用了,甚至資料類型很多,結構也都不一樣,如果每次收發一個消息都要這樣去處理的話,太麻煩了。這裡需要進行一層封裝處理,友善業務使用,封裝後使用大概如下代碼:

   假如我們後端是php,前端是web,protobuf生成一個兩個語言的工具類,互相通信都要通過各自的類解析和封裝,如下圖:

運用google-protobuf的IM消息應用開發(前端篇)

  實際是我們會有三個前端:

運用google-protobuf的IM消息應用開發(前端篇)

   一個消息系統是需要長連接配接的,前端需要随時接收消息,APP使用了tcp長連接配接,前端就是websocket了。 websocket也是基于tcp的,相當于在tcp基礎上封裝了一層。 某種程度來說tcp的性能優于websocket,因為websocket就是在tcp的基礎上多了一層轉化,但是websocket使用更簡單,用tcp的app端需要自己去讀tcp流,根據標頭和包體組裝資料包,而websocket不需要,因為websocket會是一個整包的資料并不是流的形式。 具體來說,後端通過緩存區把資料沖刷(flush)給前端,app端拿到tcp資料流,需要根據消息頭給定的消息體長度,去拿取後面多少位的資料,然後組裝成一個資料包。 而websocket傳輸過來就是一個個的包,也就是幀并不是資料流,是以後端在給websocket資料的時候,必須要把一個整包,在緩沖區一次性沖刷過來,而給tcp的話就可以自由沖刷。

(引用)概念上,WebSocket确實隻是TCP上面的一層,做下面的工作:

  為浏覽器添加web 的origin-based的安全模型。

  添加定位和協定命名機制來支援在同一個端口上提供多個服務和同一個IP上有多個主機名。

  在TCP上實作幀機制,來回到IP包機制,而沒有長度限制。

  在帶内包含額外的關閉握手,是為了能在有代理和其他中間設施的地方工作。

  前端也許很少會接觸到二進制,至少我沒怎麼接觸過。 之前說的二進制傳輸,通過設定websocket對象的binaryType屬性: binaryType = 'arraybuffer'(如果沒有配置預設傳回的是個Blob對象,protobuf解析時會報錯),消息下行的時候 onmessage 拿到的 MessageEvent.data 會是一個ArrayBuffer對象,如圖:

運用google-protobuf的IM消息應用開發(前端篇)

  關于ArrayBuffer,MDN解釋: ArrayBuffer對象被用來表示一個通用的,固定長度的二進制資料緩沖區。你不能直接操縱ArrayBuffer的内容<code>;</code>相反,你應該建立一個表示特定格式的buffer的類型化數組對象(typed array objects)或資料視圖對象<code>DataView</code> 來對buffer的内容進行讀取和寫入操作。

  類型化數組(typed array objects)有下圖這些類型:

運用google-protobuf的IM消息應用開發(前端篇)

   實際就是一個ArrayBuffer我們是不能直接操作它的,需要轉成可以操作的對象類型,我們是需要轉換成Unit8Array,比如這樣:

  但是我發現在微信裡這樣用會報錯,在手機預設的浏覽器裡還是好的,看來還存在一定相容問題。後來用到DataView才沒問題的:

  相容問題不止這一點,在phone5測試的時候,一直有問題(同僚說那台手機被蘋果封過,不曉得會不會和這個有關系),一步步查下去,發現是Unit8Array一些方法在phone5裡顯示undefined,比如 Unit8Array.slice 和 Unit8Array.from,把 Unit8Array.slice用 Unit8Array.subarray 替換,Unit8Array.from 用 new 替換,像這樣:Uint8Array.from([1, 0, 0]) == new Uint8Array([1, 0, 0]),目前來說就沒出現其他相容問題了。

  我們會封裝一個獨立的websocket類,處理websocket的建立、連接配接、重連、心跳、監聽等,提供一些鈎子函數,配合前面說的ImInstance實作業務功能。長連接配接肯定是會出現斷開或者弱網等一系類情況,保證業務的健壯和穩定性,需要做心跳重連。這塊之前的部落格已經寫過,這次項目之後又對代碼和部落格進行了一些完善,具體可以看之前的部落格《初探和實作websocket心跳重連》和心跳的github源碼《https://github.com/zimv/WebSocketHeartBeat》。

  下面兩個問題有一個知識點: Number類型統一按浮點數處理,64位(bit)存儲,整數是按最大54位(bit)來算最大最小數的,否則會喪失精度;某些操作(如數組索引還有位操作)是按32位處理的。

  1.位移運算:

   每一條消息有個唯一id,id是根據時間戳加上一些其他參數再通過位移運算得出的。 本身根據id可以得出時間,是以就沒有專門給時間的字段,這裡就需要前端對id進行一次運算,得出時間,但是我在做位移操作的時候發現得出的值不對。 後來才查到了上面的知識點。 server給我們的是64位的int,但是js的位移是按照32位處理的,是以得出的值不對,後來邱桑找到了一個Long.js庫,它可以把64位整數拆分成兩個32位的去計算,最後我就得到了正确的時間。Long.js 

  2.number丢失精度:

  因為js的整數最大隻支援到54bit,範圍在 −9007199254740992 到 9007199254740992,而我們的id是超過了54bit的(這一點受到了後端同僚的瘋狂嘲笑)。  在做消息回執(收到一條消息,發送目前消息的id給後端,告知我收到這個消息了)的時候,因為超過了js的最大值,是以前端傳出去的id就會是錯誤的。 比如後端傳回了一個id為111111111111111111的值(18個1),前端通過protobuf類解析之後拿到的值直接變成了111111111111111100(16個1加2個0),因為超過了最大值,js用0來占位顯示,這樣回執給後端的id就是111111111111111100了。 我以為目前存放數字的變量就已經是這個值了,我不管做什麼都沒用了,那麼我希望後端給我一個字元串的id我才好處理(發現這個問題的時候項目正在準備上線),但是邱桑覺得這樣多一個字段太浪費。 後來他查了一些資料告訴我,就用Long.js,它可以幫我轉換成正确的字元串,我不信,我認為js存不到那麼大的資料,js直接把資料給丢失了,而邱桑說值實際還在記憶體裡精度沒有丢失,隻是js展示不出來,而且非常肯定,我當時不信,在他強烈的要求下,我使用了Long.js的轉換方法,結果他是對的。  雖然收到的值超過了js的範圍,但是數值仍然是原封不動的在記憶體裡,這個也是被狠狠的打了一下臉,果然還是邱桑厲害!  Long.js的代碼量還是比較多,當時我想我隻用位移就把位移的相關代碼抽出來整合了一下,這樣比較節約。  後來發現我現在說的這個問題也需要用到Long.js的其它方法,我又嘗試抽離,發現要抽的代碼太多了,後來幹脆就直接把Long.js全部引入進來了(裝逼失敗)。

ps:由于當時我們的id是18位的number,通過long.js轉換是沒有問題的。但是後面id到19位以後,所有的結果都不再正确了。js中的number超過安全限制以後,開始變得不安全,有些19位的number可以解析成功,有些不可以,當超過20位以後幾乎全部出問題。是以我們的結論是id如果可能特别長,盡量用string。

  3.微信localstorage:

  官方說退出微信賬号後,将會清空所有Cookie和localStorage。 網上有人說還有部分機型據說會出現無法存儲或者退出webview之後就會被清除(這個沒有親自做驗證)。 那麼我需要做的未讀消息狀态就沒法保證在任何情況下都能正确存取。 解決方案是後端提供一個可以讀取的接口,我去存取一個key和value,自己來維護狀态。 在h5做未讀消息狀态還真不容易,我需要在接收到消息的時候做一個判斷,如果目前使用者沒有在和某一個人的對話頁面,那麼這個人的消息肯定是未讀的,我需要總未讀計數+1,和這個人的未讀計數+1,當進到某個人的會話頁面,這個人的未讀數将被清空,第一次登陸之後還會拉取離線消息,然後把離線消息的整理一下做次統計,每次未讀消息出現變更都需要把之前的資料進行對比并且更新,頁面跳轉或者未讀出現變化的時候需要給底部tab和消息清單dom做一次狀态更新。給一個靜态圖,看下效果:

運用google-protobuf的IM消息應用開發(前端篇)

  4.websocket斷線重連把自己踢下線的問題:

  我們會避免使用者重複登入websocket,如果目前使用者第二次連接配接websocket的話 會把上一次登入的一端給踢下線,被踢下線的一端會收到一個消息,當收到踢下線的消息之後我便不會進行重連。 因為網絡原因、異常原因或者後端主動要求我重連,我便會去進行重連,但是有時候出現就在同一個地方執行了重複連接配接,實際都是自己這一個端,那麼就會出現登入上之後,又收到踢下去的消息,把自己給踢下去了,踢下去就不會再重連了,這樣就永久斷開了,這屬于邏輯沒控制好。 解決這個問題是首先要保證重連之前先主動對目前的websocket執行一次close,close的時候後端是會收到斷開的通知,這樣我們再去連接配接就不會重複登入了。

  這次自己碰到很多不熟悉的知識,也問了server同僚很多問題,學到很多,有靠譜的大牛同僚就是爽! 也出過一些bug和問題,多次反複追溯才查出問題的根源,有時候1個bug可能是幾個地方代碼寫錯造成的問題。 第一個版本已經順利上線,後面還有很多重要的工作要做,單從前端來說,還需要把封裝的websocket和ImInstance寫得更好,文檔,擴充性這些都要考慮(已經是一個公共類了,以後還會作為sdk開放給三方平台);還需要做一個監控展示,幫助實時監控伺服器CPU,帶寬,性能等。 經曆了一次大版本的疊代,加了一個月的班,熬了幾天夜,和團隊一起在進步,收獲到這麼多經驗包也是很開心的。

運用google-protobuf的IM消息應用開發(前端篇)

有沒有人打賞?沒有的話,那我晚點再來問問。

運用google-protobuf的IM消息應用開發(前端篇)

關注大詩人公衆号,第一時間擷取最新文章。

運用google-protobuf的IM消息應用開發(前端篇)

如果你有購買鋼琴的打算,可以從這裡了解到在售資訊,價格實惠品質保障。

---轉發請标明,并添加原文連結---

繼續閱讀