數據實時:即數據庫中的數據得到更新,頁面立刻就想得到更新并展示最新的數據狀態。通常使用在大數據可視化分析,運營數據監控等場景。
# 數據實時方案
Web想要更新頁面,通常都是客戶端發起Http異步請求,主動向服務端索取數據
,方案有:
(1)Ajax輪詢,又稱 Ajax短連接:即啟動一個定時器隔一定時間(如1s
)發送一個請求,服務端收到請求無論如何都直接返回當前數據庫狀態數據。缺點是實時性不夠,產生很多不必要的請求。可用于刷新頻率不是很高的場景。
(2)Ajax長連接:客戶端發起Http請求,并設置一個長超時時間,服務端收到請求后,檢查數據庫如果沒有更新則阻塞請求,直到有更新或超時為止。客戶端每次收到響應后,立即再發一個請求,Comet就是這種方式。缺點是服務器的處理線程長時間掛起,極大浪費資源,且網絡鏈路可能被網關關閉,需要如ping
數據來維持鏈接。
? 以上兩種機制都治標不治本,是否能有一種機制,由服務端自己檢測數據狀態,有更新主動告知客戶端
。好在,HTML5推出了 WebSocket
協議,解決了這個問題
?
# WebSocket是什么
? WebSocket(以下簡稱 ws
)是HTML5提供的一種在單個 TCP 連接上進行全雙工通訊的網絡技術,目的是在瀏覽器和服務器之間建立一個不受限的雙向通信的通道,讓雙方都可以主動給對方發消息。
? 雖說ws是H5下新的協議,但其實也不是全新的。它屬于應用層協議,復用了HTTP的握手通道。ws協議與HTTP協議都是基于TCP的,因此都是可靠的協議。ws客戶端和服務器只需要做一個握手的動作,兩者之間就形成了一條快速通道。在建立握手連接時,數據是通過http進行傳輸的,但建立之后,真正的數據傳輸階段就不需要http參與了
?
# WebSocket的優點
? ws協議相比于HTTP協議,它具有以下優勢:
- 全雙工通信能力:支持客戶端和服務端主動給對方發送消息
- 高實時性:Ajax輪詢只是不斷的請求,而服務端檢測到更新主動推送才是真正意義上的實時。
- 高效節能:HTTP協議請求一般都會有較長的頭部,而需要實時更新的數據可能就一點點,這就造成了帶寬很多不必要的消耗。而ws協議控制數據包的頭部比較小,一般只有十個字節左右。
- 支持擴展: ws協議定義了擴展,用戶可以擴展協議,或實現自定義子協議。
-
沒有跨域限制:不是xhr請求,沒有同源策略的限制
?
# WebSocket的第一次握手
? 雖說ws支持雙向通訊能力,但請求必須是由客戶發起。由于發起時是一個http握手,因此格式如下
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw== // 客戶端隨機串
Sec-WebSocket-Version: 13
值得注意的是:
(1)其只能發GET請求,且不再是 http://...
而是換成了 ws://...
開頭的地址
(2)請求頭Upgrade: websocket
和Connection: Upgrade
表示該連接將要被升級為WebSocket連接;
(3)Sec-WebSocket-Key
標識連接的Key
串(下方有更多解釋)
(4)Sec-WebSocket-Version
指定了WebSocket的協議版本。
如果服務器識別key
正確,會接收這個請求,就會響應如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU= // 服務端隨機串
服務端Accept
串是根據客戶端隨機串計算出來的,計算規則為:(1)與固定串拼接,(2)執行sha1算法,(3)轉為base64字符串。這對Key/Accept
需ws客戶端和服務端提前約定,目的是為了避免非法ws請求等一些常見的意外情況。并不能確保數據安全性,畢竟算法公開且簡單。公式如下:
toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
響應碼101
表示將切換協議,更改后的協議就是Upgrade: websocket
指定的WebSocket協議。當連接建立成功后,雙方就可以自由通訊消息了。消息一般分兩種:(1)文本,(2)二進制數據。開發中會使用JSON文本數據比較直觀。
?
# ws為什么能實現全雙工通訊
? 前文多次遇到 全雙工通信
字眼,意思就是客戶端和服務端能隨時給對方發送消息。好像理解了但又朦朦朧朧。這里解釋一下:
- 單工: 數據傳輸只支持在一個方向上的傳輸,同時只能有一方發送或接收消息。
- 半雙工:數據允許在兩個方向上傳輸,但任一時刻,只允許有一方在傳輸,是一種切換方向的單工通信
- 全雙工:任何時刻都允許兩個方向進行數據傳輸,不受對方限制。
? HTTP 和WebSocket 都是基于TCP傳輸協議的,其實TCP本身是支持全雙工通訊
的,而HTTP協議的請求,因為其應答機制限制了全雙工通信。當第一次握手完成后,協議由HTTP切換成了WebSocket,ws連接建立,其實只是簡單規定了下:后續通訊不再使用http協議,雙發可以互相發送數據了。
?
# 安全的WebSocket通訊
? 與 HTTPS 類似,安全的ws連接使用的是wss://...
開頭的請求,它首先會通過https創建安全的連接,升級協議后,底層通信依然走的 SSL/TLS 協議
?
# 連接保持 - 心跳
? WebSocket為了保持客戶端與服務端的實時雙向通訊,需保持TCP通道鏈接沒有斷開。然而長時間沒有數據往來的連接,會浪費一些連接資源,網絡鏈路同樣可能被網關關閉,畢竟網關不是我們能控制的。因此鏈路鏈接就需要提示說明還在使用周期內,這個提示就是心跳來實現的。
- 發送方 --> 接收方: ping
- 接收方 --> 發送方: pong
舉例,ws服務端向客戶端發送ping
,代碼如下
ws.ping('', false, true)
?
$ WebSocket API
? 理解了WebSocket的概念及相應的特征后,來看看怎么上手編寫
# 創建WebSocket實例
? ws提供了WebSocket(url[, protocals])
構造函數來返回實例化ws對象。參數一表示要連接的URL,參數二表示可接受的子協議。
let socket= new WebSocket('http://localhost:8080')
? 執行以上代碼,瀏覽器就開始嘗試創建連接,與 xhr 的readystatechange
類似的是,ws連接也有一個表示當前狀態的屬性readyState
,
# 連接狀態-readyState 只讀
? 用于返回當前WebSocket連接的狀態,其值即含義如下
值 | 狀態含義 |
---|---|
0 | WebSocket.CONNECTING |
1 | WebSocket.OPEN |
2 | WebSocket.CLOSING |
3 | WebSocket.CLOSED |
一個ws連接各個狀態的執行時刻如下
let socket = new WebSocket('http://localhost:8080')
// 正在創建連接
console.log('[readyState]:', socket.readyState) // 0
// 連接建立成功后觸發onopen回調
socket.onopen = function() {
console.log('connected,[readyState]:', socket.readyState) // 1
// 發送消息
socket.send('from client: Hello')
}
// 從服務端收到信息觸發onmessage回調
socket.onmessage = function() {
console.log('received,[readyState]:', socket.readyState) // 1
// 發送消息
socket.send('from client: Hello')
}
// 連接失敗觸發onerror回調
socket.onerror = function() {
console.log('connect error, [readyState]:', socket.readyState) // 3
}
// 調用關閉連接,狀態立刻變成2(正在關閉)。關閉成功觸發onclose變成3
socket.close()
// 連接關閉觸發onclose回調,有回調參數
socket.onclose = function(event) {
const { code, reason, wasClean } = event
console.log('connect closed, [readyState]:', socket.readyState) // 3
console.log(code, reason, wasClean) // wasClean表示連接是否已經關閉。boolean
}
? 當readyState
的值從 0 變成 1 后,客戶端和服務端就可以通訊了。
?
# 方法
- 發送數據 send()
? 發送數據一定是伴隨在連接已經打開的情況下
socket.addEventListener('open', function(event) {
sokcet.send('hello server')
})
- 關閉連接 close()
? 關閉當前連接。可以傳 0/1/2
個參數。code
解釋關閉原因的狀態碼。reason
解釋關閉原因的描述(限制123個字節)。
sokcet.close([code[, reason]])
如果未傳參數,會默認code
為1005
,意為:無參數,未提供關閉原因狀態碼。查看 狀態碼詳情。如果提供一個無效的狀態碼,會拋出異常INVALID_ACCESS_ERR
。
?
# 事件
- 連接已建立 onopen
socket.addEventListener('open', function(event) {
// TODO: send message
});
- 接收服務端消息回調 onmessage
? 當服務器向客戶端發來消息時,WebSocket對象會觸發message
事件。這個message
事件與其他傳遞消息的協議類似,也是把返回的數據保存在event.data
屬性中
socket.addEventListener('message', function(event) {
var data = event.data;
// TODO:
});
- 關閉連接的回調 onclose
socket.addEventListener('close', function(event) {
const { code, reason, wasClean } = event
// TODO:
});
- 連接失敗的回調 onerror
socket.addEventListener('error', function(event) {
console.error("WebSocket error observed:", event)
});
?
# 屬性
- 當前剩余未發送數據 bufferedAmount 只讀
? 用于返回已經被send()
方法放入隊列但還沒有被發送到網絡中的數據的字節數,只有發送完成它才會被重置為0。如果發送過程中連接被關閉不會重置,不斷的調用send()
該值會不斷增長。
if (ws.bufferedAmount === 0){
console.log("發送已完成");
} else {
console.log("還有", ws.bufferedAmount, "數據沒有發送");
}
- 連接二進制類型 binaryType 只讀
? 返回websocket連接所傳輸二進制數據的類型
const binaryType = socket.binaryType
- 已選擇的擴展值 extensions 只讀
? 返回服務器已選擇的擴展值
const extensions = socket.extensions
- 子協議 protocol 只讀
? 返回服務器端選中的子協議的名字;也就是在實例化WebSocket
對象時,在參數protocols
中指定的字符串
const protocol = socket.protocol
- 子協議 url 只讀
? 返回值為當構造函數創建WebSocket
實例對象時URL的絕對路徑。
const url = socket.url
?
$ 一個服務端實例
這里提供一個簡單的例子,引入了ws
庫實現。也可以使用socket.io
var app = require('express')();
var server = require('http').Server(app);
var WebSocket = require('ws');
var wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
console.log('server: receive connection.');
ws.on('message', function incoming(message) {
console.log('server: received: %s', message);
});
ws.send('world');
});
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
app.listen(3000);
?