翻译:疯狂的技术宅javascript
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 长轮询生命周期以下:git
长轮询中存在不少漏洞 —— 标头开销、延迟、超时、缓存等等。github
这种机制减小了网络延迟的痛苦,由于初始请求无限期地保持打开状态。即便在服务器推送数据以后,请求也永远不会终止。 HTTP 流中的前三步生命周期方法与 HTTP 轮询是相同的。web
可是,当响应被发送回客户端时,请求永远不会终止,服务器保持链接打开状态,并在发生更改时发送新的更新。json
使用 SSE,服务器将数据推送到客户端。聊天或游戏应用不能彻底依赖 SSE。 SSE 的完美用例是相似 Facebook 的新闻 Feed:每当有新帖发布时,服务器会将它们推送到时间线。 SSE 经过传统 HTTP 发送,而且对打开的链接数有限制。
这些方法不只效率低下,维护它们的代码也使开发人员感到厌倦。
WebSockets 旨在取代现有的双向通讯技术。当涉及全双工实时通讯时,上述现有方法既不可靠也不高效。
WebSockets 相似于 SSE,但在将消息从客户端传回服务器方面也很优秀。因为数据是经过单个 TCP 套接字链接提供的,所以链接限制再也不是问题。
正如介绍中所提到的,WebSocket 协议只有两个议程。让咱们看看 WebSockets 如何实现这些议程。为此我将分析一个 Node.js 服务器并将其链接到使用 React.js 构建的客户端上。
咱们能够用单个端口来分别提供 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 协议的第二个议程。
我将编写一个基本的实时文档编辑器,用户能够将它们链接在一块儿并编辑文档。我跟踪了两个事件:
该协议容许咱们用二进制数据或 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。
编码快乐!😊