天天看點

WebSocket 前世今生 ?建立 WebSocket 的 6 種方式?

作者:前端進階

大家好,很高興又見面了,我是"前端進階",由我帶着大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點贊、收藏、轉發!

WebSocket 前世今生 ?建立 WebSocket 的 6 種方式?

什麼是WebSocket?

前言

WebSocket 允許建立“實時”應用程式,這些應用程式比傳統的 API 協定更快、開銷也更小。 WebSocket有時被稱為高端計算機通信協定,通過 WebSocket 來建立用戶端-伺服器通信通道。本文将深入探讨 WebSocket的起源?WebSocket 是什麼?它是如何工作的?常見的 建立WebSocket 連接配接的6個通用的庫。

1.WebSocket出現之前的世界?

1.1 WebSockets 之前:使用 JavaScript 編寫 Web 腳本

1995 年,Netscape Communications 聘請了 Brendan Eich,目标是将腳本功能嵌入到其 Netscape Navigator 浏覽器中,JavaScript 就這樣誕生了。

微軟也很快憑借 Internet Explorer 浏覽器進入了賽場,這是最初的浏覽器大戰真正開始的地方。 兩家公司都在争奪誰能擷取最好浏覽器的寶座,是以不可避免地要定期向 Netscape 和 Internet Explorer 添加新特性和功能。

WebSocket 前世今生 ?建立 WebSocket 的 6 種方式?

浏覽器大戰

1.2 WebSockets 之前:XMLHttpRequest 和 AJAX 的誕生

當時引入的兩個最重要的功能是将 Java applets嵌入頁面的能力,以及 Microsoft 的ActiveX 控件。

兩者本質上是預編譯的元件,可以選擇在網頁中呈現自己的嵌入式使用者界面。

雖然通過 Java applets提供了一些類似的網絡功能,但最重要的背景通信功能首次出現在 1999 年,即 Microsoft XMLHTTP ActiveXObject 接口。 它在 Internet Explorer 5.0 中原生支援,無需安裝插件,可以用一行 JavaScript 執行個體化,并且在處理 Java applets時不會帶來任何問題。

XMLHTTP 對象可以靜默地向伺服器送出請求并接收響應,所有這些功能都無需重新加載頁面或以其他方式中斷使用者操作。 然後 JavaScript 代碼可以處理響應并對頁面進行修改,進而将大量豐富的體驗內建到網站中。

常見的早期用例包括允許下拉框根據使用者先前的輸入填充選項,以及在填寫使用者系統資料庫時“即時”驗證使用者名可用性。

比如下面用于執行個體化 XMLHTTP 對象的示例 JavaScript 代碼:

// 注意:ActiveXObject是老版本IE浏覽器的私有屬性,大多數浏覽器并不支援
const xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.open("GET", "/api/demostration", true);
xmlhttp.onreadystatechange = function () {
  if (xmlhttp.readyState == 4) {
    alert(xmlhttp.responseText);
  }
};
xmlhttp.send(null);           

XMLHTTP 後來由于被其他浏覽器采用而成為 XMLHttpRequest 事實上的标準。 這大約是創造術語“AJAX”的時間,AJAX代表“異步 JavaScript 和 XML”。下面是使用标準 XMLHttpRequest 對象的示例:

const req = new XMLHttpRequest();
req.addEventListener("load", () => console.log(this.responseText));
req.open("GET", "/api/demostration");
req.send();           

與以前代碼結構基本類似,隻是代碼更加簡潔。但是,無論如何,XMLHttpRequest 仍然遵循用于檢索原始 HTML 文檔的相同 HTTP 請求-響應模型。 沒有允許伺服器主動連接配接使用者或為更複雜的用例建立任何類型的通用雙向連接配接的能力。

盡管如此,JavaScript一直在添加新的特性和功能,浏覽器也在增強文檔對象模型 (DOM)。 這導緻在如何使用 JavaScript 來豐富使用者與網頁互動的體驗方面具有越來越大的潛力。

1.3 孵化實時網絡

當某件事在技術上可行,并且有較高的ROI時,通常會竭盡全力。是以,開發人員開始使用 XMLHttpRequest 來模拟浏覽器和伺服器之間的實時通信。

這些技術中最常見的是長輪詢, 打開與伺服器的 XMLHttpRequest 連接配接并保持打開狀态,直到不再需要通信後關閉!

下面是HTTP長輪詢的大緻流程:

WebSocket 前世今生 ?建立 WebSocket 的 6 種方式?

HTTP長輪詢

在沒有其他合适工具的情況下,對于 Web 應用程式開發人員來說,長輪詢很難正确執行,并且充滿了必須管理的意外複雜情況, 其他 Comet 技術也是如此。

2.WebSocket 橫空出世?

WebSockets 于 2008 年首次出現,并在 2010 年左右開始獲得廣泛的浏覽器支援。

如上文所示,在 WebSockets 出現之前,“實時”網絡已經存在,但它很難實作,速度較慢,并且是通過hack并非為實時應用程式設計的現有網絡技術來實作的。

網絡是建立在 HTTP 協定的基礎上,該協定最初被設計為一種請求-響應機制。 使用者打開一個連接配接,描述想要的東西,得到一個響應,然後關閉連接配接。
WebSocket 前世今生 ?建立 WebSocket 的 6 種方式?

Websocket建立流程

WebSocket 是一種全雙工協定,主要用于用戶端-伺服器通信。 它本質上是雙向的,這意味着通信在用戶端-伺服器之間來回發生。

使用 WebSocket 開發的連接配接會持續到任何g參與方停止連接配接為止。 一旦一方斷開連接配接,另一方将無法通信,因為連接配接會在前端自動斷開。

WebSocket 需要 HTTP 的支援來發起連接配接,當涉及到無縫資料流和各種不同步流量時,它是現代 Web 應用程式開發的支柱。

3.WebSocket的場景覆寫?

WebSocket 是一種必不可少的用戶端-伺服器通信工具,需要充分了解其作用和适用場景才能更好的使用它。在以下情況下可以使用 WebSocket:

‍開發實時網絡應用程式

WebSocket 最常見的用途是在實時應用程式開發中,它有助于在用戶端持續顯示資料。 由于後端伺服器不斷發回此資料,WebSocket 允許在已打開的連接配接中不間斷地推送或傳輸此資料。 WebSockets 的使用使此類資料傳輸更快,并利用了應用程式的性能。

‍建立聊天應用程式

聊天應用程式開發人員在一次性交換和釋出/廣播消息等操作中調用 WebSocket 尋求幫助。 由于使用相同的 WebSocket 連接配接來發送/接收消息,是以通信變得簡單快捷。

遊戲應用程式

在遊戲應用程式開發過程中,伺服器必須不間斷地接收資料,而不需要重新整理 UI。 WebSocket 在不影響遊戲應用程式 UI 的情況下實作了這一目标。

4.Websocket vs HTTP的異同?

由于 HTTP 和 WebSocket 都用于應用程式通信,是以人們常常感到困惑,很難從這兩者中選擇。 如前所述,WebSocket 是一種雙向協定。 與此相反,HTTP 是一種單向協定,作用于 TCP 協定之上。

由于WebSocket協定能夠支援不間斷的資料傳輸,是以主要用于實時應用開發。 HTTP 是無狀态的,用于開發 RESTful 和 SOAP 應用程式。

SOAP 仍然可以使用 HTTP 來實作,但是 REST 被廣泛傳播和使用。
WebSocket 前世今生 ?建立 WebSocket 的 6 種方式?

Websocket和HTTP連接配接建立流程

在 WebSocket 中,通信發生在雙端,這使其成為速度更快的協定。 在 HTTP 中,連接配接隻能在一端建立,這使得它比 WebSocket 更慢。

WebSocket 使用統一的 TCP 連接配接,需要一方終止連接配接。 在它發生之前,連接配接保持持續活動狀态。 HTTP 需要為單獨的請求建立不同的連接配接。 請求完成後,連接配接會自動斷開。

5.WebSocket 連接配接是如何建立的?

該過程從涉及使用新方案 ws 或 wss 的 WebSocket 握手開始。 為了快速了解,您可以将它們分别視為 HTTP 和 HTTPS。

WebSocket 前世今生 ?建立 WebSocket 的 6 種方式?

注意:HTTP和WebSocket相交部分表示,WebSocket的握手階段需要HTTP參與

使用此方案,伺服器和用戶端應遵循标準的 WebSocket 連接配接協定。 WebSocket 連接配接建立從 HTTP 請求更新開始,它具有幾個标頭,例如: Connection: Upgrade、Upgrade: WebSocket、Sec-WebSocket-Key 等。下面是這個連接配接是如何建立的大緻流程。

注意:WebSocket的握手階段需要HTTP參與,連接配接建立後将會自動切換為WebSocket協定傳輸

4.1 請求階段

Connection: Upgrade :表示 WebSocket 握手,而 Sec-WebSocket-Key 表示 Base64 編碼的随機值, 該值是在每次 WebSocket 握手期間随機生成的。 除了上述之外,Key标頭也是此請求的一部分。

上面列出的标頭組合起來就形成了一個 HTTP GET 請求。 它将包含類似的資料:

GET ws://websocketexample.com:8181/ HTTP/1.1
Host: localhost:8181
Connection: Upgrade
// 握手
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
// 更新為websocket協定
Sec-WebSocket-Version: 13
// 協定版本
Sec-WebSocket-Key: b6gjhT32u488lpuRwKaOWs==
// 随機生成           

Sec-WebSocket-Version表示可供用戶端使用的 WebSocket 協定版本。

4.2 響應階段

響應标頭 Sec-WebSocket-Accept 具有在 Sec-WebSocket-Key 請求标頭中同等的作用。 這與特定的協定規範有關,并被廣泛用于防止誤導資訊。 換句話說,它增強了 API 的安全性,并阻止配置不當的伺服器在應用程式開發中造成失誤。

在先前發送的請求成功後,将收到類似于下面提到的文本序列的響應:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
// 更新成功
Sec-WebSocket-Accept: rG8wsswmHTJ85lJgAE3M5RTmcCE=           

在用戶端、服務端建立連接配接的安全性方面,可以通過下面一段話進行總結:

WebSocket沒有規定伺服器可以在握手期間對用戶端進行身份驗證的任何特定方式。 WebSocket 伺服器可以使用通用 HTTP 伺服器可用的任何用戶端身份驗證機制,例如 cookie、HTTP 身份驗證或 TLS 身份驗證!

5.幾個可用的WebSocket庫?

5.1 ws

ws 是一個“簡單易用、快速且經過全面測試的 Node.js WebSocket 用戶端和伺服器”。 它絕對是一個标準實作,旨在完成協定的所有工作。 但是,連接配接恢複、釋出/訂閱等其他功能是您必須自己管理的問題。

以下是使用 Node.js 和 WebSockets 建構實時應用程式時需要實作的代碼。

用戶端

const WebSocket = require('ws');
// 導入
const ws = new WebSocket('ws://www.host.com/path');
ws.on('open', function open() {
  ws.send('something');
});
// 監聽
ws.on('message', function incoming(data) {
  console.log(data);
});
           

服務端

const WebSocket = require('ws');
// 導入擷取Server子產品
constcons wss = new WebSocket.Server({ port: 8080 });
// 啟動WebSocket伺服器
wss.on('connection', function connection(ws) {
  ws.on('message', function incoming(message) {
    console.log('received: %s', message);
  });
  ws.send('something');
});           

5.2 μWebSockets

μWS 是 ws 的直接替代品,注重性能和穩定性。 據我所知,μWS 是最快的 WebSocket 伺服器實作, 它由 SocketCluster 在背景使用。

const WebSocketServer = require('uws').Server;
// 擷取Server子產品
const wss = new WebSocketServer({ port: 3000 });
function onMessage(message) {
  console.log('received: ' + message);
}
// 監聽connection
wss.on('connection', function(ws) {
  ws.on('message', onMessage);
  ws.send('something');
});           

5.3 faye-websocket

faye-websocket 是用戶端和伺服器的标準 WebSocket 實作,作為 Faye 項目的一部分,起源于 Ruby-on-Rails 社群。

在下面的伺服器示例代碼中,可以看到處理連接配接更新和從入站套接字緩沖區轉換消息幀的所有工作都由庫提供的 WebSocket 類處理。 與其他最小化解決方案一樣,這是一個簡潔的實作——您需要自己處理特定于應用程式的問題。

用戶端代碼

const WebSocket = require('faye-websocket'),
// 導入擷取Client子產品
const ws = new WebSocket.Client('ws://www.example.com/');
ws.on('open', function(event) {
  console.log('open');
  ws.send('Hello, world!');
});
ws.on('message', function(event) {
  console.log('message', event.data);
});
ws.on('close', function(event) {
  console.log('close', event.code, event.reason);
  ws = null;
});           

服務端代碼

const WebSocket = require('faye-websocket'),
const http = require('http');
const server = http.createServer();
// 導入擷取Server子產品
server.on('upgrade', function(request, socket, body) {
  if (WebSocket.isWebSocket(request)) {
    var ws = new WebSocket(request, socket, body);
    ws.on('message', function(event) {
      ws.send(event.data);
    });
    ws.on('close', function(event) {
      console.log('close', event.code, event.reason);
      ws = null;
    });
  }
});
server.listen(8000);           

5.4 Socket.io

Socket.IO 已經存在了一段時間,可以被認為是 WebSockets 的“jQuery”。 它使用長輪詢和 WebSockets 進行傳輸,預設情況下從長輪詢開始,然後更新到 WebSockets。

長輪詢已經基本不需要,Socket.IO 如今的主要吸引力是它的主動斷連能力、自動支援 JSON 和“命名空間”,它們本質上是在同一用戶端連接配接上多路複用的隔離消息通道。

Socket.IO 實際上不能與通用的 WebSockets 解決方案互換,無論是在伺服器端還是在用戶端。它有自己的附加握手協定,每條消息中都包含一些附加中繼資料。

用戶端代碼

const io = require('socket.io-client');
const socket = io();
socket.emit('chat message', 'Hello there');           

服務端代碼

const app = require('express')();
const http = require('http').Server(app);
// 啟動server并傳遞給socket.io子產品
const io = require('socket.io')(http);
app.get('/', function(req, res){
  res.sendFile(__dirname + '/index.html');
});
io.on('connection', function(socket){
  console.log('a user connected');
});
http.listen(3000, function(){
  console.log('listening on *:3000');
});           

5.5 SocketCluster

SocketCluster 是一個功能齊全的用戶端-伺服器消息傳遞架構,完全圍繞 WebSockets 建構,并在底層使用 μWebSockets。

與 Socket.IO 等更簡單的解決方案相比,SocketCluster 需要稍微多一些安裝,但通常很容易啟動和運作。

用戶端代碼

const socket = socketCluster.create();
socket.emit('sampleClientEvent', {message: 'This is an object with a message property'});           

服務端代碼

const SocketCluster = require('socketcluster');
// 關于執行個體化參數的具體含義可以閱讀文檔
const socketCluster = new SocketCluster({
  workers: 1, 
  // Number of worker processes
  brokers: 1, 
  // Number of broker processes
  port: 8000, 
  // The port number on which your server should listen
  appName: 'myapp', 
  // A unique name for your app
  // Switch wsEngine to 'sc-uws' for a MAJOR performance boost (beta)
  wsEngine: 'ws',
  /* A JS file which you can use to configure each of your
   * workers/servers - This is where most of your backend code should go
   */
  workerController: __dirname + '/worker.js',
  /* JS file which you can use to configure each of your
   * brokers - Useful for scaling horizontally across multiple machines (optional)
   */
  brokerController: __dirname + '/broker.js',
  // Whether or not to reboot the worker in case it crashes (defaults to true)
  rebootWorkerOnCrash: true
});           

5.6 SocketCluster

SockJS 是一個浏覽器 JavaScript 庫,它提供了一個類似于 WebSocket 的對象。SockJS 為您提供了一個連貫的、跨浏覽器的 Javascript API,它在浏覽器和 Web 伺服器之間建立了一個低延遲、全雙工、跨域的通信通道”。

用戶端代碼

const sock = new SockJS('https://mydomain.com/my_prefix');
sock.onopen = function() {
  console.log('open');
  sock.send('test');
};
sock.onmessage = function(e) {
  console.log('message', e.data);
  sock.close();
};
sock.onclose = function() {
  console.log('close');
};           

服務端代碼

const http = require('http');
const sockjs = require('sockjs');
const echo = sockjs.createServer({ sockjs_url: 'http://cdn.jsdelivr.net/sockjs/1.0.1/sockjs.min.js' });
// 建立Server
echo.on('connection', function(conn) {
  conn.on('data', function(message) {
    conn.write(message);
  });
  conn.on('close', function() {});
});

var server = http.createServer();
echo.installHandlers(server, {prefix:'/echo'});
server.listen(9999, '0.0.0.0');
           

參考資料

https://www.wallarm.com/what/a-simple-explanation-of-what-a-websocket-is

https://ably.com/topic/websockets

https://github.com/uNetworking/uWebSockets

https://github.com/socketio/socket.io

繼續閱讀