本节将介绍在Emscripten中使用WebSocket的方法。
WebSocket协议在2011年已成为国际标准,目前主流浏览器均已支持。与HTTP协议相比,WebSocket有如下特点:
ws
wss
为了方便测试,我们先创建一个简单的WebSocket echo服务。笔者在此选择Go语言来创建该服务,代码如下:
//ws_echo.go package main import ( "fmt" "log" "net/http" "syscall" "golang.org/x/net/websocket" "os" "os/signal" ) func main() { log.Println("ws_echo start...") wsPort := 40001 go func() { log.Println(fmt.Sprint("WebSocket:", wsPort, " Listening ...")) http.Handle("/ws_echo", websocket.Handler(webSocketHandler)) err := http.ListenAndServe(fmt.Sprint(":", wsPort), nil) if err != nil { panic("ListenAndServe: " + err.Error()) } }() httpPort := 80 go func() { log.Println(fmt.Sprint("http:", httpPort, " Listening ...")) err := http.ListenAndServe(fmt.Sprint(":", httpPort), http.FileServer(http.Dir("./"))) if err != nil { panic("ListenAndServe: " + err.Error()) } }() ch := make(chan os.Signal, 1) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) log.Printf("ws_echo quit (%v)\n", <-ch) } func webSocketHandler(ws *websocket.Conn) { ws.PayloadType = websocket.TextFrame defer ws.Close() rtemp := make([]byte, 32768) for { n, err := ws.Read(rtemp) if err != nil { log.Println("Error:Read:", err) return } n, err = ws.Write(rtemp[:n]) } }
上述代码在80端口启动了当前文件夹的静态页面服务用于发布测试用的html文件;并在40001端口的/ws_echo路径启动了WebSocket echo服务,该服务建立WebSocket连接后,会将客户端发来的数据原样发回去。安装Go语言环境后,使用下列命令启动程序:
/ws_echo
go run ws_echo.go
在JavaScript中使用WebSocket()构造函数创建WebSocket对象,WebSocket.send()方法用于发送数据,WebSocket.onmessage()属性用于指定处理接收到的数据的回调函数,例如:
WebSocket()
WebSocket.send()
WebSocket.onmessage()
//websocket_js.html var ws = new WebSocket("ws://localhost:40001/ws_echo"); ws.onopen = function(e) { console.log("ws.onopen"); ws.send("Hello world!"); } ws.onmessage = function(e) { console.log("ws.onmessage: " + e.data); ws.close(); } ws.onclose = function(e) { console.log("ws.onclose"); } ws.onerror = function(e) { console.log("ws.onerror"); }
上述代码创建了与ws://localhost:40001/ws_echo的WebSocket连接,连接成功后发送了"Hello world!",WebSocket echo服务将收到的数据发回后,ws.onmessage()将收到的数据通过日志打印:
ws://localhost:40001/ws_echo
ws.onmessage()
与5.1节介绍的XMLHttpRequest封装方法类似,WebSocket对象的C接口封装分为两个部分:
来看C部分的代码:
//ws_wrap.cpp struct WS_WRAPPER; struct WS_CB; //imp by JavaScript, call by C: EM_PORT_API(struct WS_WRAPPER*) WSNew(const char *url, struct WS_CB *cb); EM_PORT_API(int) WSSend(struct WS_WRAPPER *ws, const char *data); EM_PORT_API(void) WSDelete(struct WS_WRAPPER *ws); //WebSocket callback: class CWSCallback{ public: CWSCallback(const char *url){ m_ws = WSNew(url, (struct WS_CB*)this); } virtual ~CWSCallback(){} void OnOpen(){ printf("OnOpen\n"); WSSend(m_ws, "I love wasm!"); } void OnClose(){ printf("OnClose\n"); } void OnMessage(const char* data){ printf("OnMessage: %s\n", data); WSDelete(m_ws); m_ws = NULL; } void OnError(){ printf("OnError\n"); } struct WS_WRAPPER *m_ws; }; //imp by C, call by JavaScript: EM_PORT_API(void) WSOnOpen(struct WS_CB *cb){ if (cb == NULL) return; CWSCallback *pc = (CWSCallback*)cb; pc->OnOpen(); } EM_PORT_API(void) WSOnClose(struct WS_CB *cb){ if (cb == NULL) return; CWSCallback *pc = (CWSCallback*)cb; pc->OnClose(); } EM_PORT_API(void) WSOnMessage(struct WS_CB *cb, const char* data){ if (cb == NULL) return; CWSCallback *pc = (CWSCallback*)cb; pc->OnMessage(data); } EM_PORT_API(void) WSOnError(struct WS_CB *cb){ if (cb == NULL) return; CWSCallback *pc = (CWSCallback*)cb; pc->OnError(); } CWSCallback wscb("ws://localhost:40001/ws_echo");
与5.1节介绍的XMLHttpRequest封装方法类似,创建WebSocket连接时,我们为它绑定了一个回调对象cb,用于处理该连接的各种回调事件。这样当程序同时启动了多个WebSocket连接时,可以为每个WebSocket连接分配不同的的回调处理对象。
cb
导入库部分代码如下:
//pkg.js mergeInto(LibraryManager.library, { WSNew: function (url, cb) { return JS_WSNew(Pointer_stringify(url), cb); }, WSSend: function (ws, data) { return JS_WSSend(ws, Pointer_stringify(data)); }, WSDelete: function (ws) { return JS_WSDelete(ws); } })
导入库调用的JavaScript方法如下:
//ws_wrap.html var g_NextWSID = 1; var g_WSTable = []; function JS_WSNew(url, cb) { var ws = new WebSocket(url); ws.onopen = function (e) { Module._WSOnOpen(cb); }; ws.onclose = function (e) { Module._WSOnClose(cb); }; ws.onmessage = function (e) { Module.ccall('WSOnMessage', 'null', ['number', 'string'], [cb, e.data]); }; ws.onerror = function (e) { Module._WSOnError(cb); }; var wsid = g_NextWSID++; g_WSTable[wsid] = ws; return wsid; } function JS_WSSend(ws, data) { var ws = g_WSTable[ws]; ws.send(data); } function JS_WSDelete(ws) { var ws = g_WSTable[ws]; ws.close(); }
整套程序综合应用了导出C++对象(4.3节)、JavaScript对象注入C(4.5节)、ccall(2.7节)等技术。使用下列命令编译:
emcc ws_wrap.cpp --js-library pkg.js -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" -o ws_wrap.js
启动WebSocket echo服务,浏览页面,控制台输出如下:
出于简化代码考虑,本节给出的例子未处理反复调用WSNew()导致g_NextWSID溢出等情况。由于IO操作往往与程序逻辑强相关,实际项目中需要考虑的问题多种多样,在此无法尽述。
WSNew()
g_NextWSID
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8