JS 服務器推送技術 WebSocket 入門指北

最近在工做中遇到了須要服務器推送消息的場景,這裏總結一下收集整理WebSocket相關資料的收穫。

1. 概述

1.1 服務器推送

WebSocket做爲一種通訊協議,屬於服務器推送技術的一種,IE10+支持。
服務器推送技術不止一種,有短輪詢、長輪詢、WebSocket、Server-sent Events(SSE)等,他們各有優缺點:


短輪詢最簡單,在一些簡單的場景也會常用,就是隔一段時間就發起一個ajax請求。那麼長輪詢是什麼呢?
長輪詢(Long Polling)是在Ajax輪詢基礎上作的一些改進,在沒有更新的時候再也不返回空響應,並且把鏈接保持到有更新的時候,客戶端向服務器發送Ajax請求,服務器接到請求後hold住鏈接,直到有新消息才返回響應信息並關閉鏈接,客戶端處理完響應信息後再向服務器發送新的請求。它是一個解決方案,但不是最佳的技術方案。
若是說短輪詢是客戶端不斷打電話問服務端有沒有消息,服務端回覆後馬上掛斷,等待下次再打;長輪詢是客戶端一直打電話,服務端接到電話不掛斷,有消息的時候再回復客戶端並掛斷。
SSE(Server-Sent Events)與長輪詢機制相似,區別是每一個鏈接不僅發送一個消息。客戶端發送一個請求,服務端保持這個鏈接直到有新消息發送回客戶端,仍然保持着鏈接,這樣鏈接就能夠支持消息的再次發送,由服務器單向發送給客戶端。然而IE直到11都不支持,很少說了....

1.2 WebSocket的特色

爲何已經有了輪詢還要WebSocket呢,是由於短輪詢和長輪詢有個缺陷:通訊只能由客戶端發起。
那麼若是後端想往前端推送消息須要前端去輪詢,不斷查詢後端是否有新消息,而輪詢的效率低且浪費資源(必須不停 setInterval 或 setTimeout 去鏈接,或者 HTTP 鏈接始終打開),WebSocket提供了一個文明優雅的全雙工通訊方案。通常適合於對數據的實時性要求比較強的場景,如通訊、股票、直播、共享桌面,特別適合於客戶端與服務頻繁交互的狀況下,如聊天室、實時共享、多人協做等平臺。

特色

  1. 創建在 TCP 協議之上,服務器端的實現比較容易。
  2. 與 HTTP 協議有着良好的兼容性。默認端口也是80和443,而且握手階段採用 HTTP 協議,所以握手時不容易屏蔽,能經過各類 HTTP 代理服務器。
  3. 數據格式比較輕量,性能開銷小,通訊高效。服務器與客戶端之間交換的標頭信息大概只有2字節;
  4. 能夠發送文本,也能夠發送二進制數據。
  5. 沒有同源限制,客戶端能夠與任意服務器通訊。
  6. 協議標識符是 ws(若是加密,則爲wss),服務器網址就是 URL。ex:ws://example.com:80/some/path
  7. 不用頻繁建立及銷燬TCP請求,減小網絡帶寬資源的佔用,同時也節省服務器資源;
  8. WebSocket是純事件驅動的,一旦鏈接創建,經過監聽事件能夠處理到來的數據和改變的鏈接狀態,數據都以幀序列的形式傳輸。服務端發送數據後,消息和事件會異步到達。
  9. 無超時處理。


2.2 創建鏈接的握手

當Web應用程序調用 newWebSocket(url)接口時,客戶端就開始了與地址爲url的WebServer創建握手鍊接的過程。
  1. 客戶端與服務端經過TCP三次握手創建鏈接,若是這個創建鏈接失敗,那麼後面的過程就不會執行,Web應用程序將收到錯誤消息通知。
  2. 在TCP創建鏈接成功後,客戶端經過HTTP協議傳送WebSocket支持的版本號、協議的字版本號、原始地址、主機地址等等一些列字段給服務器端。
  3. 服務端收到客戶端發送來的握手請求後,若是數據包數據和格式正確、客戶端和服務端的協議版本號匹配等等,就接受本次握手鍊接,並給出相應的數據回覆,一樣回覆的數據包也是採用HTTP協議傳輸。
  4. 客戶端收到服務端回覆的數據包後,若是數據包內容、格式都沒有問題的話,就表示本次鏈接成功,觸發 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出來對象實例 constws=newWebSocket('ws://localhost:8080'),再使用對象下掛載的屬性與方法來操做。後文都用ws來指代WebSocket的實例。
查看DEMO

3.1 ws上經常使用屬性

ws.readyState

WebSocket實例對象相似於XHR有個的只讀屬性 readyState來指示鏈接的當前狀態:
一個示例:

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 對象。
web

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);
}; 

複製代碼
查看DEMO

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對象。

網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~
文章轉至公衆號:前端下午茶
做者:SHERlocked93
  • 看完三件事
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我兩個個小忙:
  1. 點個「贊同」,讓更多的人也能看到這篇內容(喜歡不點贊同,都是耍流氓 -_-)
  2. 點個關注不迷路~讓咱們成爲長期關係
相關文章
相關標籤/搜索