WebSocket

2022年10月22日

WebSocket

WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

在正常情况中,每个WebSocket都会经历一系列状态(生命周期事件):

  • 连接中(connecting):每个WebSocket的初始状态。此时若发生消息,则消息可能会被排队,直到WebSocket打开之前它们不会被传输。

  • 打开状态(open):WebSocket字已被远端接受并且完全可操作。任一方向的消息队列都立即开始传输。

  • 关闭中(closing): WebSocket的某一端启动正常关闭,WebSocket将继续传输队列中的消息,但将拒绝新消息进入队列。

  • 已关闭(closed): WebSocket已传输其所有消息并已收到来自对端的所有消息。

在异常情况下,WebSocket可能由于HTTP升级问题、连接问题或任一端异常关闭,此时WebSocket会进入canceled状态:

  • 已取消(canceled): WebSocket连接中断。任何一端队列中的消息可能尚未传输到对端。

请注意,每个端的状态过程都是独立的。到达正常关闭状态表示它已发送其所有传出消息并接收其所有传入消息,但不保证其他对端将成功接收其所有传入消息。

在CloudControl Pro中,我们通过$web.newWebSocket()来创建WebSocket并监听上述生命周期事件。

let ws = $web.newWebSocket("wss://echo.websocket.org", {
    eventThread: 'this'
});
ws.on("open", (res, ws) => {
    log("WebSocket已打开");
}).on("failure", (err, res, ws) => {
    log("WebSocket连接失败或中断");
    console.error(err);
}).on("closing", (code, reason, ws) => {
    log("WebSocket关闭中");
}).on("closed", (code, reason, ws) => {
    log("WebSocket已关闭: code = %d, reason = %s", code, reason);
});

$web.newWebSocket(url[, options])

  • url {string} WebSocket链接
  • options {object} 可选项,包括:
    • eventThread {any} WebSocket事件派发的线程,默认为io
      • io 事件将在WebSocket的IO线程触发
      • this 事件将在创建WebSocket的线程触发,如果该线程被阻塞,则事件也无法被及时派发

创建一个WebSocket对象并返回。使用这个对象来监听WebSocket生命周期事件,或发送文本、二进制消息。

WebSocket

$web.newWebSocket返回的对象。

需要注意的是,在Pro 9.0.10以前,WebSocket不会在脚本退出时自动关闭或取消,因此建议在这之前的版本监听exit事件自动取消。参见WebSocket.cancel()

WebSocket.send(text)

  • text {string} 文本消息
  • 返回 {boolean}

尝试将text加入消息队列以使用UTF-8编码作为文本数据发送。

如果消息成功加入队列,则此方法返回true。若消息缓冲区(16MB)已满,则此消息将被拒绝并触发WebSocket的正常关闭(也即关闭时会发送完当前队列中所有消息)。在这种情况下,此方法返回 false。

若此WebSocket处于已关闭、关闭中、已取消的任何其他情况下,也会返回false

此方法不会等待消息最终发送才返回,而是立即返回。

let ws = $web.newWebSocket("wss://echo.websocket.org", {
    eventThread: 'this'
});

ws.on("text", (text, ws) => {
    console.info("接收到文本消息: ", text);
});

console.log(ws.send('Hello, CloudControl Pro')); // 返回true

ws.close(1000, null);

console.log(ws.send('GoodBye')); // 返回false

WebSocket.send(bytes)

  • bytes {ByteString} 二进制消息
  • 返回 {boolean}

尝试将bytes加入消息队列以作为二进制数据发送。

如果消息成功加入队列,则此方法返回true。若消息缓冲区(16MB)已满,则此消息将被拒绝并触发WebSocket的正常关闭(也即关闭时会发送完当前队列中所有消息)。在这种情况下,此方法返回 false。

若此WebSocket处于已关闭、关闭中、已取消的任何其他情况下,也会返回false

此方法不会等待消息最终发送才返回,而是立即返回。

要创建一个二进制消息,需要使用OkHttp的API,比如:

let ByteString = Packages.okio.ByteString;

let ws = $web.newWebSocket("wss://echo.websocket.org", {
    eventThread: 'this'
});

ws.on("text", (text, ws) => {
    console.info("接收到文本消息: ", text);
}).on("binary", (bytes, ws) => {
    console.info("收到二进制消息:大小 ", bytes.size());
    console.info("hex: ", bytes.hex());
    console.info("base64: ", bytes.base64());
    console.info("md5: ", bytes.md5());
    console.info("bytes: ", bytes.toByteArray());
});

ws.send(ByteString.of($files.readBytes('./test.png'))); // 从byte\[\]创建二进制数据并发送
ws.send(ByteString.encodeUtf8('你好')); // 将字符串按UTF8编码并创建二进制数据并发送
ws.send(ByteString.decodeBase64('QXV0by5qcyBQcm8geXlkcw==')); // 解码Base64并创建二进制数据并发送
ws.send(ByteString.decodeHex('621172314F60')); // 解码hex并创建二进制数据并发送

ws.close(1000, null);

更多ByteString的信息参见ByteString.javaopen in new window

WebSocket.cancel()

立即关闭WebSocket持有的资源,丢弃整个消息队列的消息。若WebSocket已关闭或已取消,则不会执行任何操作。

let ws = $web.newWebSocket("wss://echo.websocket.org", {
    eventThread: 'this'
});

// 脚本退出时取消WebSocket
events.on('exit', () => {
    ws.cancel();
});

WebSocket.close(code, reason)

  • code {number} 关闭的状态码,参见RFC 6455 的第 7.4 节open in new window。常见的值包括:

    • 1000 表示正常关闭。
    • 1001 表示本端正在“离开”,例如服务器关闭或浏览器关闭所在页面。
    • 1002 表示因协议错误而终止连接。
    • 1003 表示因收到无法处理的数据类型而终止连接。
    • ...
  • reason {string} 关闭原因,不超过123字节的UTF-8编码数,也可以为null

  • 返回 {boolean}

尝试正常关闭此WebSocket。调用此函数后,将拒绝所有的send操作并返回false,也即不再接受新的消息;但调用函数是已经在队列中的的消息将继续传输。

若指定的code不在有效范围或者reason字符串过长,则抛出IllegalArgumentException

若WebSocket处于关闭中、已关闭、已取消状态,则返回false;否则返回true

let ws = $web.newWebSocket("wss://echo.websocket.org", {
    eventThread: 'this'
});

setTimeout(() => {
    ws.close(1000, null);
}, 5000);

未完待续

上次编辑于:
贡献者: Bruce