翻譯:瘋狂的技術宅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。
編碼快樂!😊