在 go websocket server 與 javascript websocket client 互動中使用 flatbuffers

tsingson發表於2020-01-04



> 程式碼在 https://github.com/tsingson/fastws-example


## 0. 簡要說明
為某個開源專案增加 websocket 對接, 寫了這個示例

程式碼中 javascript 對 flatbuffers 的序列化/反序列化, 查了一天資料, 嗯哼, 最終完成了.
看程式碼吧.........

-----------
### 0.1 關於序列化/反序列化
序列化 serialized / 反序列化 un-serialized , 一般是指對像轉成二進位制序列 ( 或叫二進位制陣列), 以及逆向從二進位制轉成對像的過程, 一般用在幾個地方
1. 網元之間傳輸. 比如 RESTfull 是在 HTTP 協議 ( HTML over TCP ) 上進行互動時使用 JSON 資料格式進行序列化與反序列化; 比如 gRPC 預設採用 protobuffers 在 HTTP2 傳輸上進行資料序列化與反序列化;
2. 物件資料持久化儲存到檔案系統, 以及從檔案系統讀取到物件時;
3. 異構開發 SDK 或 API 之間互動或共享資料, 比如 go 語言呼叫底層 c++ 庫........

### 0.2 關於 flatbuffers
flatbuffers 是 google 員工在開發遊戲過程中, 仿 protobuffers 寫的一個高效能序列化/反序列化庫, 通過 IDL (介面描述語言) 定義, 通過 flatc 編譯為多種語言的介面物件序列化/反序列化的強型別庫, 支援 c++ / c / java / python / rust / js / typescript / go / swift.........

fastbuffers 的介紹, 參見 於德志 的文章 https://halfrost.com/flatbuffers_schema/, 幾篇文章寫得很細緻,精確,完整
> PS: 於德志 的技術專題介紹文章,語言簡練易懂, 配圖簡單明瞭,非常值得一讀
>
>
> 說起來, 我的英文閱讀能力還可以, 但不得不說, 訪問 於德志https://halfrost.com/tag/protocol/ 協議相關專題文章 還是很愉悅輕鬆. 謝謝了!



flatbuffers 的特點, 個人見解:

1. flatbuffers 的序列化, 慢於 protobuffers ( 約是 protobuffers 的兩倍耗時) , 與 JSON 相仿, 甚至有時慢於 json
2. flatbuffers 的反序列化, 約 10 倍快於 protobuffers, 當然也就快於 JSON 了
3. flatbuffers 在反序列化時, 是內在零拷貝, 序列化後的資料與記憶體中是一致的, 這讓 flatbuffers 序列化後的二進位制資料直接匯入記憶體, 以及從記憶體中讀取時都非常快

所以, 在一次序列化, 而多次反序列化的情況下, 以及對反序列化要求速度非常快的情況, 可以考慮選擇 flatbuffers , 想想 google 員工為遊戲而開發 flatbuffers 這一典型場景吧

### 0.3 我在哪裡使用 ( 或計劃使用 ) flatbuffers ?
在以下場景中, 我使用了 ( 或正在計劃使用) flatbuffers:

1. Sub/Pub 訂閱/釋出的訊息系統. 在某些 Sub/Pub 場景中, Pub 時序列化訊息物件, 尤其是 flatbuffers 中的 union , 挺好用. ------------- 而在 Sub 訂閱消費端, 尤其多端消費, 高效的反序列化, 可以減少最多達 1/4, 平均 1/5 左右時延 (注: 僅是個人應用場景的經驗值, 供參考)
2. 記憶體快取 ( 包括 session 會話資料) , 某些應用中的記憶體快取需要持久化, 這些記憶體快取通過併發儲存到多個檔案後, 在應用重啟時從檔案中重建快取, 非常快
3. IM 即時通訊, 以及某些情況下的 gRPC, 這個與第一條類似. 參見我以前的文章 GOIM 的架構與定製------ 事實上, 這一篇文章, 正是為定製開發的 IM 而準備. --------- 至於 gRPC , 是的, gRPC 預設的 ptotobuffers 可以用 flatbuffers 更換, 我在幾個商用專案中使用, 某商用專案中的 gRPC + flatbuffers 已經上線執行一年了.

### 0.4 flatbuffers 的重大改進

之前, flatbuffers 在序列化時程式碼很讓人著急, 但 2019 年 12 月的一個改進, 讓 flatbuffers 序列化時程式碼簡化不少

<br>flatc --gen-object-api ./*.fbs <br>

以上引數的新增, 讓 flatbuffers 序列化簡單如下:

<br>// --------------- 這是 fbs 檔案中的 IDL <br> table LoginRequest{<br> msgID:int=1;<br> username:string;<br> password:string;<br> }<br> <br>// -------------- 這是 flatc 編譯後的 go 程式碼<br>type LoginRequestT struct {<br> MsgID int32<br> Username string<br> Password string<br>}<br><br>func LoginRequestPack(builder *flatbuffers.Builder, t *LoginRequestT) flatbuffers.UOffsetT {<br> if t == nil {<br> return 0<br> }<br> usernameOffset := builder.CreateString(t.Username)<br> passwordOffset := builder.CreateString(t.Password)<br> LoginRequestStart(builder)<br> LoginRequestAddMsgID(builder, t.MsgID)<br> LoginRequestAddUsername(builder, usernameOffset)<br> LoginRequestAddPassword(builder, passwordOffset)<br> return LoginRequestEnd(builder)<br>}<br><br>// ----------- 這是我做的簡單封裝<br>func (a *LoginRequestT) Byte() []byte {<br> b := flatbuffers.NewBuilder(0)<br> b.Finish(LoginRequestPack(b, a))<br> return b.FinishedBytes()<br>}<br><br><br>//---------------- 這裡是序列化<br> l := &amp;LoginRequestT{<br> MsgID: 1,<br> Username: "1",<br> Password: "1",<br> }<br><br> b := l.Byte() // ------------- 變數 b 是序列化後的二進位制陣列<br> <br> <br>




## 1. 使用程式碼庫

示例程式碼使用了以下開源庫
* fasthttp
* fasthttp router
* fastws ---- fasthttp 實現的 websocket 庫
* flatbuffers ---- flatbuffers 高效反序列化通用庫, 用在 go 語言/javascript
* websockets/ws ---- javascript websocket 通用庫

## 1. flatbuffers IDL 示例
xone.fbs 示例來自 https://www.cnblogs.com/sevenstar/p/FlatBuffer.html, 感謝!!

<br>namespace xone.genflat;<br><br> table LoginRequest{<br> msgID:int=1;<br> username:string;<br> password:string;<br> }<br><br> table LoginResponse{<br> msgID:int=2;<br> uid:string;<br> }<br><br> //root_type非必須。<br><br> //root_type LoginRequest;<br> //root_type LoginRespons<br>

## 2. flatc 編譯程式碼

生成 javascript

<br>flatc -s --gen-mutable ./*.fbs<br>



生成 golang

<br>flatc --go --gen-object-api --gen-all --gen-compare --raw-binary ./*.fbs<br>

## 3. 主要程式碼說明

<br>./cmd/wsserver/main.go ----- websocket server <br>./cmd/wsclient/main.go ----- websocket client<br>./ws/... ------------------- websocket go code for websocket handler and websocket client <br>./jsclient/ws.js ---------- javascript client code , please check-out package.json for depends<br>




## 4. javascript 序列化/反序列化

請注意程式碼註釋中的--------- 特別注意這一行

<br>// ------------ ./jsclient/index.js<br><br>const flatbuffers = require('./flatbuffers').flatbuffers;<br>const xone = require('./xone_generated').xone; //Generated by `flatc`.<br><br>//-------------------------------------------<br>// serialized<br>//-------------------------------------------<br>let b = new flatbuffers.Builder(1);<br>let username = b.createString("zlssssssssssssh");<br>let password = b.createString("xxxxxxxxxxxxxxxxxxx");<br>xone.genflat.LoginRequest.startLoginRequest(b);<br>xone.genflat.LoginRequest.addUsername(b, username);<br>xone.genflat.LoginRequest.addPassword(b, password);<br>xone.genflat.LoginRequest.addMsgID(b, 5);<br>let req = xone.genflat.LoginRequest.endLoginRequest(b);<br>b.finish(req); //建立結束時記得呼叫這個finish方法。<br><br><br>let uint8Array = b.asUint8Array(); // ------------- 特別注意這一行<br><br>console.log(uint8Array);<br>// console.log(b.dataBuffer() );<br>//-------------------------------------------<br>// un-serialized<br>//-------------------------------------------<br>let bb = new flatbuffers.ByteBuffer(uint8Array); //-------------- 特別注意這一行<br>let lgg = xone.genflat.LoginRequest.getRootAsLoginRequest(bb);<br><br><br>console.log("username: ", lgg.username());<br>console.log("password", lgg.password());<br>console.log("msgID: ", lgg.msgID());<br><br>



## 5. golang 中對 flatbuffers 的序列化/反序列化

<br><br>// ------ ./apis/genflat/model.go<br><br>func (a *LoginRequestT) Byte() []byte {<br> b := flatbuffers.NewBuilder(0)<br> b.Finish(LoginRequestPack(b, a))<br> return b.FinishedBytes()<br>}<br><br>func ByteLoginRequestT(b []byte) *LoginRequestT {<br> return GetRootAsLoginRequest(b, 0).UnPack()<br>}<br><br><br>// ------- ./apis/genflat/model_test.go<br><br>func TestLoginRequestT_Byte(t *testing.T) {<br> as := assert.New(t)<br> // serialized<br> l := &amp;LoginRequestT{<br> MsgID: 1,<br> Username: "1",<br> Password: "1",<br> }<br><br> b := l.Byte()<br><br> // un-serialized <br> c := ByteLoginRequestT(b)<br> if l.MsgID &gt; 0 {<br> fmt.Println(" id &gt; ", c.MsgID, " u &gt; ", c.Username, " pw &gt; ", c.Password)<br> }<br><br> as.Equal(l.Password, c.Password)<br><br>}<br><br>

## 6. websocket 程式碼
<br><br>ws.onmessage = (event) =&gt; {<br> //-------------------------------------------------------------------<br> // read from websocket and un-serialized via flatbuffers<br> //--------------------------------------------------------------------<br> let aa = str2ab(event.data);<br> let bb = new flatbuffers.ByteBuffer(aa);<br> let lgg = xone.genflat.LoginRequest.getRootAsLoginRequest(bb);<br> let pw = lgg.password();<br><br> if (typeof pw === 'string') {<br> console.log("----------------------------------------------");<br><br> console.log("username: ", lgg.username());<br> console.log("password", lgg.password());<br> console.log("msgID: ", lgg.msgID());<br> } else {<br> console.log("=================================");<br> console.log(event.data);<br> }<br><br><br> // console.log(`Roundtrip time: ${Date.now() }` , ab2str(d ));<br><br> setTimeout(function timeout() {<br> //-------------------------------------------------------------------<br> // serialized via flatbuffers and send to websocket <br> //--------------------------------------------------------------------<br> let b = new flatbuffers.Builder(1);<br> let username = b.createString("zlssssssssssssh");<br> let password = b.createString("xxxxxxxxxxxxxxxxxxx");<br> xone.genflat.LoginRequest.startLoginRequest(b);<br> xone.genflat.LoginRequest.addUsername(b, username);<br> xone.genflat.LoginRequest.addPassword(b, password);<br> xone.genflat.LoginRequest.addMsgID(b, 5);<br> let req = xone.genflat.LoginRequest.endLoginRequest(b);<br> b.finish(req); //建立結束時記得呼叫這個finish方法。<br><br><br> let uint8Array = b.asUint8Array();<br><br> ws.send(uint8Array);<br> }, 500);<br>};<br><br>function str2ab(str) {<br> let array = new Uint8Array(str.length);<br> for (let i = 0; i &lt; str.length; i++) {<br> array[i] = str.charCodeAt(i);<br> }<br> return array<br>}<br><br>



## 6. 參考

* https://github.com/google/flatbuffers/issues/3781



## 7. 其他

macOS 下從原始碼編譯 flatc
<br>git clone https://github.com/google/flatbuffers<br><br>cd github.com/google/flatbuffers<br><br>cmake -G "Xcode" -DCMAKE_BUILD_TYPE=Release<br><br>cmake --build . --target install<br> <br>

更多原創文章乾貨分享,請關注公眾號
  • 在 go websocket server 與 javascript websocket client 互動中使用 flatbuffers
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章