您可以在SharedWorker WebSocket 示例中找到本文的代码。

网络套接字

Web Sockets 允许客户端浏览器和服务器之间进行实时通信。它们与HTTP不同,因为它们不仅允许客户端向服务器请求数据,还允许服务器从服务器推送数据。

图像

问题

但为了允许这一点,每个客户端都需要打开与服务器的连接并保持连接,直到客户端关闭选项卡/离线为止。他们创建持久连接。这使得交互成为有状态的,导致客户端和服务器在每个打开的客户端连接的 WebSocket 服务器的内存中至少存储一些数据。

因此,如果客户端打开了 15 个选项卡,那么他们将有 15 个与服务器的打开连接。这篇文章是尝试减少单个客户端负载的解决方案。

图像

WebWorkersSharedWorkersBroadcastChannels进行救援

Web Workers是 Web 内容在后台线程中运行脚本的一种简单方法。工作线程可以在不干扰用户界面的情况下执行任务。创建后,工作人员可以通过将消息发布到由该代码指定的事件处理程序来向创建它的 JavaScript 代码发送消息(反之亦然)。

共享 Workers是一种 Web Worker,可以从多个浏览上下文(例如多个窗口、iframe 甚至 Worker)进行访问。

广播通道 允许具有相同来源的浏览上下文(即窗口选项卡框架iframe )之间进行简单的通信。

以上所有定义均来自MDN。

使用 SharedWorkers 减少服务器负载

我们可以使用SharedWorker它来解决单个客户端从同一浏览器打开多个连接的问题。我们可以使用 a 打开与服务器的连接,而不是从每个选项卡/浏览器窗口SharedWorker打开连接。

此连接将一直打开,直到网站的所有选项卡都关闭为止。所有打开的选项卡都可以使用单个连接来与服务器通信并从服务器接收消息。

我们将使用广播通道 API 将 Web 套接字的状态更改广播到所有上下文(选项卡)。

设置基本的 Web Socket 服务器

现在让我们进入代码。出于本文的目的,我们将设置一个非常简单的 Web 服务器,它支持使用wsnpm 模块的套接字连接。使用以下命令初始化 npm 项目:

$ npm init

运行这些步骤,一旦有了文件package.json,就添加ws模块并express创建基本的 http 服务器:

$ npm install --save ws express

完成此操作后,使用以下代码创建一个 index.js 文件,以设置静态服务器,从public端口 3000 的目录提供文件服务,并ws在端口 3001 运行服务器:

const  express  =  require("express");
const  path  =  require("path");
const  WebSocket  =  require("ws");
const  app  =  express();

// Use the public directory for static file requests
app.use(express.static("public"));

// Start our WS server at 3001
const wss = new WebSocket.Server({ port: 3001 });

wss.on("connection", ws => {
  console.log('A new client connected!');
  ws.on("message", data => {
    console.log(`Message from client: ${data}`);

    // Modify the input and return the same.
    const  parsed  =  JSON.parse(data);
    ws.send(
      JSON.stringify({
        ...parsed.data,
        // Additional field set from the server using the from field.
        // We'll see how this is set in the next section.
        messageFromServer: `Hello tab id: ${parsed.data.from}`
      })
    );
  });
  ws.on("close", () => {
    console.log("Sad to see you go :(");
  });
});

// Listen for requests for static pages at 3000
const  server  =  app.listen(3000, function() {
  console.log("The server is running on http://localhost:"  +  3000);
});

创建一个SharedWorker

要在 JavaScript 中创建任何类型的 a Worker,您需要创建一个单独的文件来定义工作线程将执行的操作。

在工作程序文件中,您需要定义初始化该工作程序时要执行的操作。该代码只会在SharedWorker初始化时调用一次。此后,直到连接到该工作程序的最后一个选项卡未关闭/结束与该工作程序的连接为止,该代码无法重新运行。

我们可以定义一个onconnect事件处理程序来处理连接到 this 的每个选项卡SharedWorker。让我们看一下该worker.js文件。

// Open a connection. This is a common
// connection. This will be opened only once.
const ws = new WebSocket("ws://localhost:3001");

// Create a broadcast channel to notify about state changes
const broadcastChannel = new BroadcastChannel("WebSocketChannel");

// Mapping to keep track of ports. You can think of ports as
// mediums through we can communicate to and from tabs.
// This is a map from a uuid assigned to each context(tab)
// to its Port. This is needed because Port API does not have
// any identifier we can use to identify messages coming from it.
const  idToPortMap  = {};

// Let all connected contexts(tabs) know about state cahnges
ws.onopen = () => broadcastChannel.postMessage({ type: "WSState", state: ws.readyState });
ws.onclose = () => broadcastChannel.postMessage({ type: "WSState", state: ws.readyState });

// When we receive data from the server.
ws.onmessage  = ({ data }) => {
  console.log(data);
  // Construct object to be passed to handlers
  const parsedData = { data:  JSON.parse(data), type:  "message" }
  if (!parsedData.data.from) {
    // Broadcast to all contexts(tabs). This is because 
    // no particular id was set on the from field here. 
    // We're using this field to identify which tab sent
    // the message
    broadcastChannel.postMessage(parsedData);
  } else {
    // Get the port to post to using the uuid, ie send to
    // expected tab only.
    idToPortMap[parsedData.data.from].postMessage(parsedData);
  }
};

// Event handler called when a tab tries to connect to this worker.
onconnect = e => {
  // Get the MessagePort from the event. This will be the
  // communication channel between SharedWorker and the Tab
  const  port  =  e.ports[0];
  port.onmessage  =  msg  => {
    // Collect port information in the map
    idToPortMap[msg.data.from] =  port;
    
    // Forward this message to the ws connection.
    ws.send(JSON.stringify({ data:  msg.data }));
  };

  // We need this to notify the newly connected context to know
  // the current state of WS connection.
  port.postMessage({ state: ws.readyState, type: "WSState"});
};

我们在这里所做的一些事情可能从一开始就不清楚。当您阅读这篇文章时,我们会清楚地了解我们为什么这样做。还有几点我想澄清一下:

  • 我们使用广播通道 API 来广播套接字的状态更改。
  • 我们使用postMessage连接上的端口来设置上下文(选项卡)的初始状态。
  • 我们使用from来自上下文(选项卡)本身的字段来确定将响应重定向到何处。
  • 如果我们没有from从来自服务器的消息中设置字段,我们只会将其广播给所有人!

注意console.log此处的语句在您的选项卡控制台中不起作用。您需要打开 SharedWorker 控制台才能查看这些日志。要打开 SharedWorkers 的开发工具,请转到 chrome://inspect。

消耗一个SharedWorker

让我们首先创建一个 HTML 页面来容纳我们的脚本,该脚本将使用SharedWorker.

<!DOCTYPE  html>
<html  lang="en">
<head>
  <meta  charset="UTF-8"  />
  <title>Web Sockets</title>
</head>
<body>
  <script  src="https://cdnjs.cloudflare.com/ajax/libs/node-uuid/1.4.8/uuid.min.js"></script>
  <script  src="main.js"></script>
</body>
</html>

我们已经在worker.js文件中定义了我们的工作人员并设置了一个 HTML 页面。现在让我们看看如何从任何上下文(选项卡)使用此共享 Web 套接字连接。创建一个main.js包含以下内容的文件:

// Create a SharedWorker Instance using the worker.js file. 
// You need this to be present in all JS files that want access to the socket
const worker = new SharedWorker("worker.js");

// Create a unique identifier using the uuid lib. This will help us
// in identifying the tab from which a message was sent. And if a 
// response is sent from server for this tab, we can redirect it using
// this id.
const id = uuid.v4();

// Set initial web socket state to connecting. We'll modify this based
// on events.
let  webSocketState  =  WebSocket.CONNECTING;
console.log(`Initializing the web worker for user: ${id}`);

// Connect to the shared worker
worker.port.start();

// Set an event listener that either sets state of the web socket
// Or handles data coming in for ONLY this tab.
worker.port.onmessage = event => {
  switch (event.data.type) {
    case "WSState":
      webSocketState = event.data.state;
      break;
    case "message":
      handleMessageFromPort(event.data);
      break;
  }
};

// Set up the broadcast channel to listen to web socket events.
// This is also similar to above handler. But the handler here is
// for events being broadcasted to all the tabs.
const broadcastChannel = new BroadcastChannel("WebSocketChannel");
broadcastChannel.addEventListener("message", event => {
  switch (event.data.type) {
    case  "WSState":
      webSocketState  =  event.data.state;
      break;
    case  "message":
      handleBroadcast(event.data);
      break;
  }
});

// Listen to broadcasts from server
function  handleBroadcast(data) {
  console.log("This message is meant for everyone!");
  console.log(data);
}

// Handle event only meant for this tab
function  handleMessageFromPort(data) {
  console.log(`This message is meant only for user with id: ${id}`);
  console.log(data);
}

// Use this method to send data to the server.
function  postMessageToWSServer(input) {
  if (webSocketState  ===  WebSocket.CONNECTING) {
    console.log("Still connecting to the server, try again later!");
  } else  if (
    webSocketState  ===  WebSocket.CLOSING  ||
    webSocketState  ===  WebSocket.CLOSED
  ) {
    console.log("Connection Closed!");
  } else {
    worker.port.postMessage({
      // Include the sender information as a uuid to get back the response
      from:  id,
      data:  input
    });
  }
}

// Sent a message to server after approx 2.5 sec. This will 
// give enough time to web socket connection to be created.
setTimeout(() =>  postMessageToWSServer("Initial message"), 2500);```

发送消息至SharedWorker

正如我们在上面看到的,您可以SharedWorker使用向此发送消息worker.port.postMessage()。您可以在此处传递任何 JS 对象/数组/原始值。

这里的一个好的做法是传递一个对象,该对象指定消息来自哪个上下文,以便工作人员可以采取相应的操作。例如,如果我们有一个聊天应用程序,并且其中一个选项卡想要发送一条消息,我们可以使用如下内容:

{
    // Define the type and the 
  type: 'message',
  from: 'Tab1'
  value: {
    text: 'Hello',
    createdAt: new Date()
  }
}

如果我们有一个文件共享应用程序,则在删除文件时,可以使用相同的结构和不同的类型和值:

{
  type: 'deleteFile',
  from: 'Tab2'
  value: {
    fileName: 'a.txt',
    deletedBy: 'testUser'
  }
}

这将允许工人决定如何处理它。

听取工人的消息

我们一开始就设置了一张地图来跟踪MessagePorts不同的选项卡。然后,我们设置一个worker.port.onmessage事件处理程序来处理SharedWorker直接来自选项卡的事件。

如果服务器没有设置 from 字段,我们只需使用广播通道将消息广播到所有选项卡。所有选项卡都会有一个消息侦听器,WebSocketChannel它将处理所有消息广播。

这种类型的设置可用于以下 2 种场景:

  • 假设您正在通过选项卡玩游戏。您只希望消息到达此选项卡。其他选项卡不需要此信息。这是您可以使用第一种情况的地方。
  • 现在,如果你在 Facebook 上玩这个游戏,并收到一条短信。此信息应在所有选项卡上广播,因为标题中的通知计数需要更新。

最终图示

我们使用 SharedWorkers 来优化 Web Sockets 的使用。这是如何使用它的最终图解表示:

图像

笔记

这只是一个实验,我想尝试在多个浏览上下文之间共享相同的套接字连接。我认为这可以帮助减少每个客户端所需的连接数量。这方面仍然存在很多粗糙的问题。让我知道您对实时应用程序扩展问题的解决方案的看法。包含代码的存储库:SharedWorker WebSocket 示例

https://ayushgp.xyz/scaling-websockets-using-sharedworkers/

使用共享工作线程扩展 WebSocket 连接
标签: