問題:用戶端與伺服器之間的連線延遲時間過長
網路大部分都是根據 HTTP 要求/回應模式而建置。用戶端先載入網頁,使用者接著點選下一頁,系統才會啟動瀏覽器。大約在 2005 年左右,AJAX 開始讓網頁變得更具動態性。不過,所有 HTTP 通訊都由用戶端引導,因此需要使用者互動或定期輪詢,才能從伺服器載入新資料。
讓伺服器在知道有新資料可用時,立即將資料傳送至用戶端的技術,早已存在一段時間了。名稱格式為「Push」或「Comet」。其中一個最常見的駭客攻擊手法,就是讓使用者誤以為是伺服器啟動的連線,這類攻擊手法稱為長時間輪詢。使用長時間輪詢時,用戶端會開啟與伺服器的 HTTP 連線,並在傳送回應前保持連線狀態。每當伺服器實際收到新的資料時,就會傳送回應 (其他技術涉及 Flash、XHR 多部分要求,稱為 htmlfiles)。長時間輪詢和其他技巧都能順暢運作。您每天在 Gmail 聊天等應用程式中使用這些字詞。
不過,所有這些解決方法都存在一個問題:它們都會產生 HTTP 的額外負擔,因此不適合用於低延遲應用程式。您可以用瀏覽器或是任何其他具備即時元件的線上遊戲,來設計多人對戰的第一人稱射擊遊戲。
WebSocket 簡介:將網路連線引進網路
WebSocket 規格定義了在網路瀏覽器和伺服器之間建立「Socket」連線的 API。簡單來說,用戶端與伺服器之間維持永久連線,且雙方都能隨時開始傳送資料。
開始使用
只要呼叫 WebSocket 建構函式,即可開啟 WebSocket 連線:
var connection = new WebSocket('ws://html5rocks.websocket.org/echo', ['soap', 'xmpp']);
請注意 ws:
。這是 WebSocket 連線的新網址結構定義。wss:
也適用於安全的 WebSocket 連線,就像 https:
可用於安全的 HTTP 連線一樣。
您可以立即將部分事件處理常式附加至連線,以便瞭解連線何時開啟、收到傳入訊息或發生錯誤。
第二個引數接受選用的子通訊協定。可以是字串或字串陣列。每個字串都應代表子通訊協定名稱,且伺服器只會接受陣列中傳遞的其中一個子通訊協定。您可以存取 WebSocket 物件的 protocol
屬性,判斷系統接受哪種子通訊協定。
子協定名稱必須是 IANA 註冊資料庫中已註冊的子協定名稱之一。截至 2012 年 2 月,目前只註冊了一個子通訊協定名稱 (soap)。
// When the connection is open, send some data to the server
connection.onopen = function () {
connection.send('Ping'); // Send the message 'Ping' to the server
};
// Log errors
connection.onerror = function (error) {
console.log('WebSocket Error ' + error);
};
// Log messages from the server
connection.onmessage = function (e) {
console.log('Server: ' + e.data);
};
與伺服器通訊
只要我們與伺服器建立連線 (open
事件觸發時),就可以開始使用連線物件的 send('your message')
方法,將資料傳送至伺服器。它原本只支援字串,但在最新規格中,現在也能傳送二進位訊息。如要傳送二進位資料,您可以使用 Blob
或 ArrayBuffer
物件。
// Sending String
connection.send('your message');
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
binary[i] = img.data[i];
}
connection.send(binary.buffer);
// Sending file as Blob
var file = document.querySelector('input[type="file"]').files[0];
connection.send(file);
同樣地,伺服器也可能隨時傳送訊息給我們。發生這種情況時,系統會觸發 onmessage
回呼。回呼會接收事件物件,您可以透過 data
屬性存取實際訊息。
WebSocket 也可以接收符合最新規格的二進位訊息。支援二進位影格的接收格式為 Blob
或 ArrayBuffer
。如要指定收到的二進位檔格式,請將 WebSocket 物件的 binaryType 屬性設為「blob」或「arraybuffer」。預設格式為「blob」。(您不需要在傳送時對齊 binaryType 參數。)
// Setting binaryType to accept received binary as either 'blob' or 'arraybuffer'
connection.binaryType = 'arraybuffer';
connection.onmessage = function(e) {
console.log(e.data.byteLength); // ArrayBuffer object if binary
};
WebSocket 的另一項新功能是擴充功能。使用擴充功能,您可以傳送壓縮、多工序等影格。您可以在開啟事件後檢查 WebSocket 物件的 extensions 屬性,找出伺服器接受的擴充功能。截至 2012 年 2 月,尚未發布任何官方的擴充功能規格。
// Determining accepted extensions
console.log(connection.extensions);
跨來源通訊
做為新型通訊協定,跨來源通訊已內建至 WebSocket。雖然您仍應確保只與信任的用戶端和伺服器通訊,但 WebSocket 可讓任何網域中的各方進行通訊。伺服器會決定是否要將服務提供給所有用戶端,或是只提供給位於一組明確定義的網域的用戶端。
Proxy 伺服器
每項新技術都會帶來一連串新問題。就 WebSocket 而言,這指的是與 Proxy 伺服器的相容性,後者會在大多數公司網路中調解 HTTP 連線。WebSocket 通訊協定會使用 HTTP 升級系統 (通常用於 HTTP/SSL),將 HTTP 連線「升級」為 WebSocket 連線。部分 Proxy 伺服器會因此失去連線。因此,即使特定用戶端使用 WebSocket 通訊協定,也可能無法建立連線。因此,下一節就顯得更加重要了 :)
立即使用 WebSocket
WebSocket 仍然是一項年輕技術,並未在所有瀏覽器中全面導入。不過,您現在可以使用 WebSocket,並搭配使用上述備用方案之一的程式庫,以便在 WebSocket 無法使用時進行備用。在這個領域中,socket.io 程式庫相當受歡迎,其中包含用戶端和伺服器的通訊協定實作方式,並提供備用方案 (截至 2012 年 2 月,socket.io 尚不支援二進位訊息傳送)。另外還有 PusherApp 等商業解決方案,只要提供 HTTP API 來傳送 WebSocket 訊息給用戶端,即可輕鬆整合至任何網路環境。由於需要額外的 HTTP 要求,因此相較於純 WebSocket,這項方法一律會產生額外的額外負擔。
伺服器端
使用 WebSocket 可為伺服器端應用程式建立全新的使用模式。雖然 LAMP 等傳統伺服器堆疊是以 HTTP 要求/回應週期為設計基礎,但通常無法妥善處理大量的開放 WebSocket 連線。如要同時保持大量連線,就必須採用可在低效能成本下接收大量並行作業的架構。這類架構通常是圍繞著執行緒或所謂的非阻斷式 I/O 設計。
伺服器端導入
- Node.js
- Java
- Ruby
- Python
- Erlang
- C++
- .NET
通訊協定版本
WebSocket 的線路通訊協定 (握手和用戶端與伺服器之間的資料傳輸) 現為 RFC6455。最新版 Chrome 和 Chrome 適用於 Android 的版本,完全相容於 RFC6455,包括二進位訊息。此外,Firefox 將支援 11 版,Internet Explorer 則支援 10 版。您仍然可以使用舊版通訊協定,但我們不建議這麼做,因為已知版本很有安全漏洞。如果您已安裝適用於舊版 WebSocket 通訊協定的伺服器,建議將其升級至最新版本。
用途
只要需要在用戶端和伺服器之間建立低延遲、近乎即時的連線,就請使用 WebSocket。請注意,這可能需要重新思考如何建構伺服器端應用程式,並將重點放在事件佇列等新技術上。以下是一些用途範例:
- 多人線上遊戲
- 即時通訊應用程式
- 即時運動賽事資訊列
- 即時更新社群媒體動態串流