编写 WebSocket 客户端应用程序

在本指南中,我们将介绍一个基于 WebSocket 的 ping 应用程序的实现。在此应用程序中,客户端每秒向服务器发送一个“ping”消息,服务器响应一个“pong”消息。客户端侦听“pong”消息并记录它们,同时跟踪已经进行了多少条消息交换。

虽然这是一个非常简单的应用程序,但它涵盖了编写 WebSocket 客户端所涉及的基本要点。

您可以在 https://github.com/mdn/dom-examples/tree/main/websockets 找到完整的示例。服务器端是用 Deno 编写的,因此如果您想在本地运行示例,需要先安装它。

创建 WebSocket 对象

要使用 WebSocket 协议进行通信,您需要创建一个 WebSocket 对象。创建此对象后,它将立即开始尝试连接到指定的服务器。

js
const wsUri = "ws://127.0.0.1/";
const websocket = new WebSocket(wsUri);

WebSocket 构造函数接受一个必需参数——要连接的 WebSocket 服务器的 URL。在本例中,由于我们将在本地运行服务器,因此我们使用 localhost 地址。

注意: 在此示例中,我们使用的是 ws 协议进行连接,因为我们在示例中连接到 localhost。在实际应用程序中,网页应使用 HTTPS 提供服务,WebSocket 连接应使用 wss 作为协议。

构造函数还接受另一个可选参数 protocols,该参数允许单个服务器实现多个子协议。我们在示例中不使用此功能。

如果目标不允许访问,构造函数将抛出 SecurityError。当您尝试使用不安全的连接时,可能会发生这种情况(大多数 用户代理 现在都要求所有 WebSocket 连接都使用安全链接,除非它们位于同一设备上或可能在同一网络上)。

侦听 open 事件

创建 WebSocket 实例会启动与服务器建立连接的过程。一旦连接建立,将触发 open 事件,之后该套接字就可以传输数据了。

在下面的示例代码中,当触发 open 事件时,我们使用 Window.setInterval() API 每秒向服务器发送一个“ping”消息。

js
websocket.addEventListener("open", () => {
  log("CONNECTED");
  pingInterval = setInterval(() => {
    log(`SENT: ping: ${counter}`);
    websocket.send("ping");
  }, 1000);
});

侦听错误

如果在建立连接时或建立连接后的任何时间发生错误,将触发 error 事件。

我们的应用程序在发生错误时不会执行任何特殊操作,但我们会记录错误。

js
websocket.addEventListener("error", (e) => {
  log(`ERROR`);
});

发生错误后,连接将关闭,并将触发 close 事件。

发送消息

我们已经看到,一旦连接建立,我们就可以使用 send() 方法将消息发送到服务器。

js
websocket.addEventListener("open", () => {
  log("CONNECTED");
  pingInterval = setInterval(() => {
    log(`SENT: ping: ${counter}`);
    websocket.send("ping");
  }, 1000);
});

在我们的示例中,我们发送文本,但您也可以将二进制数据作为 BlobArrayBufferTypedArrayDataView 发送。

一种常见的方法是使用 JSON 将序列化的 JavaScript 对象作为文本发送。例如,我们的客户端可以发送一个包含已交换消息数量的序列化对象,而不是仅仅发送“ping”文本消息。

js
const message = {
  iteration: counter,
  content: "ping",
};
websocket.send(JSON.stringify(message));

send() 方法是异步的:它在返回给调用者之前不会等待数据传输完成。它只是将数据添加到其内部缓冲区并开始传输过程。WebSocket.bufferedAmount 属性表示尚未传输的字节数。请注意,WebSocket 协议使用 UTF-8 对文本进行编码,因此 bufferedAmount 是根据任何已缓冲文本数据的 UTF-8 编码计算的。

接收消息

要从服务器接收消息,我们侦听 message 事件。

我们的消息事件处理程序会记录接收到的消息,并增加已发生的已交换消息数量的计数。

js
websocket.addEventListener("message", (e) => {
  log(`RECEIVED: ${e.data}: ${counter}`);
  counter++;
});

服务器还可以发送二进制数据,这些数据将作为 BlobArrayBuffer 提供给客户端,具体取决于 WebSocket.binaryType 属性的值。

正如我们在发送消息时所见,服务器也可以发送 JSON 字符串,客户端可以将其解析为对象。

js
websocket.addEventListener("message", (e) => {
  const message = JSON.parse(e.data);
  log(`RECEIVED: ${message.iteration}: ${message.content}`);
  counter++;
});

处理断开连接

当连接关闭时(无论是客户端还是服务器关闭它,还是因为发生了错误),将触发 close 事件。

我们的应用程序侦听 close 事件,并在触发时清理 interval 计时器。

js
websocket.addEventListener("close", () => {
  log("DISCONNECTED");
  clearInterval(pingInterval);
});

使用 bfcache

后退/前进缓存(或 bfcache)可以更快地在用户最近访问的页面之间进行后退和前进导航。它通过存储页面的完整快照(包括 JavaScript 堆)来实现这一点。

当页面被添加到 bfcache 或从 bfcache 恢复时,浏览器会暂停然后恢复 JavaScript 执行。这意味着,根据页面正在做什么,浏览器不一定总能安全地使用 bfcache。如果浏览器确定不安全,则不会将页面添加到 bfcache,用户也无法获得它带来的性能优势。

不同的浏览器使用不同的标准将页面添加到 bfcache,并且打开的 WebSocket 连接可能会阻止浏览器将您的页面添加到 bfcache。这意味着,当用户不再使用您的页面时,关闭连接是一种好的做法。用于此目的的最佳事件是 pagehide 事件。

我们在示例应用程序中执行此操作。

js
window.addEventListener("pagehide", () => {
  if (websocket) {
    log("CLOSING");
    websocket.close();
    websocket = null;
    window.clearInterval(pingInterval);
  }
});

相反,通过侦听 pageshow 事件,您可以在页面从 bfcache 恢复时无缝地重新启动连接。由于 pageshow 事件在页面加载时也会触发,因此它也可以用于在页面首次加载时启动 WebSocket 连接。

js
let websocket = null;

window.addEventListener("pageshow", () => {
  log("OPENING");

  websocket = new WebSocket(wsUri);

  websocket.addEventListener("open", () => {
    log("CONNECTED");
    pingInterval = setInterval(() => {
      log(`SENT: ping: ${counter}`);
      websocket.send("ping");
    }, 1000);
  });

  websocket.addEventListener("close", () => {
    log("DISCONNECTED");
    clearInterval(pingInterval);
  });

  websocket.addEventListener("message", (e) => {
    log(`RECEIVED: ${e.data}: ${counter}`);
    counter++;
  });

  websocket.addEventListener("error", (e) => {
    log(`ERROR: ${e.data}`);
  });
});

如果您运行我们的示例,请尝试导航到另一个页面,然后再返回示例。在 Chrome 中,您应该会看到示例重新启动了连接,并保持了其原始上下文:例如,它会记住已交换消息的计数。

有关 bfcache 兼容性和 WebSockets API 的更多背景信息,请参阅 web.dev 关于 bfcache 的文章

在支持它的浏览器上,您可以使用 Performance API 的 notRestoredReasons 属性来获取页面未添加到 bfcache 的原因。

安全注意事项

不应在混合内容环境中(即,不应从使用 HTTPS 加载的页面打开非安全 WebSocket 连接,反之亦然)使用 WebSockets。大多数浏览器现在只允许安全的 WebSocket 连接,并且不再支持在不安全的环境中使用它们。