最近在工作中遇到了需要伺服器推送訊息的場景,這裡總結一下收集整理WebSocket相關資料的收穫。
1. 概述
1.1 伺服器推送
WebSocket作為一種通訊協議,屬於伺服器推送技術的一種,IE10+支援。
伺服器推送技術不止一種,有短輪詢、長輪詢、WebSocket、Server-sent Events(SSE)等,他們各有優缺點:
# | 短輪詢 | 長輪詢 | Websocket | sse |
---|---|---|---|---|
通訊方式 | http | http | 基於TCP長連線通訊 | http |
觸發方式 | 輪詢 | 輪詢 | 事件 | 事件 |
優點 | 相容性好容錯性強,實現簡單 | 比短輪詢節約資源 | 全雙工通訊協議,效能開銷小、安全性高,有一定可擴充套件性 | 實現簡便,開發成本低 |
缺點 | 安全性差,佔較多的記憶體資源與請求數 | 安全性差,佔較多的記憶體資源與請求數 | 傳輸資料需要進行二次解析,增加開發成本及難度 | 只適用高階瀏覽器 |
適用範圍 | b/s服務 | b/s服務 | 網路遊戲、銀行互動和支付 | 服務端到客戶端單向推送 |
短輪詢最簡單,在一些簡單的場景也會經常使用,就是隔一段時間就發起一個ajax請求。那麼長輪詢是什麼呢?
長輪詢(Long Polling)是在Ajax輪詢基礎上做的一些改進,在沒有更新的時候不再返回空響應,而且把連線保持到有更新的時候,客戶端向伺服器傳送Ajax請求,伺服器接到請求後hold住連線,直到有新訊息才返回響應資訊並關閉連線,客戶端處理完響應資訊後再向伺服器傳送新的請求。它是一個解決方案,但不是最佳的技術方案。
如果說短輪詢是客戶端不斷打電話問服務端有沒有訊息,服務端回覆後立刻結束通話,等待下次再打;長輪詢是客戶端一直打電話,服務端接到電話不結束通話,有訊息的時候再回復客戶端並結束通話。
SSE(Server-Sent Events)與長輪詢機制類似,區別是每個連線不只傳送一個訊息。客戶端傳送一個請求,服務端保持這個連線直到有新訊息傳送回客戶端,仍然保持著連線,這樣連線就可以支援訊息的再次傳送,由伺服器單向傳送給客戶端。然而IE直到11都不支援,不多說了....
1.2 WebSocket的特點
為什麼已經有了輪詢還要WebSocket呢,是因為短輪詢和長輪詢有個缺陷:通訊只能由客戶端發起。
那麼如果後端想往前端推送訊息需要前端去輪詢,不斷查詢後端是否有新訊息,而輪詢的效率低且浪費資源(必須不停 setInterval 或 setTimeout 去連線,或者 HTTP 連線始終開啟),WebSocket提供了一個文明優雅的全雙工通訊方案。一般適合於對資料的實時性要求比較強的場景,如通訊、股票、直播、共享桌面,特別適合於客戶端與服務頻繁互動的情況下,如聊天室、實時共享、多人協作等平臺。
特點
- 建立在 TCP 協議之上,伺服器端的實現比較容易。
- 與 HTTP 協議有著良好的相容性。預設埠也是80和443,並且握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器。
- 資料格式比較輕量,效能開銷小,通訊高效。伺服器與客戶端之間交換的標頭資訊大概只有2位元組;
- 可以傳送文字,也可以傳送二進位制資料。
- 沒有同源限制,客戶端可以與任意伺服器通訊。
- 協議識別符號是
ws
(如果加密,則為wss),伺服器網址就是 URL。ex:ws://example.com:80/some/path
- 不用頻繁建立及銷燬TCP請求,減少網路頻寬資源的佔用,同時也節省伺服器資源;
- WebSocket是純事件驅動的,一旦連線建立,通過監聽事件可以處理到來的資料和改變的連線狀態,資料都以幀序列的形式傳輸。服務端傳送資料後,訊息和事件會非同步到達。
- 無超時處理。
HTTP與WS協議結構
WebSocket協議識別符號用ws
表示。`wss協議表示加密的WebSocket協議,對應HTTPs協議。結構如下:
- HTTP: TCP > HTTP
- HTTPS: TCP > TLS > HTTP
- WS: TCP > WS
- WSS: TCP > TLS > WS
2 WebSocket的通訊過程
首先,Websocket是一個持久化的協議,相對於HTTP這種非持久的協議來說。
一個HTTP的通訊生命週期通過 Request 來界定,也就是一個 Request 一個 Response ,那麼在 HTTP1.0 中,這次HTTP請求就結束了。
在HTTP1.1中進行了改進,有了一個keep-alive
,在一個HTTP連線中,可以傳送多個Request,接收多個Response,也就是合併多個請求。但是一個Request只能對應一個Response,而且這個Response是被動的,不能主動發起。
Websocket 其實是一個新協議,但是為了相容現有瀏覽器的握手規範而借用了HTTP的協議來完成一部分握手。
WebSocket是純事件驅動的,一旦連線建立,通過監聽事件可以處理到來的資料和改變的連線狀態,資料都以幀序列的形式傳輸。服務端傳送資料後,訊息和事件會非同步到達。WebSocket程式設計遵循一個非同步程式設計模型,只需要對WebSocket物件增加回撥函式就可以監聽事件。
2.1 WebSocket通訊流程圖
這裡可以看出傳統HTTP通訊與WebSocket通訊的通訊流程上的區別,下圖顯示WebSocket主要的三步中瀏覽器和伺服器端分別做了哪些事情。
2.2 建立連線的握手
當Web應用程式呼叫new WebSocket(url)
介面時,客戶端就開始了與地址為url的WebServer建立握手連線的過程。
- 客戶端與服務端通過TCP三次握手建立連線,如果這個建立連線失敗,那麼後面的過程就不會執行,Web應用程式將收到錯誤訊息通知。
- 在TCP建立連線成功後,客戶端通過HTTP協議傳送WebSocket支援的版本號、協議的字版本號、原始地址、主機地址等等一些列欄位給伺服器端。
- 服務端收到客戶端傳送來的握手請求後,如果資料包資料和格式正確、客戶端和服務端的協議版本號匹配等等,就接受本次握手連線,並給出相應的資料回覆,同樣回覆的資料包也是採用HTTP協議傳輸。
- 客戶端收到服務端回覆的資料包後,如果資料包內容、格式都沒有問題的話,就表示本次連線成功,觸發
onopen
,此時Web開發者就可以在此時通過send()
向伺服器傳送資料。否則握手連線失敗,Web應用程式觸發onerror
,並且能知道連線失敗的原因。
這個握手很像HTTP,但是實際上卻不是,它允許伺服器以HTTP的方式解釋一部分handshake的請求,然後切換為websocket。
2.3 WebSocket握手報文
一個瀏覽器發出的WebSocket請求報文類似於:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
複製程式碼
HTTP1.1協議規定,Upgrade頭資訊表示將通訊協議從HTTP/1.1轉向該項所指定的協議。
Connection: Upgrade
表示瀏覽器通知伺服器,如果可以,就升級到webSocket協議。Origin
用於驗證瀏覽器域名是否在伺服器許可的範圍內。Sec-WebSocket-Key
則是用於握手協議的金鑰,是瀏覽器生成的Base64編碼的16位元組隨機字串。Sec-WebSocket-Protocol
是一個使用者定義的字串,用來區分同URL下,不同的服務所需要的協議。Sec-WebSocket-Version
是告訴伺服器所使用的協議版本。
服務端WebSocket回覆報文:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/
複製程式碼
- 伺服器端同樣用
Connection: Upgrade
通知瀏覽器,服務端已經成功切換協議。 Sec-WebSocket-Accept
是經過伺服器確認並且加密過後的Sec-WebSocket-Key
。Sec-WebSocket-Location
表示進行通訊的WebSocket網址。Sec-WebSocket-Protocol
表示最終使用的協議。
在這樣一個類似於HTTP通訊的握手結束之後,下面就按照WebSocket協議進行通訊了。客戶端與伺服器之間不會再發生HTTP通訊,一切由WebSocket 協議接管。
3. WebSocket API
瀏覽器提供了一個WebSocket物件的實現,可以用這個物件來建立和管理WebSocket連線,並且可以通過該連線傳送和接受資料。WebSocket是事件驅動的,因此只需要對WebSocket物件增加回撥函式就可以監聽事件的發生。
跟XMLHttpRequest一樣,通過該建構函式先new出來物件例項const ws = new WebSocket('ws://localhost:8080')
,再使用物件下掛載的屬性與方法來操作。後文都用ws來指代WebSocket的例項。
3.1 ws上常用屬性
ws.readyState
WebSocket例項物件類似於XHR有個的只讀屬性readyState
來指示連線的當前狀態:
狀態 | 值 | 描述 |
---|---|---|
CONNECTING | 0 | 連線還沒開啟。 |
OPEN | 1 | 連線已開啟並準備好進行通訊。 |
CLOSING | 2 | 連線正在關閉的過程中。 |
CLOSED | 3 | 連線已經關閉,或者連線無法建立。 |
一個示例:
switch (ws.readyState) {
case WebSocket.CONNECTING:
// ...
break;
case WebSocket.OPEN:
// ...
break;
case WebSocket.CLOSING:
// ...
break;
case WebSocket.CLOSED:
// ...
break;
default:
// this never happens
break;
}
複製程式碼
ws.onopen / ws.onclose
例項物件的onopen
屬性,用於指定連線成功後的回撥函式。
ws.onopen = function () {
ws.send('Hello Server!');
}
複製程式碼
如果要指定多個回撥函式,可以addEventListener
。
ws.addEventListener('open', function (event) {
ws.send('Hello Server!');
});
複製程式碼
例項物件的onclose
屬性,用於指定連線關閉後的回撥函式。
ws.onclose = function(event) {
const { code, reason, wasClean} = event
// ...
};
ws.addEventListener('close', function(event) {
const { code, reason, wasClean} = event
// ...
})
複製程式碼
ws.onmessage
例項物件的onmessage
屬性,用於指定收到伺服器資料後的回撥函式。
ws.onmessage = function(event) {
const { data } = event;
// ...
};
ws.addEventListener('message', function(event) {
const { data } = event;
// ...
});
複製程式碼
注意,伺服器資料可能是文字,也可能是二進位制資料(blob物件或Arraybuffer物件)。
ws.onmessage = function(event){
if(typeof event.data === String) {
// string
}
if(event.data instanceof ArrayBuffer){
const { data: buffer } = event;
// array buffer
}
}
複製程式碼
除了動態判斷收到的資料型別,也可以使用binaryType
屬性,顯式指定收到的二進位制資料型別。binaryType
取值應當是'blob'或者'arraybuffer','blob'表示使用 Blob 物件,而'arraybuffer'表示使用 ArrayBuffer 物件。
ws.binaryType = 'blob'; // 收到的是 Blob 資料
ws.onmessage = function(e) {
console.log(e.data.size);
};
ws.binaryType = 'arraybuffer'; // 收到的是 ArrayBuffer 資料
ws.onmessage = function(e) {
console.log(e.data.byteLength);
};
複製程式碼
ws.bufferedAmount
例項物件的bufferedAmount
只讀屬性,表示還有多少位元組的二進位制資料沒有傳送出去。它可以用來判斷髮送是否結束。 該值會在所有佇列資料被髮送後重置為 0,而當連線關閉時不會設為0。如果持續呼叫send(),這個值會持續增長。
var data = new ArrayBuffer(10000000);
ws.send(data);
if (ws.bufferedAmount === 0) {
// 傳送完畢
} else {
// 傳送還沒結束
}
複製程式碼
ws.onerror
例項物件的onerror
屬性,用於指定報錯時的回撥函式。
ws.onerror = function(event) {
// handle error event
};
ws.addEventListener("error", function(event) {
// handle error event
});
複製程式碼
3.2 ws上常用方法
ws.close()
關閉WebSocket連線或停止正在進行的連線請求。如果連線的狀態已經是closed,這個方法不會有任何效果。
ws.send()
例項物件的send()
方法用於向伺服器傳送資料。
ws.send('your message'); // 傳送文字的例子
var file = document
.querySelector('input[type="file"]')
.files[0];
ws.send(file); // 傳送 Blob 物件的例子
// 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];
}
ws.send(binary.buffer); // 傳送 ArrayBuffer 物件的例子
複製程式碼
最後一個ArrayBuffer物件栗子中的canvas_context例項是CanvasRenderingContext2D型別的物件,其上的.getImageData()
方法返回一個ImageData物件。
網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~
參考:
官方文件:
工具:
PS:歡迎大家關注我的公眾號【前端下午茶】,一起加油吧~