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提供了一個文明優雅的全雙工通信方案。一般適合於對數據的實時性要求比較強的場景,如通信、股票、直播、共享桌面,特別適合於客戶端與服務頻繁交互的情況下,如聊天室、實時共享、多人協作等平臺。

特點

建立在 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 > HTTPHTTPS: TCP > TLS > HTTPWS: TCP > WSWSS: 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應用程序調用 newWebSocket(url)接口時,客戶端就開始了與地址為url的WebServer建立握手連接的過程。

客戶端與服務端通過TCP三次握手建立連接,如果這個建立連接失敗,那麼後面的過程就不會執行,Web應用程序將收到錯誤消息通知。在TCP建立連接成功後,客戶端通過HTTP協議傳送WebSocket支持的版本號、協議的字版本號、原始地址、主機地址等等一些列字段給服務器端。服務端收到客戶端發送來的握手請求後,如果數據包數據和格式正確、客戶端和服務端的協議版本號匹配等等,就接受本次握手連接,並給出相應的數據回覆,同樣回覆的數據包也是採用HTTP協議傳輸。客戶端收到服務端回覆的數據包後,如果數據包內容、格式都沒有問題的話,就表示本次連接成功,觸發 onopen,此時Web開發者就可以在此時通過 send()向服務器發送數據。否則握手連接失敗,Web應用程序觸發 onerror,並且能知道連接失敗的原因。

這個握手很像HTTP,但是實際上卻不是,它允許服務器以HTTP的方式解釋一部分handshake的請求,然後切換為websocket。

2.3 WebSocket握手報文

一個瀏覽器發出的WebSocket請求報文類似於:

HTTP1.1協議規定,Upgrade頭信息表示將通信協議從HTTP/1.1轉向該項所指定的協議。

Connection:Upgrade表示瀏覽器通知服務器,如果可以,就升級到webSocket協議。Origin用於驗證瀏覽器域名是否在服務器許可的範圍內。Sec-WebSocket-Key則是用於握手協議的密鑰,是瀏覽器生成的Base64編碼的16字節隨機字符串。Sec-WebSocket-Protocol是一個用戶定義的字符串,用來區分同URL下,不同的服務所需要的協議。Sec-WebSocket-Version是告訴服務器所使用的協議版本。

服務端WebSocket回覆報文:

服務器端同樣用 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來指示連接的當前狀態:

一個示例:

ws.onopen / ws.onclose

實例對象的 onopen屬性,用於指定連接成功後的回調函數。

如果要指定多個回調函數,可以 addEventListener。

實例對象的 onclose屬性,用於指定連接關閉後的回調函數。

ws.onmessage

實例對象的 onmessage屬性,用於指定收到服務器數據後的回調函數。

注意,服務器數據可能是文本,也可能是二進制數據(blob對象或Arraybuffer對象)。

除了動態判斷收到的數據類型,也可以使用 binaryType屬性,顯式指定收到的二進制數據類型。binaryType取值應當是'blob'或者'arraybuffer','blob'表示使用 Blob 對象,而'arraybuffer'表示使用 ArrayBuffer 對象。

查看DEMO

ws.bufferedAmount

實例對象的 bufferedAmount只讀屬性,表示還有多少字節的二進制數據沒有發送出去。它可以用來判斷髮送是否結束。該值會在所有隊列數據被髮送後重置為 0,而當連接關閉時不會設為0。如果持續調用send(),這個值會持續增長。

ws.onerror

實例對象的 onerror屬性,用於指定報錯時的回調函數。

3.2 ws上常用方法

ws.close()

關閉WebSocket連接或停止正在進行的連接請求。如果連接的狀態已經是closed,這個方法不會有任何效果。

ws.send()

實例對象的 send()方法用於向服務器發送數據。

最後一個ArrayBuffer對象栗子中的canvas_context實例是CanvasRenderingContext2D類型的對象,其上的 .getImageData()方法返回一個ImageData對象。

網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

文章轉至公眾號:前端下午茶

喜歡文章的記得點個關注不迷路~

私信+轉發回覆關

鍵字(資料)送你一份前端面試題