WebSocket協議解析


WebSocket協議解析

轉載請注明出處:WebSocket解析

現在,很多網站為了實現推送技術,所用的技術都是輪詢。輪詢是指在特定的時間間隔(如每一秒),由瀏覽器對服務器發起HTTP請求,然后由服務器返回數據給瀏覽器
。由於HTTP協議是惰性的,只有客戶端發起請求,服務器才會返回數據。輪詢技術實現的前提條件同樣是基於這種機制。而WebSocket屬於服務端推送技術,本質是一種應用層協議,可以實現持久連接的全雙工雙向通信。在介紹WebSocket之前,先談談輪詢技術和HTTP流技術。

文章目錄

  • 傳統輪詢技術:Ajax短輪詢
  • Comet
    • Ajax長輪詢
    • HTTP流
  • HTML5實現服務端推送
    • SSE
    • WebSocket

Ajax短輪詢(Ajax Polling)

Ajax短輪詢即客戶端周期性的向服務器發起HTTP請求,不管服務器是否真正獲取到數據,都會向客戶端返回響應。每個request對應一個response,由於HTTP/1.1的持久連接(建立一次TCP連接,發送多個請求)和管線化技術(異步發送請求),使得HTTP請求可以在建立一次TCP連接之后發起多個異步請求。

這種傳統的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務器發出請求,然而HTTP請求在每次發送時都會帶上很長的請求頭部字段,其中真正有效的數據可能只是很小的一部分(如Cookie字段),顯然服務器會浪費帶寬等資源。

有朋友可能會想,那可以加大Ajax的傳輸時間,如改為3s為一個周期。但是時間長了,對於實時性要求比較高的項目來說,頁面更新的數據也就太慢了。

Comet(服務端推送)

而比較新的技術向服務器輪詢獲取數據的實現是Comet,即服務端推送。簡單的說,服務端推送就是在客戶端發起HTTP請求之后,服務器可以主動的向客戶端推送數據。實現Comet的方式有兩種:Ajax長輪詢和HTTP流。

Ajax長輪詢(Ajax Long-polling)

Ajax長輪詢本身不是一個真正的推送。長輪詢是短輪詢的一種變體。在客戶端向服務器發起HTTP請求之后,服務器並不是每次都立即響應:當服務器得到最新數據時,會向客戶端傳輸數據;當數據沒有更新時,服務器會保持這個連接,等待更新數據之后,才向客戶端傳輸數據。當然,如果服務端數據長時間沒有更新,一段時間后,請求就會超時。客戶端收到超時信息后,會重新發送一個HTTP請求給服務器。

也就是說,只有在服務器獲取更新后的數據,才會向客戶端傳輸數據。這種方式也存在弊端。雖然服務端可以主動的向客戶端傳輸數據,但是依然需要反復發出請求(HTTP請求數量比短輪詢少很多)。

短輪詢和長輪詢的相同點在於客戶端都需要向服務器發起HTTP請求,不同點在於服務器如何響應:短輪詢是服務器立即響應,不管數據是否有效;長輪詢是等待數據更新后響應。

HTTP流

HTTP流不同於輪詢技術,HTTP流只建立一次TCP連接,在3次握手之后進行HTTP通信,此時客戶端向服務器發起一個HTTP請求,而服務器保持連接打開,周期性的向客戶端傳輸數據。雙方在沒有明確提出斷開連接時,服務器就會持續向客戶端傳輸數據。也就是說,假如服務器數據沒有更新,服務器不會返回響應,而是保持連接;如果數據更新了,會立即將數據傳輸給客戶端。此時會發起下一個HTTP請求,過程周而復始。

在JS中,可以通過偵聽readystatechange事件及檢測readyState的值是否為3來實現HTTP流。隨着不斷從服務器接收數據,readyState的值會周期性的變為3。當readyState值變為3時,responseText屬性就會保存接受到的所有數據。此時,就需要比較此前接收到的數據,決定從什么位置開始取得最新的數據。用XHR對象實現HTTP流的方式如下:

let httpStream = (url, processor, finished) => {
  let xhr = new XMLHttpRequest()
  let received = 0
  xhr.open(url, 'get', true)
  xhr.addEvetntListener('readystatechange', () => {
    let result
    if (xhr.readyState === 3) {
      result = xhr.responseText.slice(received)
      received += result.length
      processor(result)
    } else if (xhr.readyState === 4) {
      finished(xhr.responseText)
    }
  })
}

只要readyState為3,就對responseText進行分隔以獲取最新數據。這里的received表示記錄已經處理了多少字符。然后通過processor回調函數來處理最新數據。而當readyState為4時,表示數據已經完全獲取到,則直接將xhr.responseText傳入finished回調函數處理即可。

調用方式如下.

httpStream(url, data => {
  console.log(data)
}, finishedData => {
  console.log(data)
})

對(長、短)輪詢和HTTP流做一個小小的總結

  1. 傳統輪詢技術(Ajax短輪詢)是客戶端向服務器發起HTTP請求,無論數據是否更新,服務器都會傳輸數據。一個request對應一個response。
  2. 服務器推送技術(Ajax長輪詢)是短輪詢的變種,是客戶端向服務器發起HTTP請求,只有等待數據更新后才會傳輸數據,否則服務器保持連接狀態。接着發起下一次HTTP請求,一個request對應一個response。
  3. 服務器推送技術(HTTP流),在客戶端只發起一次HTTP請求,服務器保持連接狀態,在數據更新之后,服務器會傳輸數據,否則保持連接狀態。此時一個requset對應多個response。
  4. 無論是短輪詢、長輪詢,還是HTTP流,相同點在於都需要客戶端先發起HTTP請求

HTML5實現服務端推送

由於服務器推送的重要性(實現賽事結果更新、聊天室等),HTML5實現了兩個服務端推送接口,SSE和WebSocket。

SSE

SSE(Server-Sent Eevents,服務器發送事件)用於創建到服務器的單向連接,服務器通過這個連接可以發送任意數量的數據。實現SSE有以下幾點要求

  1. 服務器響應的MIME類型必須是text/event-stream。
  2. 必須按照指定的格式輸出。

用法如下,其實理解了服務器推送之后,SSE使用起來相對簡單

// EventSource接受的參數必須同源。
// 使用message事件監聽從服務器收到的消息,並存儲在event.data對象里。
let source = new EventSource('index.php')
source.onmessage = e => {
  console.log(e.data)
}

SSE在IE下都不支持,ios4.0以上、android4.4以上都支持SSE。

WebSocket

鋪墊了那么久的前文,終於到WebSocket了... 😃 感謝各位朋友不嫌棄的耐心閱讀。

簡單來說,WebSocket是一種協議,與HTTP協議一樣位於應用層,都是TCP/IP協議的子集。HTTP協議是單向通信協議,只有客戶端發起HTTP請求,服務端才會返回數據。而WebSocket協議是雙向通信協議,在建立連接之后,客戶端和服務器都可以主動向對方發送或接受數據。WebSocket協議建立的前提需要借助HTTP協議,建立連接之后,持久連接的雙向通信就與HTTP協議無關了。

WebSocket協議的目標是在一個獨立的持久連接上提供全雙工雙向通信。客戶端和服務器可以向對方主動發送和接受數據。在JS中創建WebSocket后,會有一個HTTP請求發向瀏覽器以發起請求。在取得服務器響應后,建立的連接會使用HTTP升級將HTTP協議轉換為WebSocket協議。也就是說,使用標准的HTTP協議無法實現WebSocket,只有支持那些協議的專門瀏覽器才能正常工作。

請認真閱讀、記住上面一段話。: )

由於WebScoket使用了自定義協議,所以URL與HTTP協議略有不同。未加密的連接為ws://,而不是http://。加密的連接為wss://,而不是https://。

使用JavaScript是實現WebScoket協議相對簡單,以下是WebSocket APIs

// 打開WebSocket, 傳遞的參數url沒有同源策略的限制。
let websocket = new WebSocket(url)

// 監聽open事件,在成功建立websocket時向url發送純文本字符串數據(如果是對象則必須序列化處理)。
websocket.onopen = () => {
  if (websocket.readyState === WebSocket.OPEN) {
    websocket.send('hello world')
  }
}

// 監聽message事件,在服務器響應時接受數據。返回的數據存儲在事件對象中。
websocket.onmessage = e => {
  let data = e.data
  console.log(data)
}

// 監聽error事件,在發生錯誤時觸發,連接不能持續。
websocket.onerror = () => {
  console.log('websocket connecting error!!')
}

// 監聽close事件,在連接關閉時觸發。只有close事件的事件對象擁有額外的信息。可以通過這些信息來查看關閉狀態
websocket.onclose = e => {
  let clean = e.wasClean // 是否已經關閉
  let code = e.code // 服務器返回的數值狀態碼。
  let reason = e.reason //服務器返回的消息。
}

注意,WebScoket不支持DOM2語法為事件綁定事件處理程序,因此必須使用DOM0級語法來每個事件綁定事件處理程序。

// correct!
websocket.onerror = () => {}
// error!
websocket.addEventListener('error', () => {})

看完了WebSocket APIs之后,再來看看WebSocket是如何實現連接的(奶思,看到這里的朋友耐心真棒.. 只剩下一點點了:) )

WebSocket是應用層協議,是TCP/IP協議的子集,通過HTTP/1.1協議的101狀態碼進行握手。也就是說,WebSocket協議的建立需要先借助HTTP協議,在服務器返回101狀態碼之后,就可以進行websocket全雙工雙向通信了,就沒有HTTP協議什么事情了

參照wiki握手協議的例子:並對一些字段進行說明。

Connection:Connection必須設置為Upgrade,表示客戶端希望連接升級

Upgrade:Upgrade必須設置為WebSocket,表示在取得服務器響應之后,使用HTTP升級將HTTP協議轉換(升級)為WebSocket協議。

Sec-WebSocket-key:隨機字符串,用於驗證協議是否為WebSocket協議而非HTTP協議

Sec-WebSocket-Version:表示使用WebSocket的哪一個版本。

Sec-WebSocket-Accept:根據Sec-WebSocket-Accept和特殊字符串計算。驗證協議是否為WebSocket協議。

Sec-WebSocket-Location:與Host字段對應,表示請求WebSocket協議的地址。

HTTP/1.1 101 Switching Protocols:101狀態碼表示升級協議,在返回101狀態碼后,HTTP協議完成工作,轉換為WebSocket協議。此時就可以進行全雙工雙向通信了。

WebSocket協議的瀏覽器兼容性較好。

參考資料:

1.《JavaScript高級程序設計 第三版》

2.wiki: WebSocket

3.wiki 服務端推送技術

4.WebSocket 教程

5.WebSocket 與 Socket.IO

6.WebSocket 是什么原理?為什么可以實現持久連接?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM