WebSocket 是什么?
3月 15, 2022
WebSocket 是 HTML5 开始提供的一种网络传输协议,可以在单个 TCP 连接上进行全双工通信,位于 OSI 参考模型的应用层。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket 协议出现之前如果为了实现服务器推送技术,就需要客户端每隔一段时间发起 Ajax 请求,WebSocket 解决了 HTTP 不能够实现持久性的通信、以及只能由客户端发起请求服务器进行响应的限制。
协议内容 #
URI #
WebSocket 是一种与 HTTP 不同的协议,两者都建立在 TCP 协议之上,并且 WebSocket 协议也通过 HTTP 端口 80 和 443 进行工作,从而使其与 HTTP 协议兼容。协议标识符是 ws
,如果加密就是 wss
,服务器网址就是 URL。
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
握手过程(Handshaking) #
客户端发起请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
其中:
- Connection 必须为 Upgrade,表示客户端希望连接升级。
- Upgrade 字段必须为 Websocket,表示希望升级到 Websocket 协议。
- Sec-WebSocket-Key 是一个随机的字符串,用于服务端校验。
- Sec-WebSocket-Protocol 可以用来自定义 WebSocket 子协议(你可能希望服务端处理不同类型的子协议)。
- Sec-WebSocket-Version 表示支持的 Websocket 版本。
- Origin 字段是必须的。如果缺少 Origin 字段,WebSocket 服务器需要回复 HTTP 403 状态码(禁止访问)。
- 其他一些定义在 HTTP 协议中的字段,如 Cookie 等,也可以在 Websocket 中使用。
关于 Sec-WebSocket-Key
多说一句,服务器在收到后会将 Sec-WebSocket-Key
会加上一个特殊字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,然后计算 SHA-1 摘要,之后再进行 Base64 编码,然后将结果做为 Sec-WebSocket-Accept
头的值返回给客户端。这样做的主要目的是为了与普通的 HTTP 请求做区分,确保服务器理解客户端希望使用 WebSocket 协议,并不是为了安全性1。
服务器响应请求:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
其中:
- HTTP
101 Switching Protocol
也表明了服务端应客户端升级协议的请求正在切换协议。 - 服务器在识别是 WebSocket 协议后,计算
Sec-WebSocket-Accept
值,并放入进响应中。 Sec-WebSocket-Protocol
仅返回了 chat 表示服务端不支持 superchat,仅支持 chat 子协议。
经过这次握手之后,双方就可以开始通过这条已经建立的 TCP 连接进行收发数据了。
连接状态 #
WebSocket 有四种状态:
- 0
CONNECTING
表示正在连接中。 - 1
OPEN
表示已经建立连接并且可以通讯。 - 2
CLOSING
表示连接正在关闭。 - 3
CLOSED
表示连接已关闭或者没有连接成功。
数据格式 #
返回 WebSocket 连接所传输二进制数据的类型。这个值可以是 blob
或者 ArrayBuffer
。可以通过 WebSocket.binaryType
指定数据类型,默认是 blob
:
// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
console.log(e.data.size);
};
// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};
浏览器 API #
服务端实现以 ws 库为例:
// server.mjs
import { WebSocketServer } from "ws";
const ws = new WebSocketServer({ port: 8080 });
ws.on("connection", function connection(ws) {
ws.on("message", function message(data) {
ws.send("received: " + data);
});
});
浏览器中可通过下面语句建立与服务器的连接:
let ws = new websocket("ws://127.0.0.1:8080");
拿到 ws
对象后,就已经完成了握手过程。接下来就可以使用其 send()
方法进行数据传输。
ws.send("hello");
我封装了一个 index.html
,可以自己手动调试下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Example</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
crossorigin="anonymous"></script>
</head>
<body>
<div id="root">
<label>通信地址:</label>
<input type="text" id="wsaddr" value="ws:localhost:8080" />
<button type="button" onclick='newsocket();'>连接</button>
<button type="button" onclick='closesocket();'>断开</button>
<button type="button" onclick='$("#wsaddr").val("")'>清空</button>
<br />
<input type="text" id="send" />
<button type="button" onclick='send();'>发送</button>
<br />
<div id="output"></div>
</div>
</body>
<script>
let ws;
function closesocket() {
if (ws) ws.close();
}
function newsocket() {
let wsaddr = $("#wsaddr").val();
if (wsaddr) {
ws = new WebSocket(wsaddr);
ws.onopen = function(evt) {
output("连接成功");
};
ws.onclose = function(evt) {
output("连接关闭");
};
ws.onmessage = function(evt) {
output("收到消息\n" + evt.data);
};
ws.onerror = function(evt) {
output("连接错误");
};
}
}
function send() {
if (ws) {
let msg = $("#send").val();
ws.send(msg);
output("发送消息\n" + msg);
}
}
function output(msg) {
$("#output").append("<p>" + msg + "</p>");
}
</script>
</html>
事件处理 #
上面 index
代码可以看到,WebSocket 提供了几个响应事件。
onopen #
WebSocket.onopen
属性定义一个事件处理程序,当 WebSocket 的连接状态 readyState
变为 1 时触发 open
事件。
ws.onopen = function(event) {
console.log("连接成功");
};
onmessage #
message
事件会在 WebSocket
接收到新消息时被触发。
ws.onmessage = function(evt) {
console.log("收到消息:" + evt.data);
};
onerror #
在 WebSocket
处理过程中遇到错误时会触发 error
事件。
ws.onerror = function(evt) {
console.log("连接错误");
};
onclose #
WebSocket.onclose
属性返回一个事件监听器,这个事件监听器将在 WebSocket 连接的 readyState
变为 CLOSED
时被调用,它接收一个名字为 close
的 CloseEvent
事件。
ws.onclose = function(event) {
console.log("WebSocket is closed now.");
};
流量控制 #
WebSocket 提供了一个 WebSocket.bufferedAmount
的只读属性,可以用来读取当前在发送队列,但还没被发送出去的字节数,一旦队列中的所有数据被发送至网络,则该属性值将被重置为 0。
通过读取该字段,适当地调用 send()
方法可以实现一定程度的流量控制,不至于数据无限制发送,当然 TCP 协议本身也有流量控制,两者并不冲突。这个字段也可以用来在调用 close()
前判断数据是否都发送完毕,避免关闭连接时有未发送完的数据。
连接关闭 #
当一方想关闭连接时,会通过 send()
发送一个带有数字码 code
和文本形式的 reason
。
// 关闭方
socket.close(1000, "Work complete");
// 另一方
socket.onclose = event => {
// event.code === 1000
// event.reason === "Work complete"
// event.wasClean === true (clean close)
};
拓展协议 #
客户端通过 Sec-WebSocket-Extensions
请求头字段来请求扩展。扩展与传输数据有关,扩展了 WebSocket 协议的功能。比如 Sec-WebSocket-Extensions: deflate-frame
表示浏览器支持数据压缩。服务端可以在这个字段中返回相同的值表示其支持数据压缩。
总结 #
优缺点 #
- 较少的控制开销:数据包头部协议较小,不同于 HTTP 每次请求需要携带完整的头部
- 更强的实时性:相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
- 保持创连接状态:创建通信后,可省略状态信息,不同于 HTTP 每次请求需要携带身份验证;
- 更好的二进制支持:定义了二进制帧,更好处理二进制内容
- 支持扩展:用户可以扩展 WebSocket 协议、实现部分自定义的子协议;
- 更好的压缩效果:WebSocket 在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
应用场景 #
- 弹幕
- 即时聊天
- 协同编辑
- 共享实时位置
- 体育实况更新
- 股票基金报价实时更新
与 HTTP/2 区别 #
从功能特性上,可以发现 WebSocket 和 HTTP/2 还是有一点像,但是在实现上,WebSocket 比 HTTP/2 简单很多,并且 WebSocket 更强调实时通信,HTTP/2 更侧重传输效率,比如 HTTP/2 的多路复用、优先级等特性,WebSocket 都没有。
参考 #
https://datatracker.ietf.org/doc/html/rfc6455
https://zh.wikipedia.org/zh-hans/WebSocket
https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket