WebSocket实战:在 Node 和 React 之间进行实时通讯

翻译:疯狂的技术宅

原文:https://blog.logrocket.com/we...javascript


本文首发微信公众号:前端先锋
欢迎关注,天天都给你推送新鲜的前端技术文章html


img

Web 为了支持客户端和服务器之间的全双工(或双向)通讯已经走过了很长的路。这是 WebSocket 协议的主要目的:经过单个 TCP 套接字链接在客户端和服务器之间提供持久的实时通讯。前端

WebSocket 协议只有两个议程:1)打开握手,2)帮助数据传输。一旦服务器和客户端握手成功,他们就能够随意地以较少的开销相互发送数据。java

WebSocket 通讯使用WS(端口80)或WSS(端口443)协议在单个 TCP 套接字上进行。根据 Can I Use,撰写本文时除了 Opera Mini 以外几乎全部的浏览器支持 WebSockets 。node

现状

从历史上看,建立须要实时数据通信(如游戏或聊天应用程序)的 Web 应用须要滥用 HTTP 协议来创建双向数据传输。尽管有许多种方法用于实现实时功能,但没有一种方法与 WebSockets 同样高效。 HTTP 轮询、HTTP流、Comet、SSE —— 它们都有本身的缺点。react

HTTP 轮询

解决问题的第一个尝试是按期轮询服务器。 HTTP 长轮询生命周期以下:git

  1. 客户端发出请求并一直等待响应。
  2. 服务器推迟响应,直到发生更改、更新或超时。请求保持“挂起”,直到服务器有东西返回客户端。
  3. 当服务器端有一些更改或更新时,它会将响应发送回客户端。
  4. 客户端发送新的长轮询请求以侦听下一组更改。

长轮询中存在不少漏洞 —— 标头开销、延迟、超时、缓存等等。程序员

HTTP 流式传输

这种机制减小了网络延迟的痛苦,由于初始请求无限期地保持打开状态。即便在服务器推送数据以后,请求也永远不会终止。 HTTP 流中的前三步生命周期方法与 HTTP 轮询是相同的。github

可是,当响应被发送回客户端时,请求永远不会终止,服务器保持链接打开状态,并在发生更改时发送新的更新。web

服务器发送事件(SSE)

使用 SSE,服务器将数据推送到客户端。聊天或游戏应用不能彻底依赖 SSE。 SSE 的完美用例是相似 Facebook 的新闻 Feed:每当有新帖发布时,服务器会将它们推送到时间线。 SSE 经过传统 HTTP 发送,而且对打开的链接数有限制。

这些方法不只效率低下,维护它们的代码也使开发人员感到厌倦。

WebSocket

WebSockets 旨在取代现有的双向通讯技术。当涉及全双工实时通讯时,上述现有方法既不可靠也不高效。

WebSockets 相似于 SSE,但在将消息从客户端传回服务器方面也很优秀。因为数据是经过单个 TCP 套接字链接提供的,所以链接限制再也不是问题。


实战教程

正如介绍中所提到的,WebSocket 协议只有两个议程。让咱们看看 WebSockets 如何实现这些议程。为此我将分析一个 Node.js 服务器并将其链接到使用 React.js 构建的客户端上。

议程1:WebSocket在服务器和客户端之间创建握手

在服务器级别建立握手

咱们能够用单个端口来分别提供 HTTP 服务和 WebSocket 服务。下面的代码显示了一个简单的 HTTP 服务器的建立过程。一旦建立,咱们会将 WebSocket 服务器绑定到 HTTP 端口:

const webSocketsServerPort = 8000;
const webSocketServer = require('websocket').server;
const http = require('http');
// Spinning the http server and the websocket server.
const server = http.createServer();
server.listen(webSocketsServerPort);
const wsServer = new webSocketServer({
  httpServer: server
});

建立 WebSocket 服务器后,咱们须要在接收来自客户端的请求时接受握手。我将全部链接的客户端做为对象保存在代码中,并在收请从浏览器发来的求时使用惟一的用户ID。

// I'm maintaining all active connections in this object
const clients = {};

// This code generates unique userid for everyuser.
const getUniqueID = () => {
  const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
  return s4() + s4() + '-' + s4();
};

wsServer.on('request', function(request) {
  var userID = getUniqueID();
  console.log((new Date()) + ' Recieved a new connection from origin ' + request.origin + '.');
  // You can rewrite this part of the code to accept only the requests from allowed origin
  const connection = request.accept(null, request.origin);
  clients[userID] = connection;
  console.log('connected: ' + userID + ' in ' + Object.getOwnPropertyNames(clients))
});

那么,当接受链接时会发生什么?

在发送常规 HTTP 请求以创建链接时,在请求头中,客户端发送 *Sec-WebSocket-Key*。服务器对此值进行编码和散列,并添加预约义的 GUID。它回应了服务器发送的握手中 *Sec-WebSocket-Accept*中生成的值。

一旦请求在服务器中被接受(在必要验证以后),就完成了握手,其状态代码为 101。若是在浏览器中看到除状态码 101 以外的任何内容,则意味着 WebSocket 升级失败,而且将遵循正常的 HTTP 语义。

*Sec-WebSocket-Accept* 头字段指示服务器是否愿意接受链接。此外若是响应缺乏 *Upgrade* 头字段,或者 *Upgrade* 不等于 websocket,则表示 WebSocket 链接失败。

成功的服务器握手以下所示:

HTTP GET ws://127.0.0.1:8000/ 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: Nn/XHq0wK1oO5RTtriEWwR4F7Zw=
Upgrade: websocket

在客户端级别建立握手

在客户端,我使用与服务器中的相同 WebSocket 包来创建与服务器的链接(Web IDL 中的 WebSocket API 正在由W3C 进行标准化)。一旦服务器接受请求,咱们将会在浏览器控制台上看到 WebSocket Client Connected

这是建立与服务器的链接的初始脚手架:

import React, { Component } from 'react';
import { w3cwebsocket as W3CWebSocket } from "websocket";

const client = new W3CWebSocket('ws://127.0.0.1:8000');

class App extends Component {
  componentWillMount() {
    client.onopen = () => {
      console.log('WebSocket Client Connected');
    };
    client.onmessage = (message) => {
      console.log(message);
    };
  }
  
  render() {
    return (
      <div>
        Practical Intro To WebSockets.
      </div>
    );
  }
}

export default App;

客户端发送如下标头来创建握手:

HTTP GET ws://127.0.0.1:8000/ 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: vISxbQhM64Vzcr/CD7WHnw==
Origin: http://localhost:3000
Sec-WebSocket-Version: 13

如今客户端和服务器经过相互握手进行了链接,WebSocket 链接能够在接收消息时传输消息,从而实现 WebSocket 协议的第二个议程。

议程2:实时信息传输

内容修改的实时流。

我将编写一个基本的实时文档编辑器,用户能够将它们链接在一块儿并编辑文档。我跟踪了两个事件:

  1. 用户活动:每次用户加入或离开时,我都会将消息广播给全部链接其余的客户端。
  2. 内容更改:每次修改编辑器中的内容时,都会向全部链接的其余客户端广播。

该协议容许咱们用二进制数据或 UTF-8 发送和接收消息(注意:传输和转换 UTF-8 的开销较小)。

只要咱们对套接字事件onopenoncloseonmessage有了充分的了解,理解和实现 WebSockets 就很是简单。客户端和服务器端的术语相同。

在客户端发送和接收消息

在客户端,当新用户加入或内容更改时,咱们用 client.send 向服务器发消息,以将新信息提供给服务器。

/* When a user joins, I notify the
server that a new user has joined to edit the document. */
logInUser = () => {
  const username = this.username.value;
  if (username.trim()) {
    const data = {
      username
    };
    this.setState({
      ...data
    }, () => {
      client.send(JSON.stringify({
        ...data,
        type: "userevent"
      }));
    });
  }
}

/* When content changes, we send the
current content of the editor to the server. */
onEditorStateChange = (text) => {
 client.send(JSON.stringify({
   type: "contentchange",
   username: this.state.username,
   content: text
 }));
};

咱们跟踪的事件是:用户加入和内容更改。

从服务器接收消息很是简单:

componentWillMount() {
  client.onopen = () => {
   console.log('WebSocket Client Connected');
  };
  client.onmessage = (message) => {
    const dataFromServer = JSON.parse(message.data);
    const stateToChange = {};
    if (dataFromServer.type === "userevent") {
      stateToChange.currentUsers = Object.values(dataFromServer.data.users);
    } else if (dataFromServer.type === "contentchange") {
      stateToChange.text = dataFromServer.data.editorContent || contentDefaultMessage;
    }
    stateToChange.userActivity = dataFromServer.data.userActivity;
    this.setState({
      ...stateToChange
    });
  };
}

在服务器端发送和侦听消息

在服务器中,咱们只需捕获传入的消息并将其广播到链接到 WebSocket 的全部客户端。这是臭名昭着的 Socket.IO 和 WebSocket 之间的差别之一:当咱们使用 WebSockets 时,咱们须要手动将消息发送给全部客户端。 Socket.IO 是一个成熟的库,因此它本身来处理。

const sendMessage = (json) => {
  // We are sending the current data to all connected clients
  Object.keys(clients).map((client) => {
    clients[client].sendUTF(json);
  });
}

connection.on('message', function(message) {
    if (message.type === 'utf8') {
      const dataFromClient = JSON.parse(message.utf8Data);
      const json = { type: dataFromClient.type };
      if (dataFromClient.type === typesDef.USER_EVENT) {
        users[userID] = dataFromClient;
        userActivity.push(`${dataFromClient.username} joined to edit the document`);
        json.data = { users, userActivity };
      } else if (dataFromClient.type === typesDef.CONTENT_CHANGE) {
        editorContent = dataFromClient.content;
        json.data = { editorContent, userActivity };
      }
      sendMessage(JSON.stringify(json));
    }
  });

将消息广播到全部链接的客户端。

img

浏览器关闭后会发生什么?

在这种状况下,WebSocket调用 close 事件,它容许咱们编写终止当前用户链接的逻辑。在个人代码中,当用户离开文档时,会向其他用户广播消息:

connection.on('close', function(connection) {
    console.log((new Date()) + " Peer " + userID + " disconnected.");
    const json = { type: typesDef.USER_EVENT };
    userActivity.push(`${users[userID].username} left the document`);
    json.data = { users, userActivity };
    delete clients[userID];
    delete users[userID];
    sendMessage(JSON.stringify(json));
  });

该应用程序的源代码位于GitHub上的 repo 中。

结论

WebSockets 是在应用中实现实时功能的最有趣和最方便的方法之一。它为咱们提供了可以充分利用全双工通讯的灵活性。我强烈建议在尝试使用 Socket.IO 和其余可用库以前先试试 WebSockets。

编码快乐!😊


本文首发微信公众号:前端先锋

欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章

欢迎扫描二维码关注公众号,天天都给你推送新鲜的前端技术文章


欢迎继续阅读本专栏其它高赞文章: