WebSocket 淺析

版權聲明:本文由史燕飛原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/241html

來源:騰雲閣 https://www.qcloud.com/community前端

 

做者介紹:史燕飛(英文名:Jeri),16年畢業於武漢大學並加入騰訊。目前在騰訊雲從事前端開發工做,喜歡研究前端相關技術(如:計算機網絡、WebKit內核、React等),也喜歡關注數據挖掘及機器學習等前沿科技。web

在WebSocket API還沒有被衆多瀏覽器實現和發佈的時期,開發者在開發須要接收來自服務器的實時通知應用程序時,不得不求助於一些「hacks」來模擬實時鏈接以實現實時通訊,最流行的一種方式是長輪詢 。 長輪詢主要是發出一個HTTP請求到服務器,而後保持鏈接打開以容許服務器在稍後的時間響應(由服務器肯定)。爲了這個鏈接有效地工做,許多技術須要被用於確保消息不錯過,如須要在服務器端緩存和記錄多個的鏈接信息(每一個客戶)。雖然長輪詢是能夠解決這一問題的,但它會耗費更多的資源,如CPU、內存和帶寬等,要想很好的解決實時通訊問題就須要設計和發佈一種新的協議。算法

WebSocket 是伴隨HTML5發佈的一種新協議。它實現了瀏覽器與服務器全雙工通訊(full-duplex),能夠傳輸基於消息的文本和二進制數據。WebSocket 是瀏覽器中最靠近套接字的API,除最初創建鏈接時須要藉助於現有的HTTP協議,其餘時候直接基於TCP完成通訊。它是瀏覽器中最通用、最靈活的一個傳輸機制,其極簡的API 可讓咱們在客戶端和服務器之間以數據流的形式實現各類應用數據交換(包括JSON 及自定義的二進制消息格式),並且兩端均可以隨時向另外一端發送數據。在這個簡單的API 以後隱藏了不少的複雜性,並且還提供了更多服務,如:api

  • 鏈接協商和同源策略;
  • 與既有 HTTP 基礎設施的互操做;
  • 基於消息的通訊和高效消息分幀;
  • 子協議協商及可擴展能力。

所幸,瀏覽器替咱們完成了上述工做,咱們只須要簡單的調用便可。任何事物都不是完美的,設計限制和性能權衡始終會有,利用WebSocket 也不例外,在提供自定義數據交換協議同時,也再也不享有在一些本由瀏覽器提供的服務和優化,如狀態管理、壓縮、緩存等。數組

隨着HTML5的發佈,愈來愈多的瀏覽器開始支持WebSocket,若是你的應用還在使用長輪詢,那就能夠考慮切換了。下面的圖表顯示了在一種常見的使用案例下,WebSocket和長輪詢之間的帶寬消耗差別:
瀏覽器

1.WebSocket API

WebSocket 對象提供了一組 API,用於建立和管理 WebSocket 鏈接,以及經過鏈接發送和接收數據。瀏覽器提供的WebSocket API很簡潔,調用示例以下:緩存

var ws = new WebSocket('wss://example.com/socket'); // 建立安全WebSocket 鏈接(wss) ws.onerror = function (error) { ... } // 錯誤處理 ws.onclose = function () { ... } // 關閉時調用 ws.onopen = function () { // 鏈接創建時調用 ws.send("Connection established. Hello server!"); // 向服務端發送消息 } ws.onmessage = function(msg) { // 接收服務端發送的消息 if(msg.data instanceof Blob) { // 處理二進制信息 processBlob(msg.data); } else { processText(msg.data); // 處理文本信息 } } 

1.1.接收和發送數據

WebSocket提供了極簡的API,開發者能夠輕鬆的調用,瀏覽器會爲咱們完成緩衝、解析、重建接收到的數據等工做。應用只需監聽onmessage事件,用回調處理返回數據便可。 WebSocket支持文本和二進制數據傳輸,瀏覽器若是接收到文本數據,會將其轉換爲DOMString 對象,若是是二進制數據或Blob 對象,可直接將其轉交給應用或將其轉化爲ArrayBuffer,由應用對其進行進一步處理。從內部看,協議只關注消息的兩個信息:淨荷長度和類型(前者是一個可變長度字段),據以區別UTF-8 數據和二進制數據。示例以下:安全

var wss = new WebSocket('wss://example.com/socket'); ws.binaryType = "arraybuffer"; // 接收數據 wss.onmessage = function(msg) { if(msg.data instanceof ArrayBuffer) { processArrayBuffer(msg.data); } else { processText(msg.data); } } // 發送數據 ws.onopen = function () { socket.send("Hello server!"); socket.send(JSON.stringify({'msg': 'payload'})); var buffer = new ArrayBuffer(128); socket.send(buffer); var intview = new Uint32Array(buffer); socket.send(intview); var blob = new Blob([buffer]); socket.send(blob); } 

Blob 對象是包含有隻讀原始數據的類文件對象,可存儲二進制數據,它會被寫入磁盤;ArrayBuffer (緩衝數組)是一種用於呈現通用、固定長度的二進制數據的類型,做爲內存區域能夠存放多種類型的數據。服務器

對於將要傳輸的二進制數據,開發者能夠決定以何種方式處理,能夠更好的處理數據流,Blob 對象通常用來表示一個不可變文件對象或原始數據,若是你不須要修改它或者不須要把它切分紅更小的塊,那這種格式是理想的;若是你還須要再處理接收到的二進制數據,那麼選擇ArrayBuffer 應該更合適。

WebSocket 提供的信道是全雙工的,在同一個TCP 鏈接上,能夠雙向傳輸文本信息和二進制數據,經過數據幀中的一位(bit)來區分二進制或者文本。WebSocket 只提供了最基礎的文本和二進制數據傳輸功能,若是須要傳輸其餘類型的數據,就須要經過額外的機制進行協商。WebSocket 中的send( ) 方法是異步的:提供的數據會在客戶端排隊,而函數則當即返回。在傳輸大文件時,不要由於回調已經執行,就錯誤地覺得數據已經發送出去了,數據極可能還在排隊。要監控在瀏覽器中排隊的數據量,能夠查詢套接字的bufferedAmount 屬性:

var ws = new WebSocket('wss://example.com/socket'); ws.onopen = function () { subscribeToApplicationUpdates(function(evt) { if (ws.bufferedAmount == 0) ws.send(evt.data); }); }; 

前面的例子是向服務器發送應用數據,全部WebSocket 消息都會按照它們在客戶端排隊的次序逐個發送。所以,大量排隊的消息,甚至一個大消息,均可能致使排在它後面的消息延遲——隊首阻塞!爲解決這個問題,應用能夠將大消息切分紅小塊,經過監控bufferedAmount 的值來避免隊首阻塞。甚至還能夠實現本身的優先隊列,而不是盲目都把它們送到套接字上排隊。要實現最優化傳輸,應用必須關心任意時刻在套接字上排隊的是什麼消息!

1.2.子協議協商

在以往使用HTTP 或XHR 協議來傳輸數據時,它們能夠經過每次請求和響應的HTTP 首部來溝通元數據,以進一步肯定傳輸的數據格式,而WebSocket 並無提供等價的機制。上文已經提到WebSocket只提供最基礎的文本和二進制數據傳輸,對消息的具體內容格式是未知的。所以,若是WebSocket須要溝通關於消息的元數據,客戶端和服務器必須達成溝通這一數據的子協議,進而間接地實現其餘格式數據的傳輸。下面是一些可能策略的介紹:

  • 客戶端和服務器能夠提早肯定一種固定的消息格式,好比全部通訊都經過 JSON編碼的消息或者某種自定義的二進制格式進行,而必要的元數據做爲這種數據結構的一個部分;
  • 若是客戶端和服務器要發送不一樣的數據類型,那它們能夠肯定一個雙方都知道的消息首部,利用它來溝通說明信息或有關淨荷的其餘解碼信息;
  • 混合使用文本和二進制消息能夠溝通淨荷和元數據,好比用文本消息實現 HTTP首部的功能,後跟包含應用淨荷的二進制消息。

上面介紹了一些可能的策略來實現其餘格式數據的傳輸,肯定了消息的串行格式化,但怎麼確保客戶端和服務端是按照約定發送和處理數據,這個約定客戶端和服務端是如何協商的呢?這就須要WebSocket 提供一個機制來協商,這時WebSocket構造器方法的第二個可選參數就派上用場了,經過這個參數客戶端和服務端就能夠根據約定好的方式處理髮送及接收到的數據。
WebSocket構造器方法以下所示:

WebSocket WebSocket(
in DOMString url, // 表示要鏈接的URL。這個URL應該爲響應WebSocket的地址。 in optional DOMString protocols // 能夠是一個單個的協議名字字符串或者包含多個協議名字字符串的數組。默認設爲一個空字符串。 ); 

經過上述WebSocket構造器方法的第二個參數,客戶端能夠在初次鏈接握手時,能夠告知服務器本身支持哪一種協議。以下所示:

var ws = new WebSocket('wss://example.com/socket',['appProtocol', 'appProtocol-v2']); ws.onopen = function () { if (ws.protocol == 'appProtocol-v2') { ... } else { ... } } 

如上所示,WebSocket 構造函數接受了一個可選的子協議名字的數組,經過這個數組,客戶端能夠向服務器通告本身可以理解或但願服務器接受的協議。當服務器接收到該請求後,會根據自身的支持狀況,返回相應信息。

  • 有支持的協議,則子協議協商成功,觸發客戶端的onopen回調,應用能夠查詢WebSocket 對象上的protocol 屬性,從而得知服務器選定的協議;
  • 沒有支持的協議,則協商失敗,觸發onerror 回調,鏈接斷開。

1.3.WS與WSS

WebSocket 資源URI採用了自定義模式:ws 表示純文本通訊( 如ws://example.com/socket),wss 表示使用加密信道通訊(TCP+TLS)。爲何不使用http而要自定義呢?
WebSocket 的主要目的,是在瀏覽器中的應用與服務器之間提供優化的、雙向通訊機制。但是,WebSocket 的鏈接協議也能夠用於瀏覽器以外的場景,能夠經過非HTTP協商機制交換數據。考慮到這一點,HyBi Working Group 就選擇採用了自定義的URI模式:

  • ws協議:普通請求,佔用與http相同的80端口;
  • wss協議:基於SSL的安全傳輸,佔用與tls相同的443端口。

各自的URI以下:

ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] 

不少現有的HTTP 中間設備可能不理解新的WebSocket 協議,而這可能致使各類問題:盲目的鏈接升級、意外緩衝WebSocket 幀、不明就裏地修改內容、把WebSocket 流量誤看成不完整的HTTP 通訊,等等。這時WSS就提供了一種不錯的解決方案,它創建一條端到端的安全通道,這個端到端的加密隧道對中間設備模糊了數據,所以中間設備就不能再感知到數據內容,也就沒法再對請求作特殊處理。

2. WebSocket協議

HyBi Working Group 制定的WebSocket 通訊協議(RFC 6455)包含兩個高層組件:開放性HTTP 握手用於協商鏈接參數,二進制消息分幀機制用於支持低開銷的基於消息的文本和二進制數據傳輸。WebSocket 協議嘗試在既有HTTP 基礎設施中實現雙向HTTP 通訊,所以也使用HTTP 的80 和443 端口。不過,這個設計不限於經過HTTP 實現WebSocket 通訊,將來的實現能夠在某個專用端口上使用更簡單的握手,而沒必要從新定義一個協議。WebSocket 協議是一個獨立完善的協議,能夠在瀏覽器以外實現。不過,它的主要應用目標仍是實現瀏覽器應用的雙向通訊。

2.1.數據成幀

WebSocket 使用了自定義的二進制分幀格式,把每一個應用消息切分紅一或多個幀,發送到目的地以後再組裝起來,等到接收到完整的消息後再通知接收端。基本的成幀協議定義了幀類型有操做碼、有效載荷的長度,指定位置的Extension data和Application data,統稱爲Payload data,保留了一些特殊位和操做碼供後期擴展。在打開握手完成後,終端發送一個關閉幀以前的任什麼時候間裏,數據幀可能由客戶端或服務器的任何一方發送。具體的幀格式以下所示:

0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ 
  • FIN: 1 bit 。表示此幀是不是消息的最後幀,第一幀也多是最後幀。
  • RSV1,RSV2,RSV3: 各1 bit 。必須是0,除非協商了擴展定義了非0的意義。
  • opcode:4 bit。表示被傳輸幀的類型:x0 表示一個後續幀;x1 表示一個文本幀;x2 表示一個二進制幀;x3-7 爲之後的非控制幀保留;x8 表示一個鏈接關閉;x9 表示一個ping;xA 表示一個pong;xB-F 爲之後的控制幀保留。
  • Mask: 1 bit。表示淨荷是否有掩碼(只適用於客戶端發送給服務器的消息)。
  • Payload length: 7 bit, 7 + 16 bit, 7 + 64 bit。 淨荷長度由可變長度字段表示: 若是是 0~125,就是淨荷長度;若是是 126,則接下來 2 字節表示的 16 位無符號整數纔是這一幀的長度; 若是是 127,則接下來 8 字節表示的 64 位無符號整數纔是這一幀的長度。
  • Masking-key:0或4 Byte。 用於給淨荷加掩護,客戶端到服務器標記。
  • Extension data: x Byte。默認爲0 Byte,除非協商了擴展。
  • Application data: y Byte。 在"Extension data"以後,佔據了幀的剩餘部分。
  • Payload data: (x + y) Byte。"extension data" 後接 "application data"。

幀:最小的通訊單位,包含可變長度的幀首部和淨荷部分,淨荷可能包含完整或部分應用消息。
消息:一系列幀,與應用消息對等。

是否把消息分幀由客戶端和服務器實現決定,應用並不須要關注WebSocket幀和如何分幀,由於客戶端(如瀏覽器)和服務端爲完成該工做。那麼客戶端和服務端是按照什麼規則進行分幀的呢?RFC 6455規定的分幀規則以下:

  1. 一個未分幀的消息包含單個幀,FIN設置爲1,opcode非0。
  2. 一個分幀了的消息包含:開始於:單個幀,FIN設爲0,opcode非0;後接 :0個或多個幀,FIN設爲0,opcode設爲0;終結於:單個幀,FIN設爲1,opcode設爲0。一個分幀了消息在概念上等價於一個未分幀的大消息,它的有效載荷長度等於全部幀的有效載荷長度的累加;然而,有擴展時,這可能不成立,由於擴展定義了出現的Extension data的解釋。例如,Extension data可能只出如今第一幀,並用於後續的全部幀,或者Extension data出現於全部幀,且只應用於特定的那個幀。在缺乏Extension data時,下面的示例示範了分幀如何工做。舉例:如一個文本消息做爲三個幀發送,第一幀的opcode是0x1,FIN是0,第二幀的opcode是0x0,FIN是0,第三幀的opcode是0x0,FIN是1。   
  3. 控制幀可能被插入到分幀了消息中,控制幀必須不能被分幀。若是控制幀不能插入,例如,若是是在一個大消息後面,ping的延遲將會很長。所以要求處理消息幀中間的控制幀。
  4. 消息的幀必須以發送者發送的順序傳遞給接受者。
  5. 一個消息的幀必須不能交叉在其餘幀的消息中,除非有擴展可以解釋交叉。
  6. 一個終端必須可以處理消息幀中間的控制幀。
  7. 一個發送者可能對任意大小的非控制消息分幀。
  8. 客戶端和服務器必須支持接收分幀和未分幀的消息。
  9. 因爲控制幀不能分幀,中間設施必須不嘗試改變控制幀。
  10. 中間設施必須不修改消息的幀,若是保留位的值已經被使用,且中間設施不明白這些值的含義。

在遵循了上述分幀規則以後,一個消息的全部幀屬於一樣的類型,由第一個幀的opcdoe指定。因爲控制幀不能分幀,消息的全部幀的類型要麼是文本、二進制數據或保留的操做碼中的一個。
雖然客戶端和服務端都遵循一樣的分幀規則,但也是有些差別的。在客戶端往服務端發送數據時,爲防止客戶端中運行的惡意腳本對不支持WebSocket 的中間設備進行緩存投毒攻擊(cache poisoning attack),發送幀的淨荷都要使用幀首部中指定的值加掩碼。被標記的幀必須設置MASK域爲1,Masking-key必須完整包含在幀裏,它用於標記Payload data。Masking-key是由客戶端隨機選擇的32位值,標記鍵應該是不可預測的,給定幀的Masking-key必須不能簡單到服務器或代理能夠預測Masking-key是用於一序列幀的,不可預測的Masking-key是阻止惡意應用的做者從wire上獲取數據的關鍵。因爲客戶端發送到服務端的信息須要進行掩碼處理,因此客戶端發送數據的分幀開銷要大於服務端發送數據的開銷,服務端的分幀開銷是2~10 Byte,客戶端是則是6~14 Byte。

控制幀

控制幀由操做碼標識,操做碼的最高位是1。當前爲控制幀定義的操做碼有0x8(關閉)、0x9(Ping)和0xA(Pong),操做碼0xB-0xF是保留的,未定義。控制幀用來交流WebSocket的狀態,可以插入到消息的多個幀的中間。全部的控制幀必須有一個小於等於125字節的有效載荷長度,必須不能被分幀。

  • 關閉:操做碼爲0x8。關閉幀可能包含一個主體(幀的應用數據部分)指明關閉的緣由,如終端關閉,終端接收到的幀太大,或終端接收到的幀不符合終端的預期格式。從客戶端發送到服務器的關閉幀必須標記,在發送關閉幀後,應用程序必須再也不發送任何數據。若是終端接收到一個關閉幀,且先前沒有發送關閉幀,終端必須發送一個關閉幀做爲響應。終端可能延遲發送關閉幀,直到它的當前消息發送完成。在發送和接收到關閉消息後,終端認爲WebSocket鏈接已關閉,必須關閉底層的TCP鏈接。服務器必須當即關閉底層的TCP鏈接;客戶端應該等待服務器關閉鏈接,但並不是必須等到接收關閉消息後才關閉,若是它在合理的時間間隔內沒有收到反饋,也能夠將TCP關閉。若是客戶端和服務器同時發送關閉消息,兩端都已發送和接收到關閉消息,應該認爲WebSocket鏈接已關閉,並關閉底層TCP鏈接。

  • Ping:操做碼爲0x9。一個Ping幀可能包含應用程序數據。當接收到Ping幀,終端必須發送一個Pong幀響應,除非它已經接收到一個關閉幀。它應該儘快返回Pong幀做爲響應。終端可能在鏈接創建後、關閉前的任意時間內發送Ping幀。注意:Ping幀可做爲keepalive或做爲驗證遠程終端是否可響應的手段。

  • Pong:操做碼爲0xA。Pong 幀必須包含與被響應Ping幀的應用程序數據徹底相同的數據。若是終端接收到一個Ping 幀,且尚未對以前的Ping幀發送Pong 響應,終端可能選擇發送一個Pong 幀給最近處理的Ping幀。一個Pong 幀可能被主動發送,這做爲單向心跳。對主動發送的Pong 幀的響應是不但願的。

數據幀

數據幀攜帶須要發送的目標數據,由操做碼標識,操做碼的最高位是0。當前爲數據幀定義的(文本),0x2(二進制),操做碼0x3-0x7爲之後的非控制幀保留,未定義。
操做碼決定了數據的解釋:

  • 文本:操做碼爲0x1。有效載荷數據是UTF-8編碼的文本數據。特定的文本幀可能包含部分的UTF-8 序列,然而,整個消息必須包含有效的UTF-8,當終端以UTF-8解釋字節流時發現字節流不是一個合法的UTF-8流,那麼終端將關閉鏈接。
  • 二進制:操做碼爲0x2。有效載荷數據是任意的二進制數據,它的解釋由應用程序層惟一決定。

2.2.協議擴展

從上述的數據分幀格式能夠知道,有不少擴展位預留,WebSocket 規範容許對協議進行擴展,可使用這些預留位在基本的WebSocket 分幀層之上實現更多的功能。
下面是負責制定WebSocket 規範的HyBi Working Group進行的兩項擴展:

  • 多路複用擴展(A Multiplexing Extension for WebSockets):這個擴展能夠將WebSocket 的邏輯鏈接獨立出來,實現共享底層的TCP 鏈接。每一個WebSocket 鏈接都須要一個專門的TCP 鏈接,這樣效率很低。多路複用擴展解決了這個問題。它使用「信道ID」擴展每一個WebSocket 幀,從而實現多個虛擬的WebSocket 信道共享一個TCP 鏈接。
  • 壓縮擴展(Compression Extensions for WebSocket):給WebSocket 協議增長了壓縮功能。基本的WebSocket 規範沒有壓縮數據的機制或建議,每一個幀中的淨荷就是應用提供的淨荷。雖然這對優化的二進制數據結構不是問題,但除非應用實現本身的壓縮和解壓縮邏輯,不然不少狀況下都會形成傳輸載荷過大的問題。實際上,壓縮擴展就至關於HTTP 的傳輸編碼協商。

要使用擴展,客戶端必須在第一次的Upgrade 握手中通知服務器,服務器必須選擇並確認要在商定鏈接中使用的擴展。下面就是對升級協商的介紹。

2.3.升級協商

從上面的介紹可知,WebSocket具備很大的靈活性,提供了不少強大的特性:基於消息的通訊、自定義的二進制分幀層、子協議協商、可選的協議擴展等等。上面也講到,客戶端和服務端需先經過HTTP方式協商適當的參數後纔可創建鏈接,完成協商以後,全部信息的發送和接收再也不和HTTP相關,全由WebSocket自身的機制處理。固然,完成最初的鏈接參數協商並不是必須使用HTTP協議,它只是一種實現方案,能夠有其餘選擇。但使用HTTP協議完成最初的協商,有如下好處:讓WebSockets 與現有HTTP 基礎設施兼容:WebSocket 服務器能夠運行在80 和443 端口上,這一般是對客戶端惟一開放的端口;能夠重用並擴展HTTP 的Upgrade 流,爲其添加自定義的WebSocket 首部,以完成協商。
在協商過程當中,用到的一些頭域以下:

  • Sec-WebSocket-Version:客戶端發送,表示它想使用的WebSocket 協議版本(13表示RFC 6455)。若是服務器不支持這個版本,必須迴應本身支持的版本。

  • Sec-WebSocket-Key:客戶端發送,自動生成的一個鍵,做爲一個對服務器的「挑戰」,以驗證服務器支持請求的協議版本;

  • Sec-WebSocket-Accept:服務器響應,包含Sec-WebSocket-Key 的簽名值,證實它支持請求的協議版本;

  • Sec-WebSocket-Protocol:用於協商應用子協議:客戶端發送支持的協議列表,服務器必須只回應一個協議名;

  • Sec-WebSocket-Extensions:用於協商本次鏈接要使用的WebSocket 擴展:客戶端發送支持的擴展,服務器經過返回相同的首部確認本身支持一或多個擴展。

在進行HTTP Upgrade以前,客戶端會根據給定的URI、子協議、擴展和在瀏覽器狀況下的origin,先打開一個TCP鏈接,隨後再發起升級協商。升級協商具體以下:

GET /socket HTTP/1.1 // 請求的方法必須是GET,HTTP版本必須至少是1.1 Host: thirdparty.com Origin: http://example.com Connection: Upgrade Upgrade: websocket // 請求升級到WebSocket 協議 Sec-WebSocket-Version: 13 // 客戶端使用的WebSocket 協議版本 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 自動生成的鍵,以驗證服務器對協議的支持,其值必須是nonce組成的隨機選擇的16字節的被base64編碼後的值 Sec-WebSocket-Protocol: appProtocol, appProtocol-v2 // 可選的應用指定的子協議列表 Sec-WebSocket-Extensions: x-webkit-deflate-message, x-custom-extension // 可選的客戶端支持的協議擴展列表,指示了客戶端但願使用的協議級別的擴展 

在安全工程中,Nonce是一個在加密通訊只能使用一次的數字。在認證協議中,它每每是一個隨機或僞隨機數,以免重放攻擊。Nonce也用於流密碼以確保安全。若是須要使用相同的密鑰加密一個以上的消息,就須要Nonce來確保不一樣的消息與該密鑰加密的密鑰流不一樣。

與瀏覽器中客戶端發起的任何鏈接同樣,WebSocket 請求也必須遵照同源策略:瀏覽器會自動在升級握手請求中追加Origin 首部,遠程服務器可能使用CORS 判斷接受或拒絕跨源請求。要完成握手,服務器必須返回一個成功的「Switching Protocols」(切換協議)響應,具體以下:

HTTP/1.1 101 Switching Protocols // 101 響應碼確認升級到WebSocket 協議 Upgrade: websocket Connection: Upgrade Access-Control-Allow-Origin: http://example.com // CORS 首部表示選擇贊成跨源鏈接 Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 簽名的鍵值驗證協議支持 Sec-WebSocket-Protocol: appProtocol-v2 // 服務器選擇的應用子協議 Sec-WebSocket-Extensions: x-custom-extension // 服務器選擇的WebSocket 擴展 

全部兼容RFC 6455 的WebSocket 服務器都使用相同的算法計算客戶端挑戰的答案:將Sec-WebSocket-Key 的內容與標準定義的惟一GUID 字符串拼接起來,計算出SHA1 散列值,結果是一個base-64 編碼的字符串,把這個字符串發給客戶端便可。Sec-WebSocket-Accept 這個頭域的 ABNF [RFC2616]定義以下:

  Sec-WebSocket-Accept = base64-value-non-empty   base64-value-non-empty = (1*base64-data [ base64-padding ]) |   base64-padding   base64-data = 4base64-character   base64-padding = (2base64-character "==") |   (3base64-character "=")   base64-character = ALPHA | DIGIT | "+" | "/" 

若是客戶端發送的key值爲:dGhlIHNhbXBsZSBub25jZQ==,服務端將把258EAFA5-E914-47DA-95CA-C5AB0DC85B11 這個惟一的GUID與它拼接起來,就是dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CAC5AB0DC85B11,而後對其進行SHA-1哈希,結果爲0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea,再進行base64-encoded便可得s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

成功的WebSocket 握手必須是客戶端發送協議版本和自動生成的挑戰值,服務器返回101 HTTP 響應碼(Switching Protocols)和散列形式的挑戰答案,確認選擇的協議版本。
一旦客戶端打開握手發送出去,在發送任何數據以前,客戶端必須等待服務器的響應。客戶端必須按以下步驟驗證響應:

  1. 若是從服務器接收到的狀態碼不是101,按HTTP【RFC2616】程序處理響應。在特殊狀況下,若是客戶端接收到401狀態碼,可能執行認證;服務器可能用3xx狀態碼重定向客戶端(但不要求客戶端遵循他們)。不然按下面處理。
  2. 若是響應缺失Upgrade頭域或Upgrade頭域的值沒有包含大小寫不敏感的ASCII 值"websocket",客戶端必須使WebSocket鏈接失敗。
  3. 若是響應缺失Connection頭域或其值不包含大小寫不敏感的ASCII值"Upgrade",客戶端必須使WebSocket鏈接失敗。
  4. 若是響應缺失Sec-WebSocket-Accept頭域或其值不包含 [Sec-WebSocket-Key] (做爲字符串,非base64解碼的)+ "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 的base64編碼 SHA-1值,客戶端必須使WebSocket鏈接失敗。
  5. 若是響應包含Sec-WebSocket-Extensions頭域,且其值指示使用的擴展不出如今客戶端發送的握手(服務器指示的擴展不是客戶端要求的),客戶端必須使WebSocket鏈接失敗。
  6. 若是響應包含Sec-WebSocket-Protocol頭域,且這個頭域指示使用的子協議不包含在客戶端的握手(服務器指示的子協議不是客戶端要求的),客戶端必須使WebSocket鏈接失敗。

若是客戶端完成了對服務端響應的升級協商驗證,該鏈接就能夠用做雙向通訊信道交換WebSocket 消息。今後之後,客戶端與服務器之間不會再發生HTTP 通訊,一切由WebSocket 協議接管。

3.使用場景及性能

Websocket協議具備極簡的API,開發者能夠很簡便的調用,並且提供了二進制分幀、可擴展性以及子協議協商等強大特性,使得WebSocket 成爲在瀏覽器中採用自定義應用協議的最佳選擇。但,在計算機世界裏,任何技術和理論通常都是爲解決特定問題而生的,並非普世化的解決方案,WebSocket亦是如此。WebSocket 不能取代XHR 或SSE,什麼時候以及如何使用,毋庸置疑會對性能產生巨大影響,要得到最佳性能,咱們必須善於利用它的長處!下面將對現有的一些協議與WebSocket 對比進行一個大體介紹。

請求和響應流

XHR 是專門爲「事務型」請求/ 響應通訊而優化的:客戶端向服務器發送完整的、格式良好的HTTP 請求,服務器返回完整的響應。這裏不支持請求流,在Streams API 可用以前,沒有可靠的跨瀏覽器響應流API。 SSE 能夠實現服務器到客戶端的高效、低延遲的文本數據流:客戶端發起 SSE 鏈接,服務器使用事件源協議將更新流式發送給客戶端。客戶端在初次握手後,不能向服務器發送任何數據。 WebSocket 是惟一一個能經過同一個TCP 鏈接實現雙向通訊的機制,客戶端和服務器隨時能夠交換數據。所以,WebSocket 在兩個方向上都能保證文本和二進制應用數據的低延遲交付。客戶端到服務端傳遞消息的總時延由如下四個部分構成:

  • 傳播延遲:消息從發送端到接收端須要的時間,是信號傳播距離和速度的函數,傳播時間取決於距離和信號經過的媒介,播速度一般不超過光速;
  • 傳輸延遲:把消息中的全部比特轉移到鏈路中須要的時間,是消息長度和鏈路速率的函數,由傳輸鏈路的速率決
    定,與客戶端到服務器的距離無關;
  • 處理延遲:處理分組首部、檢查位錯誤及肯定分組目標所需的時間,常由硬件完成,所以相應的延遲通常很是短;
  • 排隊延遲:若是分組到達的速度超過了路由器的處理能力,那麼分組就要在入站緩衝區排隊,到來的分組排隊等待處理的時間就是排隊延遲。

不管是什麼樣的傳輸機制,都不會減小客戶端與服務器間的往返次數,數據包的傳播延遲都同樣。但,採用不一樣的傳輸機制能夠有不一樣的排隊延遲。對XHR 輪詢而言,排隊延遲就是客戶端輪詢間隔:服務器上的消息可用以後,必須等到下一次客戶端XHR 請求才能發送。相對來講,SSE 和WebSocket 使用持久鏈接,這樣服務器(和客戶端——若是是WebSocket)就能夠在消息可用時當即發送它,消除了消息的排隊延遲,也就使得總的傳輸延遲更小。

消息開銷

在完成最初的升級協商以後,客戶端和服務器便可經過WebSocket 協議雙向交換數據,消息分幀以後每幀會添加2~14 字節的開銷;SSE 會給每一個 消息添加 5 字節,但僅限於 UTF-8 內容(SSE 不是爲傳輸二進制載荷而設計的!若是有必要,能夠把二進制對象編碼爲base64 形式,而後再使用SSE); HTTP 1.x 請求(XHR 及其餘常規請求)會攜帶 500~800 字節的 HTTP 元數據,加上cookie; HTTP 2.0 壓縮 HTTP 元數據,能夠顯著減小開銷,若是請求都不修改首部,那麼開銷能夠低至8 字節。WebSocket專門爲雙向通訊而設計,開銷很小,在實時通知應用開發中是不錯的選擇。

上述開銷不包括IP、TCP 和TLS 分幀的開銷,後者一共會給每一個消息增長60~100 字節,不管使用的是什麼應用協議。

效率及壓縮

在使用HTTP協議傳輸數據時,每一個請求均可以協商最優的傳輸編碼格式(如對文本數據採用gzip 壓縮);SSE 只能傳輸UTF-8 格式數據,事件流數據能夠在整個會話期間使用gzip 壓縮;WebSocket 能夠傳輸文本和二進制數據,壓縮整個會話行不通,二進制的淨荷也可能已經壓縮過了!
鑑於WebSocket的特殊性,它須要實現本身的壓縮機制,並針對每一個消息選擇應用。HyBi 工做組正在爲WebSocket 協議制定以消息爲單位的壓縮擴展,但這個擴展還沒有獲得任何瀏覽器支持。目前來講,除非應用經過細緻優化本身的二進制淨荷實現本身的壓縮邏輯,同時也針對文本消息實現本身的壓縮邏輯,不然傳輸數據過程當中必定會產生很大的字節開銷!

自定義應用協議

HTTP已經誕生了數十年,具備普遍的應用,各類優化專門的優化機制也已經被瀏覽器及服務器等設備實施,XHR 請求天然而然就繼承了全部這些功能。然而,對於只使用HTTP協議完成升級協商的WebSocket來講,流式數據處理可讓咱們在客戶端和服務器間自定義協議,但也會錯過瀏覽器提供的不少服務,應用可能必須實現自已的邏輯來填充某些功能空白,好比緩存、狀態管理、元數據交付等等。

部署WebSocket

HTTP 是專爲短時突發性傳輸設計的,不少服務器、代理和其餘中間設備的HTTP 鏈接空閒超時設置都很激進。這就與WebSocket的長時鏈接、實時雙向通訊相悖,部署時須要關注下面的三個方面:

  • 位於各自網絡中的路由器、負載均衡器和代理;
  • 外部網絡中透明、肯定的代理服務器(如 ISP 和運營商的代理);
  • 客戶網絡中的路由器、防火牆和代理。

鑑於用戶所處的網絡環境是各不相同的,不受開發者所控制。某些網絡甚至會徹底屏蔽WebSocket通訊,有些設備也不支持WebSocket協議,這時就須要採用備用機制,使用其餘技術來實現相似與WebSocket的通訊(如socket.io等)。雖然,咱們沒法處理網絡中的中間設備,但對於處在咱們本身掌控下的基礎設施仍是能夠作一些工做的,能夠對通訊路徑上的每一臺負載均衡器、路由器和Web 服務器針對長時鏈接進行調優。然而,長時鏈接和空閒會話會佔用全部中間設備及服務器的內存和套接字資源,開銷很大,部署WebSocket、SSE及HTTP 2.0等賴於長時會話的協議都會對運維提出新的挑戰。在使用WebSocket的過程當中,也須要作到優化二進制淨荷和壓縮 UTF-8 內容以最小化傳輸數據、監控客戶端緩衝數據的量、切分應用消息避免隊首阻塞、合用的狀況下利用其餘傳輸機制等。

總結

WebSocket 協議爲實時雙向通訊而設計,提供高效、靈活的文本和二進制數據傳輸,同時也錯過了瀏覽器爲HTTP提供的一些服務,在使用時須要應用本身實現。在進行應用數據傳輸時,須要根據不一樣的場景選擇恰當的協議,WebSocket 並不能取代HTTP、XHR 或SSE,關鍵仍是要利用這些機制的長處以求得最佳性能。

Socket.IO

鑑於如今不一樣的平臺及瀏覽器版本對WebSocket支持的不一樣,有開發者作了一個叫作socket.io 的爲實時應用提供跨平臺實時通訊的庫,咱們可使用它完成向WebSocket的切換。socket.io 旨在使實時應用在每一個瀏覽器和移動設備上成爲可能,模糊不一樣的傳輸機制之間的差別。socket.io 的名字源於它使用了瀏覽器支持並採用的 HTML5 WebSocket 標準,由於並非全部的瀏覽器都支持 WebSocket ,因此該庫支持一系列降級功能:

  • Websocket
  • Adobe:registered: Flash:registered: Socket
  • AJAX long polling
  • AJAX multipart streaming
  • Forever Iframe
  • JSONP Polling

在大部分情境下,你都能經過這些功能選擇與瀏覽器保持相似長鏈接的功能。具體細節請看Socket.io

參考資料

相關文章
相關標籤/搜索