WebSocket 淺析
本文來自於騰訊Bugly公眾號(weixinBugly),未經作者同意,請勿轉載,原文地址:WebSocket 淺析
前言
在WebSocket API尚未被眾多瀏覽器實現和發布的時期,開發者在開發需要接收來自伺服器的實時通知應用程序時,不得不求助於一些「hacks」來模擬實時連接以實現實時通信,最流行的一種方式是長輪詢?。?長輪詢主要是發出一個HTTP請求到伺服器,然後保持連接打開以允許伺服器在稍後的時間響應(由伺服器確定)。為了這個連接有效地工作,許多技術需要被用於確保消息不錯過,如需要在伺服器端緩存和記錄多個的連接信息(每個客戶)。雖然長輪詢是可以解決這一問題的,但它會耗費更多的資源,如CPU、內存和帶寬等,要想很好的解決實時通信問題就需要設計和發布一種新的協議。
WebSocket 是伴隨HTML5發布的一種新協議。它實現了瀏覽器與伺服器全雙工通信(full-duplex),可以傳輸基於消息的文本和二進位數據。WebSocket 是瀏覽器中最靠近套接字的API,除最初建立連接時需要藉助於現有的HTTP協議,其他時候直接基於TCP完成通信。它是瀏覽器中最通用、最靈活的一個傳輸機制,其極簡的API 可以讓我們在客戶端和伺服器之間以數據流的形式實現各種應用數據交換(包括JSON 及自定義的二進位消息格式),而且兩端都可以隨時向另一端發送數據。在這個簡單的API 之後隱藏了很多的複雜性,而且還提供了更多服務,如:
- 連接協商和同源策略;
- 與既有 HTTP 基礎設施的互操作;
- 基於消息的通信和高效消息分幀;
- 子協議協商及可擴展能力。
所幸,瀏覽器替我們完成了上述工作,我們只需要簡單的調用即可。任何事物都不是完美的,設計限制和性能權衡始終會有,利用WebSocket 也不例外,在提供自定義數據交換協議同時,也不再享有在一些本由瀏覽器提供的服務和優化,如狀態管理、壓縮、緩存等。
隨著HTML5的發布,越來越多的瀏覽器開始支持WebSocket,如果你的應用還在使用長輪詢,那就可以考慮切換了。下面的圖表顯示了在一種常見的使用案例下,WebSocket和長輪詢之間的帶寬消耗差異:
1.WebSocket API
WebSocket 對象提供了一組 API,用於創建和管理 WebSocket 連接,以及通過連接發送和接收數據。瀏覽器提供的WebSocket API很簡潔,調用示例如下:
var ws = new WebSocket(wss://example.com/socket); // 創建安全WebSocket 連接(wss)nn ws.onerror = function (error) { ... } // 錯誤處理n ws.onclose = function () { ... } // 關閉時調用nn ws.onopen = function () { // 連接建立時調用n ws.send("Connection established. Hello server!"); // 向服務端發送消息n }nn ws.onmessage = function(msg) { // 接收服務端發送的消息n if(msg.data instanceof Blob) { // 處理二進位信息n processBlob(msg.data);n } else {n processText(msg.data); // 處理文本信息n }n }n
1.1.接收和發送數據
WebSocket提供了極簡的API,開發者可以輕鬆的調用,瀏覽器會為我們完成緩衝、解析、重建接收到的數據等工作。應用只需監聽onmessage事件,用回調處理返回數據即可。 WebSocket支持文本和二進位數據傳輸,瀏覽器如果接收到文本數據,會將其轉換為DOMString 對象,如果是二進位數據或Blob 對象,可直接將其轉交給應用或將其轉化為ArrayBuffer,由應用對其進行進一步處理。從內部看,協議只關注消息的兩個信息:凈荷長度和類型(前者是一個可變長度欄位),據以區別UTF-8 數據和二進位數據。示例如下:
var wss = new WebSocket(wss://example.com/socket);n ws.binaryType = "arraybuffer"; nn // 接收數據n wss.onmessage = function(msg) {n if(msg.data instanceof ArrayBuffer) {n processArrayBuffer(msg.data);n } else {n processText(msg.data);n }n }nn // 發送數據n ws.onopen = function () {n socket.send("Hello server!"); n socket.send(JSON.stringify({msg: payload}));nn var buffer = new ArrayBuffer(128);n socket.send(buffer);nn var intview = new Uint32Array(buffer);n socket.send(intview);nn var blob = new Blob([buffer]);n socket.send(blob); n }n
Blob 對象是包含有隻讀原始數據的類文件對象,可存儲二進位數據,它會被寫入磁碟;ArrayBuffer (緩衝數組)是一種用於呈現通用、固定長度的二進位數據的類型,作為內存區域可以存放多種類型的數據。
對於將要傳輸的二進位數據,開發者可以決定以何種方式處理,可以更好的處理數據流,Blob 對象一般用來表示一個不可變文件對象或原始數據,如果你不需要修改它或者不需要把它切分成更小的塊,那這種格式是理想的;如果你還需要再處理接收到的二進位數據,那麼選擇ArrayBuffer 應該更合適。
WebSocket 提供的信道是全雙工的,在同一個TCP 連接上,可以雙向傳輸文本信息和二進位數據,通過數據幀中的一位(bit)來區分二進位或者文本。WebSocket 只提供了最基礎的文本和二進位數據傳輸功能,如果需要傳輸其他類型的數據,就需要通過額外的機制進行協商。WebSocket 中的send( ) 方法是非同步的:提供的數據會在客戶端排隊,而函數則立即返回。在傳輸大文件時,不要因為回調已經執行,就錯誤地以為數據已經發送出去了,數據很可能還在排隊。要監控在瀏覽器中排隊的數據量,可以查詢套接字的bufferedAmount 屬性:
var ws = new WebSocket(wss://example.com/socket);nn ws.onopen = function () {n subscribeToApplicationUpdates(function(evt) { n if (ws.bufferedAmount == 0) n ws.send(evt.data); n });n };n
前面的例子是向伺服器發送應用數據,所有WebSocket 消息都會按照它們在客戶端排隊的次序逐個發送。因此,大量排隊的消息,甚至一個大消息,都可能導致排在它後面的消息延遲——隊首阻塞!為解決這個問題,應用可以將大消息切分成小塊,通過監控bufferedAmount 的值來避免隊首阻塞。甚至還可以實現自己的優先隊列,而不是盲目都把它們送到套接字上排隊。要實現最優化傳輸,應用必須關心任意時刻在套接字上排隊的是什麼消息!
1.2.子協議協商
在以往使用HTTP 或XHR 協議來傳輸數據時,它們可以通過每次請求和響應的HTTP 首部來溝通元數據,以進一步確定傳輸的數據格式,而WebSocket 並沒有提供等價的機制。上文已經提到WebSocket只提供最基礎的文本和二進位數據傳輸,對消息的具體內容格式是未知的。因此,如果WebSocket需要溝通關於消息的元數據,客戶端和伺服器必須達成溝通這一數據的子協議,進而間接地實現其他格式數據的傳輸。下面是一些可能策略的介紹:
客戶端和伺服器可以提前確定一種固定的消息格式,比如所有通信都通過 JSON編碼的消息或者某種自定義的二進位格式進行,而必要的元數據作為這種數據結構的一個部分;
如果客戶端和伺服器要發送不同的數據類型,那它們可以確定一個雙方都知道的消息首部,利用它來溝通說明信息或有關凈荷的其他解碼信息;
混合使用文本和二進位消息可以溝通凈荷和元數據,比如用文本消息實現 HTTP首部的功能,後跟包含應用凈荷的二進位消息。
上面介紹了一些可能的策略來實現其他格式數據的傳輸,確定了消息的串列格式化,但怎麼確保客戶端和服務端是按照約定發送和處理數據,這個約定客戶端和服務端是如何協商的呢?這就需要WebSocket 提供一個機制來協商,這時WebSocket構造器方法的第二個可選參數就派上用場了,通過這個參數客戶端和服務端就可以根據約定好的方式處理髮送及接收到的數據。
WebSocket構造器方法如下所示:
WebSocket WebSocket(n in DOMString url, // 表示要連接的URL。這個URL應該為響應WebSocket的地址。n in optional DOMString protocols // 可以是一個單個的協議名字字元串或者包含多個協議名字字元串的數組。默認設為一個空字元串。n );n
通過上述WebSocket構造器方法的第二個參數,客戶端可以在初次連接握手時,可以告知伺服器自己支持哪種協議。如下所示:
var ws = new WebSocket(wss://example.com/socket,[appProtocol, appProtocol-v2]);nn ws.onopen = function () {n if (ws.protocol == appProtocol-v2) { n ...n } else {n ...n }n }n
如上所示,WebSocket 構造函數接受了一個可選的子協議名字的數組,通過這個數組,客戶端可以向伺服器通告自己能夠理解或希望伺服器接受的協議。當伺服器接收到該請求後,會根據自身的支持情況,返回相應信息。
有支持的協議,則子協議協商成功,觸發客戶端的onopen回調,應用可以查詢WebSocket 對象上的protocol 屬性,從而得知伺服器選定的協議;
沒有支持的協議,則協商失敗,觸發onerror 回調,連接斷開。
http://1.3.WS與WSS
WebSocket 資源URI採用了自定義模式:ws 表示純文本通信( 如ws://http://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 ]n wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]n
很多現有的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,保留了一些特殊位和操作碼供後期擴展。在打開握手完成後,終端發送一個關閉幀之前的任何時間裡,數據幀可能由客戶端或伺服器的任何一方發送。具體的幀格式如下所示:
- 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規定的分幀規則如下:
一個未分幀的消息包含單個幀,FIN設置為1,opcode非0。
一個分幀了的消息包含:開始於:單個幀,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。
控制幀可能被插入到分幀了消息中,控制幀必須不能被分幀。如果控制幀不能插入,例如,如果是在一個大消息後面,ping的延遲將會很長。因此要求處理消息幀中間的控制幀。
消息的幀必須以發送者發送的順序傳遞給接受者。
一個消息的幀必須不能交叉在其他幀的消息中,除非有擴展能夠解釋交叉。
一個終端必須能夠處理消息幀中間的控制幀。
一個發送者可能對任意大小的非控制消息分幀。
客戶端和伺服器必須支持接收分幀和未分幀的消息。
由於控制幀不能分幀,中間設施必須不嘗試改變控制幀。
中間設施必須不修改消息的幀,如果保留位的值已經被使用,且中間設施不明白這些值的含義。
在遵循了上述分幀規則之後,一個消息的所有幀屬於同樣的類型,由第一個幀的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.1n Host: thirdparty.comn Origin: Example Domainn Connection: Upgrade n Upgrade: websocket // 請求升級到WebSocket 協議n Sec-WebSocket-Version: 13 // 客戶端使用的WebSocket 協議版本n Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 自動生成的鍵,以驗證伺服器對協議的支持,其值必須是nonce組成的隨機選擇的16位元組的被base64編碼後的值n Sec-WebSocket-Protocol: appProtocol, appProtocol-v2 // 可選的應用指定的子協議列表n Sec-WebSocket-Extensions: x-webkit-deflate-message, x-custom-extension // 可選的客戶端支持的協議擴展列表,指示了客戶端希望使用的協議級別的擴展n
在安全工程中,Nonce是一個在加密通信只能使用一次的數字。在認證協議中,它往往是一個隨機或偽隨機數,以避免重放攻擊。Nonce也用於流密碼以確保安全。如果需要使用相同的密鑰加密一個以上的消息,就需要Nonce來確保不同的消息與該密鑰加密的密鑰流不同。
與瀏覽器中客戶端發起的任何連接一樣,WebSocket 請求也必須遵守同源策略:瀏覽器會自動在升級握手請求中追加Origin 首部,遠程伺服器可能使用CORS 判斷接受或拒絕跨源請求。要完成握手,伺服器必須返回一個成功的「Switching Protocols」(切換協議)響應,具體如下:
HTTP/1.1 101 Switching Protocols // 101 響應碼確認升級到WebSocket 協議n Upgrade: websocketn Connection: Upgraden Access-Control-Allow-Origin: Example Domain // CORS 首部表示選擇同意跨源連接n Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 簽名的鍵值驗證協議支持n Sec-WebSocket-Protocol: appProtocol-v2 // 伺服器選擇的應用子協議n Sec-WebSocket-Extensions: x-custom-extension // 伺服器選擇的WebSocket 擴展n
所有兼容RFC 6455 的WebSocket 伺服器都使用相同的演算法計算客戶端挑戰的答案:將Sec-WebSocket-Key 的內容與標準定義的唯一GUID 字元串拼接起來,計算出SHA1 散列值,結果是一個base-64 編碼的字元串,把這個字元串發給客戶端即可。Sec-WebSocket-Accept 這個頭域的 ABNF [RFC2616]定義如下:
Sec-WebSocket-Accept = base64-value-non-emptyn base64-value-non-empty = (1*base64-data [ base64-padding ]) |n base64-paddingn base64-data = 4base64-charactern base64-padding = (2base64-character "==") | n (3base64-character "=")n base64-character = ALPHA | DIGIT | "+" | "/"n
如果客戶端發送的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)和散列形式的挑戰答案,確認選擇的協議版本。
一旦客戶端打開握手發送出去,在發送任何數據之前,客戶端必須等待伺服器的響應。客戶端必須按如下步驟驗證響應:
如果從伺服器接收到的狀態碼不是101,按HTTP【RFC2616】程序處理響應。在特殊情況下,如果客戶端接收到401狀態碼,可能執行認證;伺服器可能用3xx狀態碼重定向客戶端(但不要求客戶端遵循他們)。否則按下面處理。
如果響應缺失Upgrade頭域或Upgrade頭域的值沒有包含大小寫不敏感的ASCII 值」websocket」,客戶端必須使WebSocket連接失敗。
如果響應缺失Connection頭域或其值不包含大小寫不敏感的ASCII值」Upgrade」,客戶端必須使WebSocket連接失敗。
如果響應缺失Sec-WebSocket-Accept頭域或其值不包含 [Sec-WebSocket-Key] (作為字元串,非base64解碼的)+ 「258EAFA5-E914-47DA-95CA-C5AB0DC85B11」 的base64編碼 SHA-1值,客戶端必須使WebSocket連接失敗。
如果響應包含Sec-WebSocket-Extensions頭域,且其值指示使用的擴展不出現在客戶端發送的握手(伺服器指示的擴展不是客戶端要求的),客戶端必須使WebSocket連接失敗。
如果響應包含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的通信(如http://socket.io等)。雖然,我們無法處理網路中的中間設備,但對於處在我們自己掌控下的基礎設施還是可以做一些工作的,可以對通信路徑上的每一台負載均衡器、路由器和Web 伺服器針對長時連接進行調優。然而,長時連接和空閑會話會佔用所有中間設備及伺服器的內存和套接字資源,開銷很大,部署WebSocket、SSE及HTTP 2.0等賴於長時會話的協議都會對運維提出新的挑戰。在使用WebSocket的過程中,也需要做到優化二進位凈荷和壓縮 UTF-8 內容以最小化傳輸數據、監控客戶端緩衝數據的量、切分應用消息避免隊首阻塞、合用的情況下利用其他傳輸機制等。
總結
WebSocket 協議為實時雙向通信而設計,提供高效、靈活的文本和二進位數據傳輸,同時也錯過了瀏覽器為HTTP提供的一些服務,在使用時需要應用自己實現。在進行應用數據傳輸時,需要根據不同的場景選擇恰當的協議,WebSocket 並不能取代HTTP、XHR 或SSE,關鍵還是要利用這些機制的長處以求得最佳性能。
http://Socket.IO
鑒於現在不同的平台及瀏覽器版本對WebSocket支持的不同,有開發者做了一個叫做http://socket.io 的為實時應用提供跨平台實時通信的庫,我們可以使用它完成向WebSocket的切換。http://socket.io 旨在使實時應用在每個瀏覽器和移動設備上成為可能,模糊不同的傳輸機制之間的差異。http://socket.io 的名字源於它使用了瀏覽器支持並採用的 HTML5 WebSocket 標準,因為並不是所有的瀏覽器都支持 WebSocket ,所以該庫支持一系列降級功能:
- Websocket
- Adobe:registered: Flash:registered: Socket
- AJAX long polling
- AJAX multipart streaming
- Forever Iframe
- JSONP Polling
在大部分情境下,你都能通過這些功能選擇與瀏覽器保持類似長連接的功能。具體細節請看http://socket.io/docs/。
參考資料
- High Performance Browser Networking
- RFC 6455 - The WebSocket Protocol
- http://www.fzb.me/2015-3-24-websocket-protocol-abstract.html
- https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
- http://www.ietf.org/rfc/rfc5234.txt
更多精彩內容歡迎關注騰訊 Bugly的微信公眾賬號:
騰訊 Bugly是一款專為移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合併功能幫助開發同學把每天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在發布後快速的了解應用的質量情況,適配最新的 iOS, Android 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!
推薦閱讀:
※SSL協議之數據加密過程詳解
※談談 HTTPS
※Windows,Mac OSX和Linux平台設置HTTPS以及自簽名證書詳細指南
※JMeter-HTTP請求sampler詳細說明
※認識HTTP----Cookie和Session篇