前端使用 WebSocket 的四大注意事项(线上踩坑,含泪分享)

15,773 阅读5分钟

背景

我是公众号「线下聚会游戏」的作者,开发了一些联机桌游网页(斗地主、五子棋等),可以和朋友联机玩游戏,不用下载就能一起玩。

其中的联机功能,就是通过WebSocket实现的,中途也踩了一些坑,分享给大家。

ws.close()有个参数,最好填上

这个参数就是错误码,表明了关闭连接的原因:

image.png

WebSocket断开连接时,会发送一个错误码给另一方。如果是浏览器主动断开连接,浏览器发错误码给服务器。如果是服务器断开连接,服务器发错误码给浏览器。

所有错误码可参考 MDN: CloseEvent Code

在浏览器中,调用ws.close()函数关闭连接时,默认错误码是1005,含义是 no status code was provided even though one was expected。

这是容易犯错的,可能很多人认为它的默认值是1000(正常关闭)。结果服务器收到的却是1005。

解决

如果前端关闭是正常关闭,你可以使用ws.close(1000)

如果前端关闭不是正常关闭,你需要自定义一个异常错误码,范围是4000-4999。

此外,如果你在开发一个框架,那么你可用的错误码范围是3000-3999。

如果接收的数据是二进制,一定要设置ws.binaryType

image.png

ws.binaryType有2种值:blobarraybuffer

blob是它的默认值。

如果你收到了二进制数据:当ws.binaryTypeblob时,event.data是Blob类型,你需要调用await event.data.arrayBuffer()获取ArrayBuffer类型的数据。

如果你收到了二进制数据,当ws.binaryTypearraybuffer时,event.data是ArrayBuffer类型。

踩坑点

我的《联机桌游合集》刚上线时,有个使用iOS的朋友告诉我,她无法进入游戏,重试了多次也不行。

但是我已经用我手头的安卓、iPad、iPhone、Mac、Windows全都测试过一遍了。

经过排查,才发现是她的iOS14中Safari浏览器搞的鬼。虽然我没有设置ws.binaryTypearraybuffer, 但是因为Safari检测到是二进制数据,就直接把event.data转换为了ArrayBuffer类型,不是Blob类型,导致我调用await event.data.arrayBuffer()时出错了。

回顾当时的commit记录:

image.png

解决

如果你ws收到的数据都是二进制格式,在调用const ws = new WebSocket()后,立马设置ws.binaryType = 'arraybuffer'

但是如果你ws可能收到二进制数据,也可能收到文本数据,建议参考MDN官方案例,设置ws.binaryTypearraybuffer,但是加个条件判断:

const ws = new WebSocket("ws://localhost:8080");
// Change binary type from "blob" to "arraybuffer"
ws.binaryType = "arraybuffer";
ws.onmessage = (event) => {
    if(event.data instanceof ArrayBuffer) {
        // binary frame
        console.log(event.data);
    } else {
        // text frame
        console.log(event.data);
    }
});

关于消息合并与拆分

我在Mac环境下,使用Safari浏览器和Chrome浏览器的WebSocket,有2种不同的现象:

如果后端发送给前端的消息中,包含了\n换行符。在Chrome中,会触发多次onmessage事件,各个消息是被Chrome基于\n分割开了,分割后的消息按顺序依次触发onmessage来处理。在Safari中,只触发了一次onmessage事件,Safari没有帮我们分隔消息。

事实上,在WebSocket消息中,\n换行符本身就是区分消息的特殊符号。如果需要短时间内连续发送多条消息给客户端,一种常见的优化手段就是把这些消息一次性发送过去,用\n分割。

Chrome做的很好,帮我们分割好了。但是像Safari这种浏览器没有帮我们分割,为了兼容性,我们也需要处理下。

解决

如果后端有「批量发送」的机制,就在onmessage事件中,把消息按\n分割后,再依次处理。如果后端没有实现「批量发送」的机制,则可以忽略。

ws.onmessage = (event) => {
  event.data.split('\n').forEach((message) => {
    // 处理各个message
  });
};

关于多个ws实例并发

这里的坑不是特别大,但如果你要做压力测试,那就可能会遇到坑。你需要知道:

Chrome有个特点:如果你同时建立多个WebSocket连接,只会一个一个建立。等前一个ws建立连接成功,后一个ws开始建立连接。

引申阅读:这不是Chrome的特点,而是在 RFC6455 中规定的:

If the client already has a WebSocket connection to the remote host (IP address) identified by /host/ and port /port/ pair, even if the remote host is known by another name, the client MUST wait until that connection has been established or for that connection to have failed. There MUST be no more than one connection in a CONNECTING state.

更多细节,详见 RFC6455 Page 14、Page 15。

如果你想测试后台服务同时被多个客户端连接,是否存在并发问题时,不要用同一个Chrome Tab来测。可以开多个Tab和多个浏览器,或者用Safari测试,也可以用NodeJS来测试。

因为在Safari上:如果你同时建立多个WebSocket连接,是同时发送ws连接请求的(当然注意ws同时连接数有上限,做压测时,一个Tab没必要一次性连太多,是没用的)。

我在写文章《多房间的聊天室(六)为什么要加锁?不加锁行不行啊?》时,发现了这个问题。

写在最后

我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,联系我,交个朋友),转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》《极致用户体验》