Node和React中怎样停止实时通讯?
教程引荐:node js教程、React教程、WebSocket教程
Web 为了支撑客户端和效劳器之间的全双工(或双向)通讯已经走过了很长的路。这是 WebSocket 和谈的主要目的:通过单个 TCP 套接字连接在客户端和效劳器之间供给耐久的实时通讯。
WebSocket 和谈只要两个议程:1)翻开握手,2)帮忙数据传输。一旦效劳器和客户端握手成功,他们就可以随便地以较少的开销彼此发送数据。
WebSocket 通讯使用WS(端口80)或WSS(端口443)和谈在单个 TCP 套接字上停止。按照 Can I Use,撰写本文时除了 Opera Mini 之外几乎所有的阅读器支撑 WebSockets 。
近况
从历史上看,创立需要实时数据通讯(如游戏或谈天利用程序)的 Web 利用需要滥用 HTTP 和谈来创立双向数据传输。尽管有很多种办法用于实实际时功效,但没有一种办法与 WebSockets 一样高效。 HTTP 轮询、HTTP流、Comet、SSE —— 它们都有本人的缺陷。
HTTP 轮询
解决问题的第一个尝试是按期轮询效劳器。 HTTP 长轮询生命周期如下:
- 客户端发出恳求并不断等候响应。
- 效劳器延迟响应,直到发生更换、更新或超时。恳求保持“挂起”,直到效劳器有东西返回客户端。
- 当效劳器端有一些更换或更新时,它会将响应发送回客户端。
- 客户端发送新的长轮询恳求以侦听下一组更换。
长轮询中存在许多破绽 —— 标头开销、延迟、超时、缓存等等。
HTTP 流式传输
这种机制减少了网络延迟的疾苦,由于初始恳求无穷期地保持翻开状态。即便在效劳器推送数据之后,恳求也永久不会终止。 HTTP 流中的前三步生命周期办法与 HTTP 轮询是雷同的。
但是,当响应被发送回客户端时,恳求永久不会终止,效劳器保持连接翻开状态,并在发生更换时发送新的更新。
效劳器发送事件(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:实时信息传输
我将编写一个根本的实时文档编纂器,会员可以将它们连接在一起并编纂文档。我跟踪了两个事件:
- 会员活动:每次会员参加或分开时,我都会将新闻播送给所有连接其他的客户端。
- 内容更换:每次修改编纂器中的内容时,都会向所有连接的其他客户端播送。
该和谈同意我们用二进制数据或 UTF-8 发送和接收新闻(留意:传输和转换 UTF-8 的开销较小)。
只要我们对套接字事件onopen
、onclose
和 onmessage
有了充分的理解,懂得和实现 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)); } });
将新闻播送到所有连接的客户端。
阅读器关闭后会发生什么?
在这种状况下,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。
编码欢乐!