W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
在 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è) 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í)它。
當(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。
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。
WebSocket 通信由 “frames”(即數(shù)據(jù)片段)組成,可以從任何一方發(fā)送,并且有以下幾種類型:
在瀏覽器里,我們僅直接使用文本或二進(jìn)制 frames。
WebSocket .send()
方法可以發(fā)送文本或二進(jìn)制數(shù)據(jù)。
socket.send(body)
調(diào)用允許 body
是字符串或二進(jìn)制格式,包括 Blob
,ArrayBuffer
等。不需要額外的設(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);
通常,當(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),可以通過帶有值的 ?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 中,我們想要做三件事:
socket.send(message)
? 用于消息。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ù)器端的算法為:
clients = new Set()
? —— 一系列 socket。clients.add(socket)
?,并為其設(shè)置 ?message
? 事件偵聽器以獲取其消息。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
。
WebSocket 是一種在瀏覽器和服務(wù)器之間建立持久連接的現(xiàn)代方式。
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)然,其他集成方式也是可行的。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: