#建立WebSocket
在客户端创建一个WebSocket
应用程序其实很简单,只需要简单的一行,例如:
const ws = new WebSocket('ws://localhost:8080');
语法也就两个参数
webSocket = new WebSocket(url, protocols);
#url
需要连接的服务器地址,一般以ws
或者wss
开头,这两种不同的协议可以类比为http
和https
的差异
#疑问一
WebSocket
自定义一套协议机制,那为什么不采用http
协议呢?
WebSocket的连接协议也可以用于浏览器之外的场景,可以通过非 HTTP 协商机制交换数据。
#疑问二
如果我设置一个错误协议的 URL 或者是一个正确协议但不存在的 URL 分别会出现什么错误提示?
这里手动测试一下就行,例如:
错误协议的 URL:
new WebSocket('http://localhost')
正确协议但不存在的 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);
});
2
3
4
5
6
7
8
9
#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 中定义的分隔符为,如图所示:
如果协议中包含了这些分隔符会出现什么场景,如下图所示:
解释:
U+0021 到 U+007E
之间的字符是啥?
可以参考维基百科(opens new window),由此可以得出中文名是不符合规范的,它不在这个范围内
ABNF
是什么?
Augmented Backus–Naur form
的缩写,这里可以参考维基百科(opens new window)
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
来应对不同的互动情况
下面列举了几种用途:
- 协议为多个域名,为不同网站提供服务
例如一个WebSocket
服务器为两个不同网站提供服务:
const ws = new WebSocket('ws://localhost:80', 'chat.example.com');
const ws1 = new WebSocket('ws://localhost:80', 'chat.example.org');
2
或者给不同版本的网站提供服务:
const ws = new WebSocket('ws://localhost:80', 'bookings.example.net');
const ws1 = new WebSocket('ws://localhost:80', 'v2.bookings.example.net');
2
- 作为消息格式,在
http
协议中的Content-Type
能够返回不同的响应内容来代表不同的数据格式,例如application/json
、text/html
,那么这个参数也可以用来表示期望的数据内容,例如可以设置json
、text
- 作为加解密
key
,比如使用这个协议的key
作为加密的密钥
总而言之:这个protocol
可以根据需要来动态设置
#实例属性
#protocol
服务器支持的协议,如果未指定协议,那么返回为空
const ws = new WebSocket('ws://localhost:80');
ws.addEventListener('open', () => {
console.log(ws.protocol === ''); // true
});
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
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#url
创建WebSocket
实例对象的URL
,注意这个返回的url是符合规范的
例如:
- 自动补全:
// 虽然后缀未加/,但是获取的时候会自动加上
const url = 'ws://localhost:55';
const ws = new WebSocket(url);
ws.addEventListener('open', () => {
console.log(ws.url); // ws://localhost:55/
});
2
3
4
5
6
7
- 默认端口不显示:
const url = 'ws://localhost:80/';
const ws = new WebSocket(url);
ws.addEventListener('open', () => {
console.log(ws.url); // ws://localhost/
});
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);
});
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
});
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
});
2
3
4
5
6
7
8
#bufferedAmount
是一个只读属性,用于返回已经被send()方法放入队列中但还没有被发送到网络中的数据的字节数。一旦队列中的所有数据被发送至网络,则该属性值将被重置为 0。但是,若在发送过程中连接被关闭,则属性值不会重置为 0。如果你不断地调用send(),则该属性值会持续增长
那么这个属性有什么用呢?这里问问ChatGPT
整体思路也就是有一个缓冲区概念,如果当前正在发送数据,那么放入缓冲区中,否则直接发送数据
#extensions
输出服务端已选择的拓展值,具体可以查看RFC6455 Extensions
(opens new window)
值得注意的是这个值由客户端决定需要哪些拓展值,如果客户端不存在,那么服务端就不应该去设置
目前我Chrome
中默认存在两种拓展值,permessage-deflate
和client_max_window_bits
#perMessageDeflate
使用deflate(opens new window)压缩技术压缩消息,
// 服务端
const wss = new WebSocketServer({
port: 80,
perMessageDeflate: true
});
2
3
4
5
#client_max_window_bits
用于控制客户端压缩拓展的最大窗口大小
const wss = new WebSocketServer({
port: 80,
perMessageDeflate: {
clientMaxWindowBits: 8
}
});
2
3
4
5
6
#实例方法
#close
关闭WebSocket
,语法为WebSocket.close(code, reason)
#code
数字状态码,它表示的是WebSocket
关闭后打印出的值,在关闭事件CloseEvent
内获取,未赋值的情况下默认1005
0-999
:未使用状态码1000-2999
:有具体定义的含义和未来可能被定义的保留值3000-3999
:提供给库和框架使用4000-4999
:提供给用户自定义
具体各个数字代表的含义可以看RFC6455-The WebSocket Protocol(opens new window)
#reason
连接关闭的原因,最大字符串长度不能超过 123 个字节
如果超过了会发生什么事?使用代码测试一下,结果如下图所示:
显而易见,直接抛出异常
#举几个案例
- 未建立连接之前或正在建立连接之前关闭
最终打印出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();
2
3
4
5
6
- 建立连接后主动关闭
其中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();
2
3
4
5
6
7
8
9
10
11
- 服务器代码主动断开
客户端打印1005
,如果手动加上状态码和原因,那么输出的就是手动赋值的状态码以及原因
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({
port: 80,
});
wss.on('connection', (ws, req) => {
ws.close();
});
2
3
4
5
6
7
8
9
- 服务器进程关闭
手动把开启的服务器进程杀掉,那么客户端显示的失败状态码为1006
,原因为空字符串
#send
发送消息,语法为WebSocket.send(data)
#data
一共有四种类型可以作为传输数据
string
ws.send('Hello World!');
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);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
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!
});
});
2
3
4
5
6
7
8
9
10
11
12
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!
});
});
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);
});
2
3
4
5
6
7
8
9
10
11
12
#事件
和其他事件一致,有两种声明方式
addEventListener('open', (event) => {});
onopen = (event) => {};
2
3
#open
当WebSocket
处于open
状态,即readyState
为1
时触发
该打开事件参数为普通的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)对象,表示与发送消息的通道关联的端口
依次打印出来如图所示:
注意:
在这里其实可以不用关注lastEventId
,因为HTML规范(opens new window)中说明了它为SSE(opens new window)提供
source
和ports
也可以不用关注,具体看HTML规范(opens new window)
#error
当WebSocket
的连接由于一些错误事件的发生 (例如无法发送一些数据) 而被关闭时,一个error
事件将被引发
该关闭事件参数为普通的Event(opens new window)
#close
当WebSocket
连接关闭后被触发,即readyState
为3
时触发
该关闭事件参数为CloseEvent(opens new window),继承于普通的Event(opens new window),相比于普通的Event,该事件多了三个参数
- code: 关闭连接的状态码
- reason: 关闭连接的原因
- wasClean: Boolean,连接是否完全关闭
前两个参与之前已经介绍过,这里说一下wasClean
,如果是代码手动close
,那么一般为true
,那么什么场景下为false
呢?
- 突然断开服务器进程
- 连接未建立时手动调用
close
方法关闭