問題:用戶端與伺服器間的連線延遲時間過長
網路主要是以 HTTP 的要求/回應模式為基礎建構而成。用戶端載入網頁後,在使用者點選下一個網頁前,不會發生任何事。2005 年左右,AJAX 開始讓網路變得更動態。不過,所有 HTTP 通訊仍由用戶端主導,需要使用者互動或定期輪詢,才能從伺服器載入新資料。
伺服器在得知有新資料可用時,立即將資料傳送至用戶端的技術,已存在相當長的一段時間。這類惡意軟體通常稱為「Push」或「Comet」。為了營造伺服器發起連線的假象,最常見的做法之一是長輪詢。使用長輪詢時,用戶端會開啟與伺服器的 HTTP 連線,伺服器會保持連線開啟,直到傳送回應為止。只要伺服器有新資料,就會傳送回應 (其他技術包括 Flash、XHR 多部分要求和所謂的 htmlfiles)。長輪詢和其他技術的效果相當好。您每天都會在 Gmail 即時通訊等應用程式中使用這些功能。
不過,這些解決方法都有一個共同問題:會產生 HTTP 負擔,因此不適合低延遲應用程式。例如瀏覽器中的多人第一人稱射擊遊戲,或任何其他具有即時元件的線上遊戲。
WebSocket 簡介:將通訊端帶入網路
WebSocket 規格定義了在網路瀏覽器和伺服器之間建立「通訊端」連線的 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 物件的擴充功能屬性。截至 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 連線。如要同時保持大量連線開啟,必須採用能以低效能成本接收高並行性的架構。這類架構通常是圍繞執行緒或所謂的非阻塞 IO 設計。
伺服器端實作
- Node.js
- Java
- Ruby
- Python
- Erlang
- C++
- .NET
通訊協定版本
WebSocket 的線路通訊協定 (用戶端與伺服器之間的交握和資料移轉) 現在是 RFC6455。最新版 Chrome 和 Android 版 Google Chrome 與 RFC6455 完全相容,包括二進位訊息。此外,Firefox 11 和 Internet Explorer 10 也能相容。您仍可使用舊版通訊協定,但由於這些版本已知有安全漏洞,因此不建議使用。如果您有舊版 WebSocket 通訊協定的伺服器實作,建議您升級至最新版本。
用途
如需在用戶端和伺服器之間建立低延遲、近乎即時的連線,請使用 WebSocket。請注意,這可能需要重新思考如何建構伺服器端應用程式,並將重點放在事件佇列等技術。部分應用實例如下:
- 多人線上遊戲
- Chat 應用程式
- 體育賽事即時資訊
- 即時更新社群媒體串流