使用 WebSocket 構建實時性應用

WebSocket 技術已經逐漸成熟,在生產環境下也已經帶給咱們很是多的便利。本文首先會努力闡明 WebSocket 的基本原理,而後會結合實際敘述如何使用它。javascript


WebSocket 不會徹底取代 HTTP

首先須要明確的是 WebSocket 的定位。WebSocket 是創建在 HTTP 基礎上,爲客戶端與服務端之間提供文本和二進制數據的全雙工通訊的技術。這裏有幾個地方須要注意:html

  • 創建在 HTTP 基礎上。WebSocket 須要在創建了 HTTP 鏈接以後,客戶端才能發起 WebSocket 「握手」請求,握手成功後客戶端與服務端才能進行 WebSocket 通訊
  • 提供文本(Text)和二進制數據(Binary Data)兩種數據的傳輸
  • 全雙工通訊。一旦確立 WebSocket 通訊鏈接,不管是客戶端仍是瀏覽器,任意一方均可以想對方發送消息,即實現了服務器想客戶端推送數據的功能

WebSocket 是爲了知足基於 Web 的日益增加的實時通訊需求而產生的。在傳統的 Web 中,要實現實時通訊(好比網頁版 QQ),經常使用的方式是經過輪詢。在特定的時間間隔,由瀏覽器向服務器發送 HTTP 請求,而後將最新的數據返回給瀏覽器。這樣的方法最明顯的缺點就是須要不斷的發送請求,不只佔用了寬帶,也佔用服務器 CPU 資源(沒有信息也要接受請求)。java

如此看來,WebSocket 的出現相比於純 HTTP 實現實時通訊至少有兩個優點:node

  • 實時性提升,只要 WebSocket 鏈接創建,就能夠一直保持鏈接狀態,發送消息時不須要額外的鏈接創建過程,通訊的實時性大大提升
  • 總數據流量下降,WebSocket 的首部相比於 HTTP 首部的體積要小的多,尤爲是發送消息次數較多,這會節省下至關客觀的數據流量

然而,WebSocket 在實現高效實時通訊的過程卻也再也不享有在一些本由瀏覽器提供的服務和優化,如狀態管理、壓縮、緩存等。之後瀏覽器廠商是否是會針對 WebSocket 做出一些服務和優化不得而知,但至少如今 WebSocket 還徹底不足以撼動 HTTP 的地位。總的來講,WebSocket 彌補了 HTTP 在某些通訊領域的短板,但毫不可能徹底取代 HTTP。react


WebSocket API

WebSocket 本來是 HTML5 標準的一部分,隨其發展壯大,如今已逐漸變成了獨立的協議標準。在客戶端,HTML5 提供了一套很是簡潔的 API 供咱們使用。git

構造方法

WebSocket(url: string, protocols?: string[] | string)
複製代碼
  • url 表示要鏈接的 URL。注意協議名是 ws 或者 wss
  • protocols 能夠是一個單個的協議名字字符串或者包含多個協議名字字符串的數組。這些字符串用來表示子協議,這樣作可讓一個服務器實現多種 WebSocket 子協議(例如你可能但願經過制定不一樣的協議來處理不一樣類型的交互)。若是沒有制定這個參數,它會默認設爲一個空字符串。

原型方法

一共就只有兩個。github

close(code?: number, reason?: string): void
複製代碼

關閉WebSocket鏈接或中止正在進行的鏈接請求。若是鏈接的狀態已是closed,這個方法不會有任何效果。web

  • code 表示關閉鏈接的狀態號,表示鏈接被關閉的緣由。若是這個參數沒有被指定,默認的取值是 1000 ,即正常鏈接關閉。更多取值查看 CloseEvent 頁面。
  • reason 一個描述性的字符串,表示鏈接被關閉的緣由。
send(data: string | ArrayBuffer | Blob): void
複製代碼

經過WebSocket鏈接向服務器發送數據。express

  • data 表示要發送到服務器的數據,能夠是 String、ArrayBuffer 或者 Blob 類型。String 類型至關因而文本數據,ArrayBuffer 和 Blob 是二進制數據。

事件

事件聽從原生 Javascript 的兩種寫法:npm

  • onevent = handler
  • addEventListener(event, handler)

支持的事件有 openmessagecloseerror。注意 message 事件在接收到全部數據時出發。

屬性

WebSocket 對象的屬性用來描述通訊細節和狀態。

  • binaryType: string 表示被傳輸二進制的內容的類型。取值應當是 'blob' 或者 'arraybuffer'

    var ws = new WebSocket('wss://example.com/socket');
    ws.binaryType = "arraybuffer"; // 強制將接收的二進制數據轉爲 ArrayBuffer 類型
    
    ws.onmessage = function(msg) {
      if(msg.data instanceof ArrayBuffer) {
        processArrayBuffer(msg.data);
      } else {
        processText(msg.data);
      }
    }
    複製代碼
  • bufferedAmount: number 調用 send() 方法將多字節數據加入到隊列中等待傳輸,可是還未發出。該值會在全部隊列數據被髮送後重置爲 0。而當鏈接關閉時不會設爲0。若是持續調用 send(),這個值會持續增加。只讀。

  • protocol: string 一個代表服務器選定的子協議名字的字符串。這個屬性的取值爲構造器傳入的 protocols 參數或者之一。

  • readyState: number 鏈接的當前狀態。取值是 Ready state constants之一。 只讀。

  • url: string 傳入構造器的URL。它必須是一個絕對地址的URL。只讀。

最簡單的一個例子

// Create WebSocket connection.
const socket = new WebSocket('ws://localhost:8080');

// Connection opened
socket.addEventListener('open', function (event) {
  socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
  console.log('Message from server', event.data);
});
複製代碼

然而以上只是在客戶端的 WebSocket 實現,若是沒有服務端的配合,WebSocket 是不能進行通訊的。關於 WebSocket 的在服務端的實現有多種方案,下一節會在 Node.js 上進行實現。

以上部分是我的簡單總結,若是想詳細瞭解其 API 能夠查閱 MDN 上的文檔


WebSocket 的基本使用

使用 WebSocket 其實並不複雜,本節將結合 socket.io 模塊進行說明。接下來看看如何構建一個最簡單的 WebSocket 應用。

第一步,安裝依賴庫:

npm install socket.io express
複製代碼

第二步,構建客戶端 /public/index.html:

<body>
  <script src="/socket.io/socket.io.js"></script>
  <script> const socket = io('http://localhost:3231'); socket.on('connect', () => { socket.emit('greet', 'Hello, websocket!'); socket.on('uppergreet', data => console.log(data)); }); </script>
</body>
複製代碼

注意上面引入 socket.io 的方式,不用懷疑路徑是否正確,由於 socket.io 會自動引入相應客戶端部分代碼,而且暴露一個 io 全局變量。

第三步,構建服務端 server.js:

const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 3000;

app.use(express.static(__dirname + '/public'));

io.on('connection', socket => {
  socket.on('greet', data => socket.emit('uppergreet', data.toUpperCase()));
});

http.listen(port, () => console.log('listening on port ' + port));
複製代碼

最後,運行 node server.js 並用瀏覽器訪問 http://localhost:3000,打開控制檯將會看到輸出:

HELLO, WEBSOCKET!
複製代碼

能夠看到,使用 socket.io 構建 WebSocket 應用相比上一節提到的 WebSocket API 在寫法上仍是有比較大的區別的。

除了本節提到的 socket.io 模塊,在 Node.js 上還有多種實現,好比 websocketnodejs-websocket,他們不只實現了服務端的 WebSocket 通訊協議,同時也對上一節提到的客戶端的 WebSocket API 進行了或多或少的封裝,功能完善了不少,在實際工做中已經獲得了普遍應用,我的比較傾向於 socket.io 庫。


WebSocket 的適用場景

WebSocket 適用於須要高效實時通訊的場景,好比網頁聊天,對戰遊戲等。本節將使用 socket.io 模塊進行說明,在 socket.io 的官網上有多個典型使用場景,這裏咱們使用官網給出的較爲簡單的一個畫板案例進行說明。 socket.io 官網還給出一個使用 jQuery 開發的聊天應用示例,我根據其最終效果實現了一個 react 版本

畫板案例的關鍵之處是多個客戶端能夠同時在一張畫板上進行繪畫,繪畫結果全部客戶端都能實時看到,(這個場景是否是和你畫我猜的遊戲相似?一人畫圖,多個用戶能實時看到畫圖結果),這裏使用 WebSocket 是很是合適的。

首先是服務端的代碼 index.js,和上一節的代碼很是相似:

const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 3000;

app.use(express.static(__dirname + '/public'));

io.on('connection', socket => {
  socket.on('drawing', data => socket.broadcast.emit('drawing', data));
});

http.listen(port, () => console.log('listening on port ' + port));
複製代碼

接下來運行以下命令:

node index.js
複製代碼

而後打開兩個瀏覽器窗口都訪問 http://localhost:3000,而後在一個瀏覽器內進行繪畫,另一個瀏覽器也能實時看到結果。

能夠看到服務端代碼很是簡潔,在這裏關鍵步驟就是 connection 事件的處理方法,在該方法內部 socket 會監聽 drawing 事件,在監聽到 drawing 事件以後的回調函數中,再次廣播發送 drawing 事件,廣播發送的意思就是向全部連上服務端的客戶端發送。

而後是客戶端的關鍵代碼 /public/main.js:

var socket = io();
socket.on('drawing', onDrawingEvent);

var canvas = document.getElementsByClassName('whiteboard')[0];
canvas.addEventListener('mousedown', onMouseDown, false);
canvas.addEventListener('mouseup', onMouseUp, false);
canvas.addEventListener('mouseout', onMouseUp, false);
canvas.addEventListener('mousemove', throttle(onMouseMove, 10), false);
複製代碼

上面代碼中的 onDrawingEventmouseUponMouseMove 方法內部都調用了一個關鍵的 drawLine 方法:

function drawLine(x0, y0, x1, y1, color, emit){
  // ... 繪製 canvas ...

  if (!emit) return;

  socket.emit('drawing', {
    // ... 繪製 canvas 的狀態信息 ...
  });
}
複製代碼

drawLine 方法根據傳入的 emit 位來判斷是否要向服務器發送 drawing 事件,若是是客戶端自己進行的繪製,則會向服務端發送 drawing 事件,不然不會。

以上就是實現一個多人同時繪畫的實時應用的總體思路,能夠看到當咱們使用 WebSocket 進行通訊,尤爲是使用相似 socket.io 這種封裝程度很高的工具庫時,通訊過程是很是簡潔的,咱們把大部分精力放在業務邏輯中便可。


WebSocket 技術的發展已經讓開發者無須瞭解協議內部便可實現實時通訊邏輯,尤爲是配合使用 Node.js 做爲服務端時,相似 socket.io 的工具庫爲服務端和客戶端都提供了很是方便的調用方法,上手很是快。若是在工做中須要構建高效實時應用,WebSocket 將會是不二選擇。


參考資料

相關文章
相關標籤/搜索