WebSocket知识盘点

5/6/2023

#建立WebSocket

在客户端创建一个WebSocket应用程序其实很简单,只需要简单的一行,例如:

const ws = new WebSocket('ws://localhost:8080');
1

语法也就两个参数

webSocket = new WebSocket(url, protocols);
1

#url

需要连接的服务器地址,一般以ws或者wss开头,这两种不同的协议可以类比为httphttps的差异

#疑问一

WebSocket自定义一套协议机制,那为什么不采用http协议呢?

WebSocket的连接协议也可以用于浏览器之外的场景,可以通过非 HTTP 协商机制交换数据。

#疑问二

如果我设置一个错误协议的 URL 或者是一个正确协议但不存在的 URL 分别会出现什么错误提示?

这里手动测试一下就行,例如:

错误协议的 URL:

new WebSocket('http://localhost')
1

image

正确协议但不存在的 URL:

const ws = new WebSocket('ws://localhost:555');

ws.addEventListener('error', event => {
    console.log(event);
});

ws.addEventListener('close', event => {
    console.log(event.code, event.reason);
});
1
2
3
4
5
6
7
8
9

image

#protocols

可以是一个协议字符串或一个协议字符串数组

#疑问一

这个协议名的约束是啥?这里看RFC 规范(opens new window)

The request MAY include a header field with the name |Sec-WebSocket-Protocol|. If present, this value indicates one or more comma-separated subprotocol the client wishes to speak, ordered by preference. The elements that comprise this value MUST be non-empty strings with characters in the range U+0021 to U+007E not including separator characters as defined in [RFC2616] and MUST all be unique strings. The ABNF for the value of this header field is 1#token, where the definitions of constructs and rules are as given in [RFC2616].

请求可能会包含一个叫Sec-WebSocket-Protocol的请求字段。如果这个字段存在,那么根据需要客户端可以使用单个或者多个以逗号分隔的子协议。组成这个值的元素必须是非空字符串,字符在 U+0021 到 U+007E 范围内,不包括 [RFC2616] 中定义的分隔符并且必须都是唯一字符串。这个头字段值的 ABNF 是1#token,其中的定义构造和规则在 [RFC2616] 中给出。

其中 RFC2616 中定义的分隔符为,如图所示:

image

如果协议中包含了这些分隔符会出现什么场景,如下图所示:

image

解释:

  1. U+0021 到 U+007E之间的字符是啥?

可以参考维基百科(opens new window),由此可以得出中文名是不符合规范的,它不在这个范围内

  1. ABNF是什么?

Augmented Backus–Naur form的缩写,这里可以参考维基百科(opens new window)

  1. 1#token是什么?

WebSocket协议中,1#token是一个ABNF规则,用于定义一个或多个标记(token)的序列。其中,#表示“至少一个”,1#表示“至少一个标记”。标记是一种由可见字符组成的字符串,用于表示协议中的各种元素,例如请求头字段、响应头字段、URI等。标记由一系列字符组成,其中每个字符都必须是以下范围内的字符:U+0021到U+007E:ASCII可见字符,不包括空格和分隔符。在1#token规则中,标记之间用逗号分隔。例如,以下是一个符合1#token规则的字符串:token1,token2,token3,在这个字符串中,有三个标记,它们之间用逗号分隔。

RFC2616(opens new window)中也有关于1#element的作用

#疑问二

这个协议有什么作用呢?

RFC6455(opens new window)中说明了协议的使用,但是这个章节在一开始也说明了这个使用规范是非标准,因此这个协议的用途其实可以多元化,这个协议的目的是一个服务器可以根据指定的protocol来应对不同的互动情况

下面列举了几种用途:

  1. 协议为多个域名,为不同网站提供服务

例如一个WebSocket服务器为两个不同网站提供服务:

const ws = new WebSocket('ws://localhost:80', 'chat.example.com');
const ws1 = new WebSocket('ws://localhost:80', 'chat.example.org');
1
2

或者给不同版本的网站提供服务:

const ws = new WebSocket('ws://localhost:80', 'bookings.example.net');
const ws1 = new WebSocket('ws://localhost:80', 'v2.bookings.example.net');
1
2
  1. 作为消息格式,在http协议中的Content-Type能够返回不同的响应内容来代表不同的数据格式,例如application/jsontext/html,那么这个参数也可以用来表示期望的数据内容,例如可以设置jsontext
  2. 作为加解密key,比如使用这个协议的key作为加密的密钥

总而言之:这个protocol可以根据需要来动态设置

#实例属性

#protocol

服务器支持的协议,如果未指定协议,那么返回为空

const ws = new WebSocket('ws://localhost:80');
ws.addEventListener('open', () => {
    console.log(ws.protocol === ''); // true
});
1
2
3
4

如果选择了一种协议

// 服务器
// 例如使用ws包进行选择
const wss = new WebSocketServer({
    port: 80,

    // 选择最后一种协议
    handleProtocols: (protocols) => {
        return Array.from(protocols).pop() || '';
    }
});

// 客户端
const ws1 = new WebSocket('ws://localhost:80', ['a', 'b']);
ws.addEventListener('open', () => {
    console.log(ws.protocol === 'b'); // true
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

#url

创建WebSocket实例对象的URL注意这个返回的url是符合规范的

例如:

  1. 自动补全:
// 虽然后缀未加/,但是获取的时候会自动加上
const url = 'ws://localhost:55';
const ws = new WebSocket(url);

ws.addEventListener('open', () => {
    console.log(ws.url); // ws://localhost:55/
});
1
2
3
4
5
6
7
  1. 默认端口不显示:
const url = 'ws://localhost:80/';
const ws = new WebSocket(url);

ws.addEventListener('open', () => {
    console.log(ws.url); // ws://localhost/
});
1
2
3
4
5
6

#readyState

当前WebSocket的连接状态

  • 0(WebSocket.CONNECTING): 正在连接中
  • 1(WebSocket.OPEN): 已经连接并且可以通信
  • 2(WebSocket.CLOSING): 正在关闭
  • 3(WebSocket.CLOSED): 连接关闭或者连接未成功
const url = 'ws://localhost:80/';
const ws = new WebSocket(url);

ws.addEventListener('open', () => {
    console.log(ws.readyState); // 1

    // 0, 1, 2, 3
    console.log(WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED);
});
1
2
3
4
5
6
7
8
9

#binaryType

返回WebSocket连接所传输的二进制数据类型

存在两种:blob(默认值)、ArrayBuffer

const url = 'ws://localhost:80/';
const ws = new WebSocket(url);

ws.addEventListener('open', () => {
    console.log(ws.binaryType); // blob
});
1
2
3
4
5
6

这个值由客户端自己决定

const url = 'ws://localhost:80/';
const ws = new WebSocket(url);

ws.binaryType = 'arraybuffer';

ws.addEventListener('open', () => {
    console.log(ws.binaryType); // arraybuffer
});
1
2
3
4
5
6
7
8

#bufferedAmount

是一个只读属性,用于返回已经被send()方法放入队列中但还没有被发送到网络中的数据的字节数。一旦队列中的所有数据被发送至网络,则该属性值将被重置为 0。但是,若在发送过程中连接被关闭,则属性值不会重置为 0。如果你不断地调用send(),则该属性值会持续增长

那么这个属性有什么用呢?这里问问ChatGPT

image

整体思路也就是有一个缓冲区概念,如果当前正在发送数据,那么放入缓冲区中,否则直接发送数据

#extensions

输出服务端已选择的拓展值,具体可以查看RFC6455 Extensions
(opens new window)

值得注意的是这个值由客户端决定需要哪些拓展值,如果客户端不存在,那么服务端就不应该去设置

目前我Chrome中默认存在两种拓展值,permessage-deflateclient_max_window_bits

#perMessageDeflate

使用deflate(opens new window)压缩技术压缩消息,

// 服务端
const wss = new WebSocketServer({
    port: 80,
    perMessageDeflate: true
});
1
2
3
4
5

image

#client_max_window_bits

用于控制客户端压缩拓展的最大窗口大小

const wss = new WebSocketServer({
    port: 80,
    perMessageDeflate: {
        clientMaxWindowBits: 8
    }
});
1
2
3
4
5
6

#实例方法

#close

关闭WebSocket,语法为WebSocket.close(code, reason)

#code

数字状态码,它表示的是WebSocket关闭后打印出的值,在关闭事件CloseEvent内获取,未赋值的情况下默认1005

  1. 0-999:未使用状态码
  2. 1000-2999:有具体定义的含义和未来可能被定义的保留值
  3. 3000-3999:提供给库和框架使用
  4. 4000-4999:提供给用户自定义

具体各个数字代表的含义可以看RFC6455-The WebSocket Protocol(opens new window)

#reason

连接关闭的原因,最大字符串长度不能超过 123 个字节

如果超过了会发生什么事?使用代码测试一下,结果如下图所示:

image

显而易见,直接抛出异常

#举几个案例

  1. 未建立连接之前或正在建立连接之前关闭

最终打印出1006,具体原因在另一篇WebSocket疑问解答(opens new window)给出

const ws = new WebSocket('ws://localhost:80/');
ws.addEventListener('close', event => {
    // 1006, ''
    console.log(event.code, event.reason);
});
ws.close();
1
2
3
4
5
6
  1. 建立连接后主动关闭

其中1005表示表示没有收到预期的状态码,如果手动加上状态码和原因,那么输出的就是手动赋值的状态码以及原因

const ws = new WebSocket('ws://localhost:80/');

ws.addEventListener('open', () => {
    ws.close();
});

ws.addEventListener('close', event => {
    // 1005, ''
    console.log(event.code, event.reason);
});
ws.close();
1
2
3
4
5
6
7
8
9
10
11
  1. 服务器代码主动断开

客户端打印1005,如果手动加上状态码和原因,那么输出的就是手动赋值的状态码以及原因

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({
    port: 80,
});

wss.on('connection', (ws, req) => {
    ws.close();
});
1
2
3
4
5
6
7
8
9
  1. 服务器进程关闭

手动把开启的服务器进程杀掉,那么客户端显示的失败状态码为1006,原因为空字符串

#send

发送消息,语法为WebSocket.send(data)

#data

一共有四种类型可以作为传输数据

  1. string
ws.send('Hello World!');
1
  1. ArrayBuffer
ws.addEventListener('open', () => {
    const buffer = new ArrayBuffer(12);
    const view = new DataView(buffer);
    const arr = [
        72, 101, 108, 108, 111,
        32, 87, 111, 114, 108,
        100, 33
    ];
    arr.forEach((num, index) => {
        view.setUint8(index, num);
    });
    ws.send(buffer);
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. Blob
// 客户端
ws.addEventListener('open', () => {
    const message = new Blob(['Hello World!']);
    ws.send(message);
});

// 服务器
wss.on('connection', (ws, req) => {
    ws.on('message', (data) => {
        console.log(data.toString()); // Hello World!
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
  1. TypedArray或者DataView
// 客户端
ws.addEventListener('open', () => {
    const message = new Uint8Array([
        72, 101, 108, 108, 111,
        32, 87, 111, 114, 108,
        100, 33
    ]);
    ws.send(message);
});

// 服务端
wss.on('connection', (ws, req) => {
    ws.on('message', (data) => {
        console.log(data.toString()); // Hello World!
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

或者

ws.addEventListener('open', () => {
    const view = new DataView(new ArrayBuffer(12));
    const arr = [
        72, 101, 108, 108, 111,
        32, 87, 111, 114, 108,
        100, 33
    ];
    arr.forEach((num, index) => {
        view.setUint8(index, num);
    });
    ws.send(view);
});
1
2
3
4
5
6
7
8
9
10
11
12

#事件

和其他事件一致,有两种声明方式

addEventListener('open', (event) => {});

onopen = (event) => {};
1
2
3

#open

WebSocket处于open状态,即readyState1时触发

该打开事件参数为普通的Event(opens new window)

#message

WebSocket接收到消息触发

该消息事件参数为MessageEvent(opens new window),继承于普通的Event(opens new window),相比于普通的 Event,该事件多了五个参数

  • data:接收到的消息
  • origin:发送消息的来源
  • lastEventId:事件唯一ID
  • source:消息发送者
  • ports:一组MessagePort(opens new window)对象,表示与发送消息的通道关联的端口

依次打印出来如图所示:

image

注意:

在这里其实可以不用关注lastEventId,因为HTML规范(opens new window)中说明了它为SSE(opens new window)提供

sourceports也可以不用关注,具体看HTML规范(opens new window)

#error

WebSocket的连接由于一些错误事件的发生 (例如无法发送一些数据) 而被关闭时,一个error事件将被引发

该关闭事件参数为普通的Event(opens new window)

#close

WebSocket连接关闭后被触发,即readyState3时触发

该关闭事件参数为CloseEvent(opens new window),继承于普通的Event(opens new window),相比于普通的Event,该事件多了三个参数

  • code: 关闭连接的状态码
  • reason: 关闭连接的原因
  • wasClean: Boolean,连接是否完全关闭

前两个参与之前已经介绍过,这里说一下wasClean,如果是代码手动close,那么一般为true,那么什么场景下为false呢?

  1. 突然断开服务器进程
  2. 连接未建立时手动调用close方法关闭

#参考文章

Last Updated:5/25/2024, 2:23:06 AM