WebSocket
协议,在规范 RFC 6455 中描述,提供了一种通过持久连接在浏览器和服务器之间交换数据的方式。数据可以在两个方向上以“数据包”的形式传递,而无需断开连接,也不需要额外的 HTTP 请求。
WebSocket 特别适合需要持续数据交换的服务,例如在线游戏、实时交易系统等等。
一个简单的例子
要打开一个 WebSocket 连接,我们需要使用 URL 中的特殊协议 ws
创建 new WebSocket
。
let socket = new WebSocket("ws://javascript.js.cn");
还有一种加密的 wss://
协议。它就像 WebSockets 的 HTTPS。
wss://
。wss://
协议不仅加密,而且更可靠。
这是因为 ws://
数据未加密,任何中间人可见。旧的代理服务器不知道 WebSocket,它们可能会看到“奇怪”的标头并中止连接。
另一方面,wss://
是 WebSocket over TLS(就像 HTTPS 是 HTTP over TLS 一样),传输安全层在发送方加密数据并在接收方解密数据。因此,数据包以加密形式通过代理传递。它们无法看到内部内容并让它们通过。
创建套接字后,我们应该监听其上的事件。总共有 4 个事件
open
– 连接已建立,message
– 收到数据,error
– WebSocket 错误,close
– 连接已关闭。
…如果我们想发送一些东西,那么 socket.send(data)
会做到这一点。
这是一个例子
let socket = new WebSocket("wss://javascript.js.cn/article/websocket/demo/hello");
socket.onopen = function(e) {
alert("[open] Connection established");
alert("Sending to server");
socket.send("My name is John");
};
socket.onmessage = function(event) {
alert(`[message] Data received from server: ${event.data}`);
};
socket.onclose = function(event) {
if (event.wasClean) {
alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
alert('[close] Connection died');
}
};
socket.onerror = function(error) {
alert(`[error]`);
};
出于演示目的,有一个用 Node.js 编写的 小型服务器 server.js 正在运行,用于上面的示例。它以“Hello from server, John”进行响应,然后等待 5 秒并关闭连接。
因此,您将看到事件 open
→ message
→ close
。
实际上就是这样,我们已经可以进行 WebSocket 通信了。很简单,不是吗?
现在让我们更深入地讨论一下。
打开 WebSocket
当创建 new WebSocket(url)
时,它会立即开始连接。
在连接过程中,浏览器(使用标头)询问服务器:“您是否支持 WebSocket?” 如果服务器回复“是”,则对话将继续使用 WebSocket 协议,这与 HTTP 完全不同。
以下是由 new WebSocket("wss://javascript.js.cn/chat")
发出的请求的浏览器标头示例。
GET /chat
Host: javascript.info
Origin: https://javascript.js.cn
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Origin
– 客户端页面的来源,例如https://javascript.js.cn
。WebSocket 对象本质上是跨域的。没有特殊的标头或其他限制。旧服务器无法处理 WebSocket,因此没有兼容性问题。但是,Origin
标头很重要,因为它允许服务器决定是否与该网站进行 WebSocket 通信。Connection: Upgrade
– 信号表明客户端希望更改协议。Upgrade: websocket
– 请求的协议是“websocket”。Sec-WebSocket-Key
– 浏览器生成的随机密钥,用于确保服务器支持 WebSocket 协议。它是随机的,以防止代理缓存任何后续通信。Sec-WebSocket-Version
– WebSocket 协议版本,当前版本为 13。
我们无法使用 XMLHttpRequest
或 fetch
来进行这种类型的 HTTP 请求,因为 JavaScript 不允许设置这些头部信息。
如果服务器同意切换到 WebSocket,它应该发送代码 101 响应
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
这里 Sec-WebSocket-Accept
是 Sec-WebSocket-Key
,使用特殊算法重新编码。浏览器看到它后,就会明白服务器确实支持 WebSocket 协议。
之后,数据将使用 WebSocket 协议传输,我们很快就会看到它的结构(“帧”)。这与 HTTP 完全不同。
扩展和子协议
可能存在额外的头部信息 Sec-WebSocket-Extensions
和 Sec-WebSocket-Protocol
,它们描述了扩展和子协议。
例如
-
Sec-WebSocket-Extensions: deflate-frame
表示浏览器支持数据压缩。扩展是与数据传输相关的,扩展 WebSocket 协议的功能。头部信息Sec-WebSocket-Extensions
由浏览器自动发送,其中包含它支持的所有扩展的列表。 -
Sec-WebSocket-Protocol: soap, wamp
表示我们希望传输的不仅仅是任何数据,而是使用 SOAP 或 WAMP(“WebSocket 应用消息协议”)协议的数据。WebSocket 子协议在 IANA 目录 中注册。因此,此头部信息描述了我们将使用的格式。此可选头部信息使用
new WebSocket
的第二个参数设置。这是子协议的数组,例如,如果我们想使用 SOAP 或 WAMPlet socket = new WebSocket("wss://javascript.js.cn/chat", ["soap", "wamp"]);
服务器应该响应它同意使用的协议和扩展列表。
例如,请求
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.js.cn
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp
响应
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap
这里服务器响应它支持扩展“deflate-frame”,并且只支持请求的子协议中的 SOAP。
数据传输
WebSocket 通信由“帧”组成 - 数据片段,可以从任一方发送,并且可以有几种类型
- “文本帧” - 包含各方相互发送的文本数据。
- “二进制数据帧” - 包含各方相互发送的二进制数据。
- “ping/pong 帧”用于检查连接,由服务器发送,浏览器会自动响应这些帧。
- 还有“连接关闭帧”和一些其他服务帧。
在浏览器中,我们直接处理的只有文本或二进制帧。
WebSocket 的 .send()
方法可以发送文本或二进制数据。
调用 socket.send(body)
允许 body
为字符串或二进制格式,包括 Blob
、ArrayBuffer
等。无需任何设置:只需以任何格式发送即可。
当我们接收数据时,文本始终以字符串形式出现。对于二进制数据,我们可以选择 Blob
和 ArrayBuffer
格式。
这由 socket.binaryType
属性设置,默认值为 "blob"
,因此二进制数据以 Blob
对象形式出现。
Blob 是一个高级二进制对象,它直接与 <a>
、<img>
和其他标签集成,因此这是一个合理的默认值。但对于二进制处理,要访问单个数据字节,我们可以将其更改为 "arraybuffer"
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
// event.data is either a string (if text) or arraybuffer (if binary)
};
速率限制
想象一下,我们的应用程序正在生成大量要发送的数据。但用户网络连接缓慢,可能是在城市以外的移动互联网上。
我们可以反复调用 socket.send(data)
。但数据将被缓冲(存储)在内存中,并且仅在网络速度允许的情况下才会发送出去。
socket.bufferedAmount
属性存储当前缓冲的字节数,等待通过网络发送。
我们可以检查它以查看套接字是否实际上可用于传输。
// every 100ms examine the socket and send more data
// only if all the existing data was sent out
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(moreData());
}
}, 100);
连接关闭
通常,当一方想要关闭连接时(浏览器和服务器拥有同等权利),它们会发送一个包含数字代码和文本原因的“连接关闭帧”。
该方法是
socket.close([code], [reason]);
code
是一个特殊的 WebSocket 关闭代码(可选)reason
是一个描述关闭原因的字符串(可选)
然后,另一方在 close
事件处理程序中获取代码和原因,例如
// closing party:
socket.close(1000, "Work complete");
// the other party
socket.onclose = event => {
// event.code === 1000
// event.reason === "Work complete"
// event.wasClean === true (clean close)
};
最常见的代码值
1000
– 默认值,正常关闭(如果未提供code
,则使用),1006
– 无法手动设置此代码,表示连接已丢失(没有关闭帧)。
还有其他代码,例如
1001
– 对方即将离开,例如服务器正在关闭,或浏览器离开页面,1009
– 消息太大,无法处理,1011
– 服务器上出现意外错误,- …等等。
完整列表可以在 RFC6455,§7.4.1 中找到。
WebSocket 代码有点像 HTTP 代码,但有所不同。特别是,低于1000
的代码是保留的,如果我们尝试设置这样的代码,就会出现错误。
// in case connection is broken
socket.onclose = event => {
// event.code === 1006
// event.reason === ""
// event.wasClean === false (no closing frame)
};
连接状态
要获取连接状态,还可以使用socket.readyState
属性,其值如下:
0
– “CONNECTING”: 连接尚未建立,1
– “OPEN”: 正在通信,2
– “CLOSING”: 连接正在关闭,3
– “CLOSED”: 连接已关闭。
聊天示例
让我们回顾一下使用浏览器 WebSocket API 和 Node.js WebSocket 模块https://github.com/websockets/ws的聊天示例。我们将主要关注客户端,但服务器也很简单。
HTML:我们需要一个<form>
来发送消息,以及一个<div>
来接收消息
<!-- message form -->
<form name="publish">
<input type="text" name="message">
<input type="submit" value="Send">
</form>
<!-- div with messages -->
<div id="messages"></div>
从 JavaScript 中,我们想要三件事
- 打开连接。
- 在表单提交时 –
socket.send(message)
用于发送消息。 - 在收到消息时 – 将其追加到
div#messages
。
以下是代码
let socket = new WebSocket("wss://javascript.js.cn/article/websocket/chat/ws");
// send message from the form
document.forms.publish.onsubmit = function() {
let outgoingMessage = this.message.value;
socket.send(outgoingMessage);
return false;
};
// message received - show the message in div#messages
socket.onmessage = function(event) {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
document.getElementById('messages').prepend(messageElem);
}
服务器端代码有点超出了我们的范围。在这里,我们将使用 Node.js,但您不必这样做。其他平台也有自己的方法来处理 WebSocket。
服务器端算法将是
- 创建
clients = new Set()
– 一组套接字。 - 对于每个接受的 websocket,将其添加到集合
clients.add(socket)
中,并设置message
事件监听器以获取其消息。 - 收到消息时:遍历客户端并将消息发送给每个人。
- 连接关闭时:
clients.delete(socket)
。
const ws = new require('ws');
const wss = new ws.Server({noServer: true});
const clients = new Set();
http.createServer((req, res) => {
// here we only handle websocket connections
// in real project we'd have some other code here to handle non-websocket requests
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);
});
function onSocketConnect(ws) {
clients.add(ws);
ws.on('message', function(message) {
message = message.slice(0, 50); // max message length will be 50
for(let client of clients) {
client.send(message);
}
});
ws.on('close', function() {
clients.delete(ws);
});
}
以下是工作示例
您也可以下载它(iframe 中的右上角按钮)并在本地运行它。只需在运行之前不要忘记安装Node.js和npm install ws
。
总结
WebSocket 是一种现代的方式,可以实现浏览器与服务器之间的持久连接。
- WebSockets 没有跨域限制。
- 它们在浏览器中得到了很好的支持。
- 可以发送/接收字符串和二进制数据。
API 很简单。
方法
socket.send(data)
,socket.close([code], [reason])
.
事件
open
,message
,error
,close
.
WebSocket 本身不包括重新连接、身份验证和许多其他高级机制。因此,有客户端/服务器库可以做到这一点,也可以手动实现这些功能。
有时,为了将 WebSocket 集成到现有项目中,人们会并行运行 WebSocket 服务器和主 HTTP 服务器,并且它们共享一个数据库。对 WebSocket 的请求使用wss://ws.site.com
,这是一个指向 WebSocket 服务器的子域,而https://site.com
指向主 HTTP 服务器。
当然,其他集成方式也是可能的。
评论
<code>
标签,对于多行代码,请用<pre>
标签包裹,对于超过 10 行的代码,请使用沙箱(plnkr,jsbin,codepen…)