H5數(shù)據(jù)實時方案「WebSocket」

數(shù)據(jù)實時:即數(shù)據(jù)庫中的數(shù)據(jù)得到更新,頁面立刻就想得到更新并展示最新的數(shù)據(jù)狀態(tài)。通常使用在大數(shù)據(jù)可視化分析,運(yùn)營數(shù)據(jù)監(jiān)控等場景。

# 數(shù)據(jù)實時方案

Web想要更新頁面,通常都是客戶端發(fā)起Http異步請求,主動向服務(wù)端索取數(shù)據(jù),方案有:
(1)Ajax輪詢,又稱 Ajax短連接:即啟動一個定時器隔一定時間(如1s)發(fā)送一個請求,服務(wù)端收到請求無論如何都直接返回當(dāng)前數(shù)據(jù)庫狀態(tài)數(shù)據(jù)。缺點是實時性不夠,產(chǎn)生很多不必要的請求??捎糜谒⑿骂l率不是很高的場景。
(2)Ajax長連接:客戶端發(fā)起Http請求,并設(shè)置一個長超時時間,服務(wù)端收到請求后,檢查數(shù)據(jù)庫如果沒有更新則阻塞請求,直到有更新或超時為止??蛻舳嗣看问盏巾憫?yīng)后,立即再發(fā)一個請求,Comet就是這種方式。缺點是服務(wù)器的處理線程長時間掛起,極大浪費資源,且網(wǎng)絡(luò)鏈路可能被網(wǎng)關(guān)關(guān)閉,需要如ping數(shù)據(jù)來維持鏈接。

? 以上兩種機(jī)制都治標(biāo)不治本,是否能有一種機(jī)制,由服務(wù)端自己檢測數(shù)據(jù)狀態(tài),有更新主動告知客戶端。好在,HTML5推出了 WebSocket 協(xié)議,解決了這個問題
?

# WebSocket是什么

? WebSocket(以下簡稱 ws)是HTML5提供的一種在單個 TCP 連接上進(jìn)行全雙工通訊的網(wǎng)絡(luò)技術(shù),目的是在瀏覽器和服務(wù)器之間建立一個不受限的雙向通信的通道,讓雙方都可以主動給對方發(fā)消息。
? 雖說ws是H5下新的協(xié)議,但其實也不是全新的。它屬于應(yīng)用層協(xié)議,復(fù)用了HTTP的握手通道。ws協(xié)議與HTTP協(xié)議都是基于TCP的,因此都是可靠的協(xié)議。ws客戶端和服務(wù)器只需要做一個握手的動作,兩者之間就形成了一條快速通道。在建立握手連接時,數(shù)據(jù)是通過http進(jìn)行傳輸?shù)?,但建立之后,真正的?shù)據(jù)傳輸階段就不需要http參與了

圖片來自菜鳥教程

?

# WebSocket的優(yōu)點

? ws協(xié)議相比于HTTP協(xié)議,它具有以下優(yōu)勢:

  • 全雙工通信能力:支持客戶端和服務(wù)端主動給對方發(fā)送消息
  • 高實時性:Ajax輪詢只是不斷的請求,而服務(wù)端檢測到更新主動推送才是真正意義上的實時。
  • 高效節(jié)能:HTTP協(xié)議請求一般都會有較長的頭部,而需要實時更新的數(shù)據(jù)可能就一點點,這就造成了帶寬很多不必要的消耗。而ws協(xié)議控制數(shù)據(jù)包的頭部比較小,一般只有十個字節(jié)左右。
  • 支持?jǐn)U展: ws協(xié)議定義了擴(kuò)展,用戶可以擴(kuò)展協(xié)議,或?qū)崿F(xiàn)自定義子協(xié)議。
  • 沒有跨域限制:不是xhr請求,沒有同源策略的限制
    ?

# WebSocket的第一次握手

? 雖說ws支持雙向通訊能力,但請求必須是由客戶發(fā)起。由于發(fā)起時是一個http握手,因此格式如下

GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw== // 客戶端隨機(jī)串
Sec-WebSocket-Version: 13

值得注意的是:
(1)其只能發(fā)GET請求,且不再是 http://... 而是換成了 ws://... 開頭的地址
(2)請求頭Upgrade: websocketConnection: Upgrade表示該連接將要被升級為WebSocket連接;
(3)Sec-WebSocket-Key 標(biāo)識連接的Key串(下方有更多解釋)
(4)Sec-WebSocket-Version 指定了WebSocket的協(xié)議版本。

如果服務(wù)器識別key正確,會接收這個請求,就會響應(yīng)如下:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=  // 服務(wù)端隨機(jī)串

服務(wù)端Accept串是根據(jù)客戶端隨機(jī)串計算出來的,計算規(guī)則為:(1)與固定串拼接,(2)執(zhí)行sha1算法,(3)轉(zhuǎn)為base64字符串。這對Key/Accept需ws客戶端和服務(wù)端提前約定,目的是為了避免非法ws請求等一些常見的意外情況。并不能確保數(shù)據(jù)安全性,畢竟算法公開且簡單。公式如下:

toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )

響應(yīng)碼101表示將切換協(xié)議,更改后的協(xié)議就是Upgrade: websocket指定的WebSocket協(xié)議。當(dāng)連接建立成功后,雙方就可以自由通訊消息了。消息一般分兩種:(1)文本,(2)二進(jìn)制數(shù)據(jù)。開發(fā)中會使用JSON文本數(shù)據(jù)比較直觀。
?

# ws為什么能實現(xiàn)全雙工通訊

? 前文多次遇到 全雙工通信 字眼,意思就是客戶端和服務(wù)端能隨時給對方發(fā)送消息。好像理解了但又朦朦朧朧。這里解釋一下:

  • 單工: 數(shù)據(jù)傳輸只支持在一個方向上的傳輸,同時只能有一方發(fā)送或接收消息。
  • 半雙工:數(shù)據(jù)允許在兩個方向上傳輸,但任一時刻,只允許有一方在傳輸,是一種切換方向的單工通信
  • 全雙工:任何時刻都允許兩個方向進(jìn)行數(shù)據(jù)傳輸,不受對方限制。

? HTTP 和WebSocket 都是基于TCP傳輸協(xié)議的,其實TCP本身是支持全雙工通訊的,而HTTP協(xié)議的請求,因為其應(yīng)答機(jī)制限制了全雙工通信。當(dāng)?shù)谝淮挝帐滞瓿珊?,協(xié)議由HTTP切換成了WebSocket,ws連接建立,其實只是簡單規(guī)定了下:后續(xù)通訊不再使用http協(xié)議,雙發(fā)可以互相發(fā)送數(shù)據(jù)了。

http、WebSocket及TCP的關(guān)系(圖片摘自網(wǎng)絡(luò))

?

# 安全的WebSocket通訊

? 與 HTTPS 類似,安全的ws連接使用的是wss://...開頭的請求,它首先會通過https創(chuàng)建安全的連接,升級協(xié)議后,底層通信依然走的 SSL/TLS 協(xié)議
?

# 連接保持 - 心跳

? WebSocket為了保持客戶端與服務(wù)端的實時雙向通訊,需保持TCP通道鏈接沒有斷開。然而長時間沒有數(shù)據(jù)往來的連接,會浪費一些連接資源,網(wǎng)絡(luò)鏈路同樣可能被網(wǎng)關(guān)關(guān)閉,畢竟網(wǎng)關(guān)不是我們能控制的。因此鏈路鏈接就需要提示說明還在使用周期內(nèi),這個提示就是心跳來實現(xiàn)的。

  • 發(fā)送方 --> 接收方: ping
  • 接收方 --> 發(fā)送方: pong
    舉例,ws服務(wù)端向客戶端發(fā)送ping,代碼如下
ws.ping('', false, true)

?

$ WebSocket API

? 理解了WebSocket的概念及相應(yīng)的特征后,來看看怎么上手編寫

# 創(chuàng)建WebSocket實例

? ws提供了WebSocket(url[, protocals])構(gòu)造函數(shù)來返回實例化ws對象。參數(shù)一表示要連接的URL,參數(shù)二表示可接受的子協(xié)議。

let socket= new WebSocket('http://localhost:8080')

? 執(zhí)行以上代碼,瀏覽器就開始嘗試創(chuàng)建連接,與 xhr 的readystatechange 類似的是,ws連接也有一個表示當(dāng)前狀態(tài)的屬性readyState

# 連接狀態(tài)-readyState 只讀

? 用于返回當(dāng)前WebSocket連接的狀態(tài),其值即含義如下

狀態(tài)含義
0 WebSocket.CONNECTING
1 WebSocket.OPEN
2 WebSocket.CLOSING
3 WebSocket.CLOSED

一個ws連接各個狀態(tài)的執(zhí)行時刻如下

let socket = new WebSocket('http://localhost:8080')
// 正在創(chuàng)建連接
console.log('[readyState]:', socket.readyState) // 0

// 連接建立成功后觸發(fā)onopen回調(diào)
socket.onopen = function() {
  console.log('connected,[readyState]:', socket.readyState) // 1
  // 發(fā)送消息
  socket.send('from client: Hello')
}

// 從服務(wù)端收到信息觸發(fā)onmessage回調(diào)
socket.onmessage = function() {
  console.log('received,[readyState]:', socket.readyState) // 1
  // 發(fā)送消息
  socket.send('from client: Hello')
}

// 連接失敗觸發(fā)onerror回調(diào)
socket.onerror = function() {
  console.log('connect error, [readyState]:', socket.readyState)  // 3
}

// 調(diào)用關(guān)閉連接,狀態(tài)立刻變成2(正在關(guān)閉)。關(guān)閉成功觸發(fā)onclose變成3
socket.close()

// 連接關(guān)閉觸發(fā)onclose回調(diào),有回調(diào)參數(shù)
socket.onclose = function(event) {
  const { code, reason, wasClean } = event
  console.log('connect closed, [readyState]:', socket.readyState) // 3
  console.log(code, reason, wasClean) // wasClean表示連接是否已經(jīng)關(guān)閉。boolean
}

? 當(dāng)readyState的值從 0 變成 1 后,客戶端和服務(wù)端就可以通訊了。
?

# 方法

- 發(fā)送數(shù)據(jù) send()

? 發(fā)送數(shù)據(jù)一定是伴隨在連接已經(jīng)打開的情況下

socket.addEventListener('open', function(event) {
  sokcet.send('hello server')
})
- 關(guān)閉連接 close()

? 關(guān)閉當(dāng)前連接??梢詡?0/1/2 個參數(shù)。code解釋關(guān)閉原因的狀態(tài)碼。reason解釋關(guān)閉原因的描述(限制123個字節(jié))。

sokcet.close([code[, reason]])

如果未傳參數(shù),會默認(rèn)code1005,意為:無參數(shù),未提供關(guān)閉原因狀態(tài)碼。查看 狀態(tài)碼詳情。如果提供一個無效的狀態(tài)碼,會拋出異常INVALID_ACCESS_ERR
?

# 事件

- 連接已建立 onopen
socket.addEventListener('open', function(event)  {
  // TODO: send message
});
- 接收服務(wù)端消息回調(diào) onmessage

? 當(dāng)服務(wù)器向客戶端發(fā)來消息時,WebSocket對象會觸發(fā)message事件。這個message事件與其他傳遞消息的協(xié)議類似,也是把返回的數(shù)據(jù)保存在event.data屬性中

socket.addEventListener('message', function(event)  {
  var data = event.data;
  // TODO:
});
- 關(guān)閉連接的回調(diào) onclose
socket.addEventListener('close', function(event)  {
  const { code, reason, wasClean } = event
  // TODO:
});
- 連接失敗的回調(diào) onerror
socket.addEventListener('error', function(event)  {
  console.error("WebSocket error observed:", event)
});

?

# 屬性

- 當(dāng)前剩余未發(fā)送數(shù)據(jù) bufferedAmount 只讀

? 用于返回已經(jīng)被send()方法放入隊列但還沒有被發(fā)送到網(wǎng)絡(luò)中的數(shù)據(jù)的字節(jié)數(shù),只有發(fā)送完成它才會被重置為0。如果發(fā)送過程中連接被關(guān)閉不會重置,不斷的調(diào)用send()該值會不斷增長。

if (ws.bufferedAmount === 0){
    console.log("發(fā)送已完成");
} else {
    console.log("還有", ws.bufferedAmount, "數(shù)據(jù)沒有發(fā)送");
}
- 連接二進(jìn)制類型 binaryType 只讀

? 返回websocket連接所傳輸二進(jìn)制數(shù)據(jù)的類型

const binaryType = socket.binaryType
- 已選擇的擴(kuò)展值 extensions 只讀

? 返回服務(wù)器已選擇的擴(kuò)展值

const extensions = socket.extensions 
- 子協(xié)議 protocol 只讀

? 返回服務(wù)器端選中的子協(xié)議的名字;也就是在實例化WebSocket對象時,在參數(shù)protocols中指定的字符串

const protocol = socket.protocol  
- 子協(xié)議 url 只讀

? 返回值為當(dāng)構(gòu)造函數(shù)創(chuàng)建WebSocket實例對象時URL的絕對路徑。

const url = socket.url 

?

$ 一個服務(wù)端實例

這里提供一個簡單的例子,引入了ws庫實現(xiàn)。也可以使用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);

?

結(jié)束語

參考文獻(xiàn)
WebSocket-菜鳥教程
WebSocket-MDN
WebSocket-廖雪峰的官方網(wǎng)站

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-WebSo...
    敢夢敢當(dāng)閱讀 9,032評論 0 50
  • WebSocket 機(jī)制 WebSocket 是 HTML5 一種新的協(xié)議。它實現(xiàn)了瀏覽器與服務(wù)器全雙工通信,能更...
    勇敢的_心_閱讀 2,375評論 0 4
  • Socket并非是一個協(xié)議,而是為了方便使用TCP而抽象出來的一層,是位于應(yīng)用層和傳輸控制層之間的一組接口。換句話...
    JunChow520閱讀 3,520評論 0 4
  • 1 述 WebSocket是一種網(wǎng)絡(luò)通信協(xié)議WebSocket 協(xié)議在2008年誕生,2011年成為國際標(biāo)準(zhǔn)。HT...
    凱玲之戀閱讀 776評論 0 0
  • 此教程無需R語言 Step 1 確定需要檢索的芯片 例如 GSE33006 Step 2 利用GEO2R分析差異基...
    Fobbite0579閱讀 12,639評論 0 5

友情鏈接更多精彩內(nèi)容