新手问题 在 go websocket server 与 javascript websocket client 交互中使用 flatbuffers

tsingson · 2020年01月04日 · 563 次阅读



> 代码在 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>

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册