Javascript WebSocket

2023-02-17 10:57 更新

在 RFC 6455 規(guī)范中描述的 ?WebSocket? 協(xié)議,提供了一種在瀏覽器和服務(wù)器之間建立持久連接來交換數(shù)據(jù)的方法。數(shù)據(jù)可以作為“數(shù)據(jù)包”在兩個(gè)方向上傳遞,而無需中斷連接也無需額外的 HTTP 請(qǐng)求。

對(duì)于需要連續(xù)數(shù)據(jù)交換的服務(wù),例如網(wǎng)絡(luò)游戲,實(shí)時(shí)交易系統(tǒng)等,WebSocket 尤其有用。

一個(gè)簡(jiǎn)單例子

要打開一個(gè) WebSocket 連接,我們需要在 url 中使用特殊的協(xié)議 ws 創(chuàng)建 new WebSocket

let socket = new WebSocket("ws://javascript.info");

同樣也有一個(gè)加密的 wss:// 協(xié)議。類似于 WebSocket 中的 HTTPS。

始終使用 ?wss://?

wss:// 協(xié)議不僅是被加密的,而且更可靠。

因?yàn)?nbsp;ws:// 數(shù)據(jù)不是加密的,對(duì)于任何中間人來說其數(shù)據(jù)都是可見的。并且,舊的代理服務(wù)器不了解 WebSocket,它們可能會(huì)因?yàn)榭吹健捌婀值摹?header 而中止連接。

另一方面,wss:// 是基于 TLS 的 WebSocket,類似于 HTTPS 是基于 TLS 的 HTTP),傳輸安全層在發(fā)送方對(duì)數(shù)據(jù)進(jìn)行了加密,在接收方進(jìn)行解密。因此,數(shù)據(jù)包是通過代理加密傳輸?shù)?。它們看不到傳輸?shù)睦锩娴膬?nèi)容,且會(huì)讓這些數(shù)據(jù)通過。

一旦 socket 被建立,我們就應(yīng)該監(jiān)聽 socket 上的事件。一共有 4 個(gè)事件:

  • ?open? —— 連接已建立,
  • ?message? —— 接收到數(shù)據(jù),
  • ?error? —— WebSocket 錯(cuò)誤,
  • ?close? —— 連接已關(guān)閉。

……如果我們想發(fā)送一些東西,那么可以使用 socket.send(data)

這是一個(gè)示例:

let socket = new WebSocket("wss://javascript.info/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 {
    // 例如服務(wù)器進(jìn)程被殺死或網(wǎng)絡(luò)中斷
    // 在這種情況下,event.code 通常為 1006
    alert('[close] Connection died');
  }
};

socket.onerror = function(error) {
  alert(`[error] ${error.message}`);
};

出于演示目的,在上面的示例中,運(yùn)行著一個(gè)用 Node.js 寫的小型服務(wù)器 server.js。它響應(yīng)為 “Hello from server, John”,然后等待 5 秒,關(guān)閉連接。

所以你看到的事件順序?yàn)椋?code>open → message → close

這就是 WebSocket,我們已經(jīng)可以使用 WebSocket 通信了。很簡(jiǎn)單,不是嗎?

現(xiàn)在讓我們更深入地學(xué)習(xí)它。

建立 WebSocket

當(dāng) new WebSocket(url) 被創(chuàng)建后,它將立即開始連接。

在連接期間,瀏覽器(使用 header)問服務(wù)器:“你支持 WebSocket 嗎?”如果服務(wù)器回復(fù)說“我支持”,那么通信就以 WebSocket 協(xié)議繼續(xù)進(jìn)行,該協(xié)議根本不是 HTTP。


這是由 new WebSocket("wss://javascript.info/chat") 發(fā)出的請(qǐng)求的瀏覽器 header 示例。

GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
  • ?Origin? —— 客戶端頁面的源,例如 ?https://javascript.info?。WebSocket 對(duì)象是原生支持跨源的。沒有特殊的 header 或其他限制。舊的服務(wù)器無法處理 WebSocket,因此不存在兼容性問題。但 ?Origin? header 很重要,因?yàn)樗试S服務(wù)器決定是否使用 WebSocket 與該網(wǎng)站通信。
  • ?Connection: Upgrade? —— 表示客戶端想要更改協(xié)議。
  • ?Upgrade: websocket? —— 請(qǐng)求的協(xié)議是 “websocket”。
  • ?Sec-WebSocket-Key? —— 瀏覽器隨機(jī)生成的安全密鑰。
  • ?Sec-WebSocket-Version? —— WebSocket 協(xié)議版本,當(dāng)前為 13。

無法模擬 WebSocket 握手

我們不能使用 XMLHttpRequest 或 fetch 來進(jìn)行這種 HTTP 請(qǐng)求,因?yàn)椴辉试S JavaScript 設(shè)置這些 header。

如果服務(wù)器同意切換為 WebSocket 協(xié)議,服務(wù)器應(yīng)該返回響應(yīng)碼 101:

101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=

這里 Sec-WebSocket-Accept 是 Sec-WebSocket-Key,是使用特殊的算法重新編碼的。瀏覽器使用它來確保響應(yīng)與請(qǐng)求相對(duì)應(yīng)。

然后,使用 WebSocket 協(xié)議傳輸數(shù)據(jù),我們很快就會(huì)看到它的結(jié)構(gòu)(“frames”)。它根本不是 HTTP。

擴(kuò)展和子協(xié)議

WebSocket 可能還有其他 header,Sec-WebSocket-Extensions 和 Sec-WebSocket-Protocol,它們描述了擴(kuò)展和子協(xié)議。

例如:

  • ?Sec-WebSocket-Extensions: deflate-frame? 表示瀏覽器支持?jǐn)?shù)據(jù)壓縮。擴(kuò)展與傳輸數(shù)據(jù)有關(guān),擴(kuò)展了 WebSocket 協(xié)議的功能。?Sec-WebSocket-Extensions? header 由瀏覽器自動(dòng)發(fā)送,其中包含其支持的所有擴(kuò)展的列表。
  • ?Sec-WebSocket-Protocol: soap, wamp? 表示我們不僅要傳輸任何數(shù)據(jù),還要傳輸 SOAP 或 WAMP(“The WebSocket Application Messaging Protocol”)協(xié)議中的數(shù)據(jù)。WebSocket 子協(xié)議已經(jīng)在  IANA catalogue 中注冊(cè)。因此,此 header 描述了我們將要使用的數(shù)據(jù)格式。
  • 這個(gè)可選的 header 是使用 new WebSocket 的第二個(gè)參數(shù)設(shè)置的。它是子協(xié)議數(shù)組,例如,如果我們想使用 SOAP 或 WAMP:

    let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]);
    

服務(wù)器應(yīng)該使用同意使用的協(xié)議和擴(kuò)展的列表進(jìn)行響應(yīng)。

例如,這個(gè)請(qǐng)求:

GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp

響應(yīng):

101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap

在這里服務(wù)器響應(yīng) —— 它支持?jǐn)U展 “deflate-frame”,并且僅支持所請(qǐng)求的子協(xié)議中的 SOAP。

數(shù)據(jù)傳輸

WebSocket 通信由 “frames”(即數(shù)據(jù)片段)組成,可以從任何一方發(fā)送,并且有以下幾種類型:

  • “text frames” —— 包含各方發(fā)送給彼此的文本數(shù)據(jù)。
  • “binary data frames” —— 包含各方發(fā)送給彼此的二進(jìn)制數(shù)據(jù)。
  • “ping/pong frames” 被用于檢查從服務(wù)器發(fā)送的連接,瀏覽器會(huì)自動(dòng)響應(yīng)它們。
  • 還有 “connection close frame” 以及其他服務(wù) frames。

在瀏覽器里,我們僅直接使用文本或二進(jìn)制 frames。

WebSocket .send() 方法可以發(fā)送文本或二進(jìn)制數(shù)據(jù)。

socket.send(body) 調(diào)用允許 body 是字符串或二進(jìn)制格式,包括 BlobArrayBuffer 等。不需要額外的設(shè)置:直接發(fā)送它們就可以了。

當(dāng)我們收到數(shù)據(jù)時(shí),文本總是以字符串形式呈現(xiàn)。而對(duì)于二進(jìn)制數(shù)據(jù),我們可以在 Blob 和 ArrayBuffer 格式之間進(jìn)行選擇。

它是由 socket.binaryType 屬性設(shè)置的,默認(rèn)為 "blob",因此二進(jìn)制數(shù)據(jù)通常以 Blob 對(duì)象呈現(xiàn)。

Blob 是高級(jí)的二進(jìn)制對(duì)象,它直接與 <a>,<img> 及其他標(biāo)簽集成在一起,因此,默認(rèn)以 Blob 格式是一個(gè)明智的選擇。但是對(duì)于二進(jìn)制處理,要訪問單個(gè)數(shù)據(jù)字節(jié),我們可以將其改為 "arraybuffer"

socket.binaryType = "arraybuffer";
socket.onmessage = (event) => {
  // event.data 可以是文本(如果是文本),也可以是 arraybuffer(如果是二進(jìn)制數(shù)據(jù))
};

限速

想象一下:我們的應(yīng)用程序正在生成大量要發(fā)送的數(shù)據(jù)。但是用戶的網(wǎng)速卻很慢,可能是在鄉(xiāng)下的移動(dòng)設(shè)備上。

我們可以反復(fù)地調(diào)用 socket.send(data)。但是數(shù)據(jù)將會(huì)緩沖(儲(chǔ)存)在內(nèi)存中,并且只能在網(wǎng)速允許的情況下盡快將數(shù)據(jù)發(fā)送出去。

socket.bufferedAmount 屬性儲(chǔ)存了目前已緩沖的字節(jié)數(shù),等待通過網(wǎng)絡(luò)發(fā)送。

我們可以檢查它以查看 socket 是否真的可用于傳輸。

// 每 100ms 檢查一次 socket
// 僅當(dāng)所有現(xiàn)有的數(shù)據(jù)都已被發(fā)送出去時(shí),再發(fā)送更多數(shù)據(jù)
setInterval(() => {
  if (socket.bufferedAmount == 0) {
    socket.send(moreData());
  }
}, 100);

連接關(guān)閉

通常,當(dāng)一方想要關(guān)閉連接時(shí)(瀏覽器和服務(wù)器都具有相同的權(quán)限),它們會(huì)發(fā)送一個(gè)帶有數(shù)字碼(numeric code)和文本形式的原因的 “connection close frame”。

它的方法是:

socket.close([code], [reason]);
  • ?code? 是一個(gè)特殊的 WebSocket 關(guān)閉碼(可選)
  • ?reason? 是一個(gè)描述關(guān)閉原因的字符串(可選)

然后,另外一方通過 close 事件處理器獲取了關(guān)閉碼和關(guān)閉原因,例如:

// 關(guān)閉方:
socket.close(1000, "Work complete");

// 另一方
socket.onclose = event => {
  // event.code === 1000
  // event.reason === "Work complete"
  // event.wasClean === true (clean close)
};

最常見的數(shù)字碼:

  • ?1000? —— 默認(rèn),正常關(guān)閉(如果沒有指明 ?code? 時(shí)使用它),
  • ?1006? —— 沒有辦法手動(dòng)設(shè)定這個(gè)數(shù)字碼,表示連接丟失(沒有 close frame)。

還有其他數(shù)字碼,例如:

  • ?1001? —— 一方正在離開,例如服務(wù)器正在關(guān)閉,或者瀏覽器離開了該頁面,
  • ?1009? —— 消息太大,無法處理,
  • ?1011? —— 服務(wù)器上發(fā)生意外錯(cuò)誤,
  • ……等。

完整列表請(qǐng)見 RFC6455, §7.4.1。

WebSocket 碼有點(diǎn)像 HTTP 碼,但它們是不同的。特別是,小于 1000 的碼都是被保留的,如果我們嘗試設(shè)置這樣的碼,將會(huì)出現(xiàn)錯(cuò)誤。

// 在連接斷開的情況下
socket.onclose = event => {
  // event.code === 1006
  // event.reason === ""
  // event.wasClean === false(未關(guān)閉 frame)
};

連接狀態(tài)

要獲取連接狀態(tài),可以通過帶有值的 ?socket.readyState? 屬性:

  • ?0? —— “CONNECTING”:連接還未建立,
  • ?1? —— “OPEN”:通信中,
  • ?2? —— “CLOSING”:連接關(guān)閉中,
  • ?3? —— “CLOSED”:連接已關(guān)閉。

聊天示例

讓我們來看一個(gè)使用瀏覽器 WebSocket API 和 Node.js 的 WebSocket 模塊 https://github.com/websockets/ws 的聊天示例。我們將主要精力放在客戶端上,但是服務(wù)端也很簡(jiǎn)單。

HTML:我們需要一個(gè) <form> 來發(fā)送消息,并且需要一個(gè) <div> 來接收消息:

<!-- 消息表單 -->
<form name="publish">
  <input type="text" name="message">
  <input type="submit" value="Send">
</form>

<!-- 帶有消息的 div -->
<div id="messages"></div>

在 JavaScript 中,我們想要做三件事:

  1. 打開連接。
  2. 在表單提交中 —— ?socket.send(message)? 用于消息。
  3. 對(duì)于傳入的消息 —— 將其附加(append)到 ?div#messages? 上。

代碼如下

let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");

// 從表單發(fā)送消息
document.forms.publish.onsubmit = function() {
  let outgoingMessage = this.message.value;

  socket.send(outgoingMessage);
  return false;
};

// 收到消息 —— 在 div#messages 中顯示消息
socket.onmessage = function(event) {
  let message = event.data;

  let messageElem = document.createElement('div');
  messageElem.textContent = message;
  document.getElementById('messages').prepend(messageElem);
}

服務(wù)端代碼有點(diǎn)超出我們的范圍。在這里,我們將使用 Node.js,但你不必這樣做。其他平臺(tái)也有使用 WebSocket 的方法。

服務(wù)器端的算法為:

  1. 創(chuàng)建 ?clients = new Set()? —— 一系列 socket。
  2. 對(duì)于每個(gè)被接受的 WebSocket,將其添加到 ?clients.add(socket)?,并為其設(shè)置 ?message? 事件偵聽器以獲取其消息。
  3. 當(dāng)接收到消息:遍歷客戶端,并將消息發(fā)送給所有人。
  4. 當(dāng)連接被關(guān)閉:?clients.delete(socket)?。
const ws = new require('ws');
const wss = new ws.Server({noServer: true});

const clients = new Set();

http.createServer((req, res) => {
  // 在這里,我們僅處理 WebSocket 連接
  // 在實(shí)際項(xiàng)目中,我們?cè)谶@里還會(huì)有其他代碼,來處理非 WebSocket 請(qǐng)求
  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); // message 的最大長(zhǎng)度為 50

    for(let client of clients) {
      client.send(message);
    }
  });

  ws.on('close', function() {
    clients.delete(ws);
  });
}

你也可以下載它然后在本地運(yùn)行。運(yùn)行之前請(qǐng)記得安裝 Node.js 和 npm install ws。

下載鏈接

總結(jié)

WebSocket 是一種在瀏覽器和服務(wù)器之間建立持久連接的現(xiàn)代方式。

  • WebSocket 沒有跨源限制。
  • 瀏覽器對(duì) WebSocket 支持很好。
  • 可以發(fā)送/接收字符串和二進(jìn)制數(shù)據(jù)。

WebSocket 的 API 很簡(jiǎn)單。

WebSocket 方法:

  • ?socket.send(data)?
  • ?socket.close([code], [reason])?

WebSocket 事件:

  • ?open?
  • ?message?
  • ?error?
  • ?close?

WebSocket 自身并不包含重新連接(reconnection),身份驗(yàn)證(authentication)和很多其他高級(jí)機(jī)制。因此,有針對(duì)于此的客戶端/服務(wù)端的庫(kù),并且也可以手動(dòng)實(shí)現(xiàn)這些功能。

有時(shí)為了將 WebSocket 集成到現(xiàn)有項(xiàng)目中,人們將主 HTTP 服務(wù)器與 WebSocket 服務(wù)器并行運(yùn)行,并且它們之間共享同一個(gè)數(shù)據(jù)庫(kù)。對(duì)于 WebSocket 請(qǐng)求使用一個(gè)通向 WebSocket 服務(wù)器的子域 wss://ws.site.com,而 https://site.com 則通向主 HTTP 服務(wù)器。

當(dāng)然,其他集成方式也是可行的。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)