在規格 RFC 6455 中所描述的 WebSocket
協定,提供了一種透過持續連線在瀏覽器和伺服器之間交換資料的方法。資料可以透過「封包」的形式雙向傳遞,而不會中斷連線,也不需要額外的 HTTP 要求。
WebSocket 特別適合需要持續資料交換的服務,例如線上遊戲、即時交易系統等。
一個簡單的範例
要開啟一個 websocket 連線,我們需要使用 url 中的特殊協定 ws
來建立 new WebSocket
let socket = new WebSocket("ws://javascriptinfo.dev.org.tw");
也有加密的 wss://
協定。它就像 websockets 的 HTTPS。
wss://
wss://
協定不僅加密,而且更可靠。
這是因為 ws://
資料未加密,任何中介者都可以看到。舊的代理伺服器不了解 WebSocket,它們可能會看到「奇怪」的標頭並中止連線。
另一方面,wss://
是透過 TLS 的 WebSocket(就像 HTTPS 是透過 TLS 的 HTTP),傳輸安全層會在傳送端加密資料,並在接收端解密資料。因此,資料封包會透過代理伺服器加密傳遞。它們看不到裡面的內容,並讓它們通過。
一旦建立 socket,我們應該監聽它的事件。總共有 4 個事件
open
– 建立連線,message
– 收到資料,error
– websocket 錯誤,close
– 連線關閉。
…如果我們想要傳送一些東西,那麼 socket.send(data)
就會執行此動作。
以下是一個範例
let socket = new WebSocket("wss://javascriptinfo.dev.org.tw/article/websocket/demo/hello");
socket.onopen = function(e) {
alert("[open] Connection established");
alert("Sending to server");
socket.send("My name is John");
};
socket.onmessage = function(event) {
alert(`[message] Data received from server: ${event.data}`);
};
socket.onclose = function(event) {
if (event.wasClean) {
alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
alert('[close] Connection died');
}
};
socket.onerror = function(error) {
alert(`[error]`);
};
為了示範目的,有一個使用 Node.js 編寫的小型伺服器 server.js,針對上面的範例執行。它會回應「Hello from server, John」,然後等待 5 秒並關閉連線。
因此,您將看到事件 open
→ message
→ close
。
實際上就是這樣,我們已經可以討論 WebSocket 了。很簡單,不是嗎?
現在讓我們更深入地討論。
開啟 websocket
當建立 new WebSocket(url)
時,它會立即開始連線。
在連線期間,瀏覽器(使用標頭)會詢問伺服器:「您是否支援 Websocket?」如果伺服器回答「是」,則會繼續使用 WebSocket 協定進行對話,這根本不是 HTTP。
以下是由 new WebSocket("wss://javascriptinfo.dev.org.tw/chat")
請求所建立的瀏覽器標頭範例。
GET /chat
Host: javascript.info
Origin: https://javascriptinfo.dev.org.tw
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Origin
– 來源客戶端頁面,例如https://javascriptinfo.dev.org.tw
。WebSocket 物件本質上是跨來源的。沒有特殊標頭或其他限制。舊伺服器無法處理 WebSocket,因此沒有相容性問題。但是Origin
標頭很重要,因為它允許伺服器決定是否與此網站對話 WebSocket。Connection: Upgrade
– 表示客戶端想要變更協定。Upgrade: websocket
– 請求的協定是「websocket」。Sec-WebSocket-Key
– 瀏覽器隨機產生的金鑰,用於確保伺服器支援 WebSocket 協定。它是隨機的,以防止代理伺服器快取任何後續通訊。Sec-WebSocket-Version
– WebSocket 協定版本,13 是目前的版本。
我們無法使用 XMLHttpRequest
或 fetch
來進行這種 HTTP 請求,因為 JavaScript 不允許設定這些標頭。
如果伺服器同意切換到 WebSocket,它應該傳送代碼 101 回應
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
這裡的 Sec-WebSocket-Accept
是 Sec-WebSocket-Key
,使用特殊演算法重新編碼。瀏覽器看到它之後,就會了解伺服器確實支援 WebSocket 協定。
之後,資料會使用 WebSocket 協定傳輸,我們很快就會看到它的結構(「框架」)。那完全不是 HTTP。
擴充功能和子協定
可能會出現其他標頭 Sec-WebSocket-Extensions
和 Sec-WebSocket-Protocol
,用來描述擴充功能和子協定。
例如
-
Sec-WebSocket-Extensions: deflate-frame
表示瀏覽器支援資料壓縮。擴充功能是與傳輸資料相關的項目,是延伸 WebSocket 協定的功能。標頭Sec-WebSocket-Extensions
會由瀏覽器自動傳送,其中包含它支援的所有擴充功能清單。 -
Sec-WebSocket-Protocol: soap, wamp
表示我們想要傳輸的不只是任何資料,而是 SOAP 或 WAMP(「WebSocket 應用程式訊息傳遞協定」)協定中的資料。WebSocket 子協定會註冊在 IANA 目錄 中。因此,這個標頭描述了我們要使用的資料格式。這個選用標頭會使用
new WebSocket
的第二個參數來設定。那是子協定的陣列,例如如果我們想要使用 SOAP 或 WAMPlet socket = new WebSocket("wss://javascriptinfo.dev.org.tw/chat", ["soap", "wamp"]);
伺服器應該回應它同意使用的協定和擴充功能清單。
例如,請求
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascriptinfo.dev.org.tw
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp
回應
101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap
這裡的伺服器回應它支援擴充功能「deflate-frame」,以及請求的子協定中只有 SOAP。
資料傳輸
WebSocket 通訊包含「框架」- 資料片段,可以從任一端傳送,而且可以有數種種類
- 「文字框架」- 包含各方彼此傳送的文字資料。
- 「二進位資料框架」- 包含各方彼此傳送的二進位資料。
- 「ping/pong 框架」用於檢查連線,由伺服器傳送,瀏覽器會自動回應這些框架。
- 還有「連線關閉框架」和一些其他服務框架。
在瀏覽器中,我們直接只使用文字或二進制框架。
WebSocket .send()
方法可以傳送文字或二進制資料。
呼叫 socket.send(body)
允許 body
為字串或二進制格式,包括 Blob
、ArrayBuffer
等。不需要任何設定:只要以任何格式傳送即可。
當我們收到資料時,文字總是會以字串形式出現。而對於二進制資料,我們可以在 Blob
和 ArrayBuffer
格式之間選擇。
這由 socket.binaryType
屬性設定,預設為 "blob"
,因此二進制資料會以 Blob
物件形式出現。
Blob 是一個高階二進制物件,它直接與 <a>
、<img>
和其他標籤整合,因此這是一個合理的預設值。但對於二進制處理,若要存取個別資料位元組,我們可以將其變更為 "arraybuffer"
socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
// event.data is either a string (if text) or arraybuffer (if binary)
};
速率限制
想像一下,我們的應用程式產生大量資料要傳送。但使用者的網路連線很慢,可能在行動網路中,在城市外。
我們可以一再呼叫 socket.send(data)
。但資料會緩衝(儲存)在記憶體中,並且只會以網路速度允許的速度傳送出去。
socket.bufferedAmount
屬性儲存目前有多少位元組仍處於緩衝狀態,等待透過網路傳送。
我們可以檢查它,看看 socket 是否實際上可供傳輸。
// every 100ms examine the socket and send more data
// only if all the existing data was sent out
setInterval(() => {
if (socket.bufferedAmount == 0) {
socket.send(moreData());
}
}, 100);
連線關閉
通常,當一方想要關閉連線時(瀏覽器和伺服器具有相同的權利),他們會傳送一個「連線關閉框架」,其中包含一個數字代碼和一個文字原因。
方法如下
socket.close([code], [reason]);
code
是特殊的 WebSocket 關閉代碼(可選)reason
是描述關閉原因的字串(可選)
然後,close
事件處理程式中的另一方會取得代碼和原因,例如
// closing party:
socket.close(1000, "Work complete");
// the other party
socket.onclose = event => {
// event.code === 1000
// event.reason === "Work complete"
// event.wasClean === true (clean close)
};
最常見的代碼值
1000
– 預設值,正常關閉(如果沒有提供code
,則使用)1006
– 無法手動設定此類代碼,表示連線已中斷(沒有關閉框架)。
還有其他代碼,例如
1001
– 一方即將離開,例如伺服器正在關閉,或瀏覽器離開頁面,1009
– 訊息過大,無法處理,1011
– 伺服器發生意外錯誤,- …等等。
完整清單可在 RFC6455, §7.4.1 中找到。
WebSocket 狀態碼有點像 HTTP 狀態碼,但不同。特別是,低於 1000
的狀態碼是保留的,如果我們嘗試設定此類狀態碼,將會發生錯誤。
// in case connection is broken
socket.onclose = event => {
// event.code === 1006
// event.reason === ""
// event.wasClean === false (no closing frame)
};
連線狀態
若要取得連線狀態,另外還有 socket.readyState
屬性,其值為
0
– “CONNECTING”:連線尚未建立,1
– “OPEN”:正在通訊,2
– “CLOSING”:連線正在關閉,3
– “CLOSED”:連線已關閉。
聊天範例
讓我們檢閱一個聊天範例,使用瀏覽器 WebSocket API 和 Node.js WebSocket 模組 https://github.com/websockets/ws。我們將主要關注於客戶端,但伺服器也很簡單。
HTML:我們需要一個 <form>
來傳送訊息,以及一個 <div>
來接收訊息
<!-- message form -->
<form name="publish">
<input type="text" name="message">
<input type="submit" value="Send">
</form>
<!-- div with messages -->
<div id="messages"></div>
從 JavaScript,我們想要三件事
- 開啟連線。
- 在表單提交時 –
socket.send(message)
傳送訊息。 - 在接收訊息時 – 將其附加到
div#messages
。
以下是程式碼
let socket = new WebSocket("wss://javascriptinfo.dev.org.tw/article/websocket/chat/ws");
// send message from the form
document.forms.publish.onsubmit = function() {
let outgoingMessage = this.message.value;
socket.send(outgoingMessage);
return false;
};
// message received - show the message in div#messages
socket.onmessage = function(event) {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
document.getElementById('messages').prepend(messageElem);
}
伺服器端程式碼超出了我們的範圍。這裡我們將使用 Node.js,但你不需要。其他平台也有自己的方式來處理 WebSocket。
伺服器端演算法將會是
- 建立
clients = new Set()
– 一組 socket。 - 對於每個已接受的 websocket,將其加入到集合
clients.add(socket)
中,並設定message
事件監聽器來取得其訊息。 - 當收到訊息時:遍歷客戶端,並將訊息傳送給所有人。
- 當連線關閉時:
clients.delete(socket)
。
const ws = new require('ws');
const wss = new ws.Server({noServer: true});
const clients = new Set();
http.createServer((req, res) => {
// here we only handle websocket connections
// in real project we'd have some other code here to handle non-websocket requests
wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);
});
function onSocketConnect(ws) {
clients.add(ws);
ws.on('message', function(message) {
message = message.slice(0, 50); // max message length will be 50
for(let client of clients) {
client.send(message);
}
});
ws.on('close', function() {
clients.delete(ws);
});
}
以下是運作範例
你也可以下載它(iframe 中的右上角按鈕)並在本地執行。在執行之前,別忘了安裝 Node.js 和 npm install ws
。
摘要
WebSocket 是一種現代的方式,可以建立持久的瀏覽器伺服器連線。
- WebSocket 沒有跨來源限制。
- 它們在瀏覽器中獲得良好的支援。
- 可以傳送/接收字串和二進位資料。
API 很簡單。
方法
socket.send(data)
,socket.close([code], [reason])
.
事件
open
,message
,error
,close
.
WebSocket 本身不包含重新連線、驗證和許多其他高階機制。因此,有客戶端/伺服器程式庫可以做到這一點,也可以手動實作這些功能。
有時,為了將 WebSocket 整合到現有的專案中,人們會在主 HTTP 伺服器平行執行 WebSocket 伺服器,並且共用一個資料庫。對 WebSocket 的請求使用 wss://ws.site.com
,一個指向 WebSocket 伺服器的子網域,而 https://site.com
則指向主 HTTP 伺服器。
當然,其他整合方式也是可行的。
留言
<code>
標籤;要插入多行程式碼,請用<pre>
標籤將它們包起來;要插入 10 行以上的程式碼,請使用沙盒 (plnkr、jsbin、codepen…)