WebSocket疑问解答

224 阅读3分钟

前言

在上一篇文章借助ChatGPT我封装了WebSocket中提出来两个疑问,那么答案是啥,这里可以使用vitest进行测试,先看结果然后再分析原因

环境准备

  1. 安装依赖包pnpm install typescript vitest jsdom ws @types/ws -D
  2. 利用tsc --init命令生成tsconfig.json
  3. 配置vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    include: ['./test/**.test.ts']
  },
});

测试案例

创建test/websocket.test.ts

问题1

为什么直接使用this.websocket?.close()而没有加codereason

import { it, describe, afterAll, beforeAll } from 'vitest';
import { WebSocketServer, Server } from 'ws';

import WebSocketClient, { WebSocketEventEnum } from '../index';

const port = 8080;

let wss: Server;

beforeAll(() => {
    return new Promise(resolve => {
        wss = new WebSocketServer({ port }, resolve);
    });
});

afterAll(() => {
    wss.close();
});

describe('Question 1', () => {
    it('close', async () => {
        const socket = new WebSocketClient(`ws://localhost:${port}`, {
            timeout: 10
        });
        await new Promise(resolve => {
            socket.on(WebSocketEventEnum.open, () => {
                resolve(true);
            });

            socket.on(WebSocketEventEnum.close, (event: CloseEvent) => {
                console.log(event.code, event.reason);
                resolve(true);
            });
        });
    });
});

结果如下图所示:

websocket_close.png

从上图中可以看出只打印了1006同时并没有给出原因

假如手动加上状态码同时加上原因看测试结果:

this.websocket?.close(3000, 'no reason');

发现与之前的结果一致

因此答案为:

  1. 默认打印出的code1006reason为空字符串
  2. 如果我在代码中手动赋值,那么打印出的codereason与默认打印出的一致

问题2

  1. 如果删除了if条件会发生什么
// ...
const delay = (wait: number) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(true);
        }, wait);
    });
};

describe('Question 2', () => {
    it('delete if', async () => {
        const port = 9000;
        const socket = new WebSocketClient(`ws://localhost:${port}`, {
            reconnectionDelay: 100
        });
        let num = 0;
        await delay(5_000);
        await new Promise(resolve => {
            new WebSocketServer({ port });
            socket.on(WebSocketEventEnum.open, async () => {
                num++;
                await delay(1000);
                console.log(num);
                expect(num).toBeGreaterThan(1);
                resolve(true);
            });
        });
    }, 10_000);
});

结果如下图所示:

websocket_q2_1.png

  1. 加上if条件
// ...
describe('Question 2', () => {
    it('normal', async () => {
        const port = 1000;
        const socket = new WebSocketClient(`ws://localhost:${port}`, {
            reconnectionDelay: 100
        });
        let num = 0;
        await delay(5_000);
        await new Promise(resolve => {
            new WebSocketServer({ port });
            socket.on(WebSocketEventEnum.open, async () => {
                num++;
                await delay(1_000);
                console.log(num);
                expect(num).toBe(1);
                resolve(true);
            });
        });
    }, 10_000);
});

结果如下图所示:

websocket_q2_2.png

原因

疑问1

RFC规范中有几个地方说明了1006出现的含义

If the WebSocket Connection is Closed and no Close control frame was received by the endpoint (such as could occur if the underlying transport connection is lost), The WebSocket Connection Close Code is considered to be 1006

如果WebSocket连接关闭了并且没有收到终端的关闭控制帧(例如,如果底层传输连接丢失),WebSocket连接关闭码被视为1006

1006 | Abnormal Closure

1006 | 异常关闭

1006 is a reserved value and MUST NOT be set as a status code in a Close control frame by an endpoint. It is designated for use in applications expecting a status code to indicate that the connection was closed abnormally, e.g., without sending or receiving a Close control frame.

1006是一个保留值,终端在关闭控制帧中不能将其设置为状态码,该状态码被指定用于应用程序以表明连接异常关闭,例如,在未发送或接收关闭控制帧的情况下。

至于这个状态码为什么没有原因在规范中也给出来了

If there is no such data in the Close control frame, The WebSocket Connection Close Reason is the empty string.

如果在关闭控制帧没有数据,那么WebSocket连接关闭原因就是空数据

由上面的信息其实可以看出为什么第一个问题中的状态码为1006并且原因为空了,至于为什么设置原因还是1006,我猜测可能未连接情况下强制为1006这个状态码了,这个与规范一致,同时不会出现行为一致但状态码不一致问题

那么为什么在disconnect中又加上codereason呢,因为手动调用disconnect一般都是在连接后,因此连接后在方法中加的参数是能够在函数中打印出来的

如下图所示:

ws_disconnect.png

注意: 如果在连接之前手动调用,那么状态码还是1006

疑问2

通过测试其实不难发现,在未连接服务器时存在超时时间,在使用代码进行关闭后this.websocket?.close()会触发error事件,然而这个时候服务器并没有关闭,即使websocket设置为null,但是没有移除事件,过了一段时间会触发close事件,因此造成创建的服务器为多个

解决方案:

  1. 要么如前文提到的加上if判断,等服务器真正关闭后再重连
  2. 注册事件的时候使用addEventListener进行注册,在websocket设置为null之前进行使用removeEventListener事件移除,那么原先创建的实例就不会触发注册的事件了

关联阅读

下一篇介绍WebSocket一些知识点,敬请期待