websocket ( go srv / JS client) 使用flatbuffers 交互 (Fasthttp系列之3)

代碼在 github.com/tsingson/fa…javascript

0. 簡要說明

爲某個開源項目增長 websocket 對接, 寫了這個示例html

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


0.1 關於序列化/反序列化

序列化 serialized / 反序列化 un-serialized , 通常是指對像轉成二進制序列( 或叫二進制數組), 以及逆向從二進制轉成對像的過程, 通常用在幾個地方python

  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.........c++

fastbuffers 的介紹, 參見 於德志 的文章 halfrost.com/flatbuffers…, 幾篇文章寫得很細緻,精確,完整git

PS: 於德志 的技術專題介紹文章,語言簡練易懂, 配圖簡單明瞭,很是值得一讀github

提及來, 個人英文閱讀能力還能夠, 但不得不說, 訪問 於德志halfrost.com/tag/protoco… 仍是很愉悅輕鬆. 謝謝了!golang

flatbuffers 的特色, 我的看法:web

  1. flatbuffers 的序列化, 慢於 protobuffers ( 約是 protobuffers 的兩倍耗時) , 與 JSON 相仿, 甚至有時慢於 json
  2. flatbuffers 的反序列化, 約10倍快於 protobuffers, 固然也就快於 JSON 了
  3. flatbuffers 在反序列化時, 是內在零拷貝, 序列化後的數據與內存中是一致的, 這讓 flatbuffers 序列化後的二進制數據直接導入內存, 以及從內存中讀取時都很是快

因此, 在一次序列化, 而屢次反序列化的狀況下, 以及對反序列化要求速度很是快的狀況, 能夠考慮選擇 flatbuffers , 想一想 google 員工爲遊戲而開發 flatbuffers 這一典型場景吧typescript

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 序列化時代碼簡化很多

flatc --gen-object-api ./*.fbs 
複製代碼

以上參數的添加, 讓 flatbuffers 序列化簡單以下:

// --------------- 這是 fbs 文件中的 IDL 
  table LoginRequest{
  msgID:int=1;
  username:string;
  password:string;
  }
  
// -------------- 這是 flatc 編譯後的 go 代碼
type LoginRequestT struct {
	MsgID    int32
	Username string
	Password string
}

func LoginRequestPack(builder *flatbuffers.Builder, t *LoginRequestT) flatbuffers.UOffsetT {
	if t == nil {
		return 0
	}
	usernameOffset := builder.CreateString(t.Username)
	passwordOffset := builder.CreateString(t.Password)
	LoginRequestStart(builder)
	LoginRequestAddMsgID(builder, t.MsgID)
	LoginRequestAddUsername(builder, usernameOffset)
	LoginRequestAddPassword(builder, passwordOffset)
	return LoginRequestEnd(builder)
}

// ----------- 這是我作的簡單封裝
func (a *LoginRequestT) Byte() []byte {
	b := flatbuffers.NewBuilder(0)
	b.Finish(LoginRequestPack(b, a))
	return b.FinishedBytes()
}


//---------------- 這裏是序列化
	l := &LoginRequestT{
		MsgID:    1,
		Username: "1",
		Password: "1",
	}

	b := l.Byte()  // ------------- 變量 b 是序列化後的二進制數組
	
	
複製代碼

1. 使用代碼庫

示例代碼使用瞭如下開源庫

1. flatbuffers IDL 示例

xone.fbs 示例來自 www.cnblogs.com/sevenstar/p…, 感謝!!

namespace xone.genflat;

  table LoginRequest{
  msgID:int=1;
  username:string;
  password:string;
  }

  table LoginResponse{
 msgID:int=2;
 uid:string;
 }

 //root_type非必須。

 //root_type LoginRequest;
 //root_type LoginRespons
複製代碼

2. flatc 編譯代碼

生成 javascript

flatc -s --gen-mutable ./*.fbs
複製代碼

生成 golang

flatc  --go --gen-object-api --gen-all  --gen-compare  --raw-binary ./*.fbs
複製代碼

3. 主要代碼說明

./cmd/wsserver/main.go ----- websocket server 
./cmd/wsclient/main.go ----- websocket client
./ws/... -------------------  websocket go code for websocket handler and websocket client 
./jsclient/ws.js  ---------- javascript client code , please check-out package.json for depends
複製代碼

4. javascript 序列化/反序列化

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

// ------------ ./jsclient/index.js

const flatbuffers = require('./flatbuffers').flatbuffers;
const xone = require('./xone_generated').xone; //Generated by `flatc`.

//-------------------------------------------
//  serialized
//-------------------------------------------
let b = new flatbuffers.Builder(1);
let username = b.createString("zlssssssssssssh");
let password = b.createString("xxxxxxxxxxxxxxxxxxx");
xone.genflat.LoginRequest.startLoginRequest(b);
xone.genflat.LoginRequest.addUsername(b, username);
xone.genflat.LoginRequest.addPassword(b, password);
xone.genflat.LoginRequest.addMsgID(b, 5);
let req = xone.genflat.LoginRequest.endLoginRequest(b);
b.finish(req); //建立結束時記得調用這個finish方法。


let uint8Array = b.asUint8Array();   // ------------- 特別注意這一行

console.log(uint8Array);
// console.log(b.dataBuffer() );
//-------------------------------------------
//  un-serialized
//-------------------------------------------
let bb = new flatbuffers.ByteBuffer(uint8Array);  //-------------- 特別注意這一行
let lgg = xone.genflat.LoginRequest.getRootAsLoginRequest(bb);


console.log("username: ", lgg.username());
console.log("password", lgg.password());
console.log("msgID: ", lgg.msgID());

複製代碼

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

// ------ ./apis/genflat/model.go

func (a *LoginRequestT) Byte() []byte {
	b := flatbuffers.NewBuilder(0)
	b.Finish(LoginRequestPack(b, a))
	return b.FinishedBytes()
}

func ByteLoginRequestT(b []byte) *LoginRequestT {
	return GetRootAsLoginRequest(b, 0).UnPack()
}


// ------- ./apis/genflat/model_test.go

func TestLoginRequestT_Byte(t *testing.T) {
	as := assert.New(t)
	// serialized
	l := &LoginRequestT{
		MsgID:    1,
		Username: "1",
		Password: "1",
	}

	b := l.Byte()

	// un-serialized 
	c := ByteLoginRequestT(b)
	if l.MsgID > 0 {
		fmt.Println(" id > ", c.MsgID, " u > ", c.Username, " pw > ", c.Password)
	}

	as.Equal(l.Password, c.Password)

}

複製代碼

6. websocket 代碼

ws.onmessage = (event) => {
    //-------------------------------------------------------------------
    //   read from websocket and un-serialized via flatbuffers
    //--------------------------------------------------------------------
    let aa = str2ab(event.data);
    let bb = new flatbuffers.ByteBuffer(aa);
    let lgg = xone.genflat.LoginRequest.getRootAsLoginRequest(bb);
    let pw = lgg.password();

    if (typeof pw === 'string') {
        console.log("----------------------------------------------");

        console.log("username: ", lgg.username());
        console.log("password", lgg.password());
        console.log("msgID: ", lgg.msgID());
    } else {
        console.log("=================================");
        console.log(event.data);
    }


    // console.log(`Roundtrip time: ${Date.now() }` , ab2str(d ));

    setTimeout(function timeout() {
    //-------------------------------------------------------------------
    //   serialized via flatbuffers and send to websocket 
    //--------------------------------------------------------------------
        let b = new flatbuffers.Builder(1);
        let username = b.createString("zlssssssssssssh");
        let password = b.createString("xxxxxxxxxxxxxxxxxxx");
        xone.genflat.LoginRequest.startLoginRequest(b);
        xone.genflat.LoginRequest.addUsername(b, username);
        xone.genflat.LoginRequest.addPassword(b, password);
        xone.genflat.LoginRequest.addMsgID(b, 5);
        let req = xone.genflat.LoginRequest.endLoginRequest(b);
        b.finish(req); //建立結束時記得調用這個finish方法。


        let uint8Array = b.asUint8Array();

        ws.send(uint8Array);
    }, 500);
};

function str2ab(str) {
    let array = new Uint8Array(str.length);
    for (let i = 0; i < str.length; i++) {
        array[i] = str.charCodeAt(i);
    }
    return array
}

複製代碼

6. 參考

8. License

MIT


code by tsingson

相關文章
相關標籤/搜索