WebSocket 是什么?

WebSocket 是什么?

Mar 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 时被调用,它接收一个名字为 closeCloseEvent 事件。

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

https://www.ruanyifeng.com/blog/2017/05/websocket.html

https://zh.javascript.info/websocket


  1. https://stackoverflow.com/questions/18265128/what-is-sec-websocket-key-for ↩︎

本文共 2645 字,上次修改于 Jul 27, 2022,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» 跨域相关问题

» 浏览器中的 HTTP 缓存使用策略

» 说说实际工作中 GraphQL 的使用体验

» TCP/IP 协议理论基础整理

» 了解下网络硬件设备