版權(quán)聲明:本文由史燕飛原創(chuàng)文章,轉(zhuǎn)載請注明出處:
文章原文鏈接:https://www.qcloud.com/community/article/241
來源:騰云閣 https://www.qcloud.com/community
作者介紹:史燕飛(英文名:Jeri),16年畢業(yè)于武漢大學(xué)并加入騰訊。目前在騰訊云從事前端開發(fā)工作,喜歡研究前端相關(guān)技術(shù)(如:計算機網(wǎng)絡(luò)、WebKit內(nèi)核、React等),也喜歡關(guān)注數(shù)據(jù)挖掘及機器學(xué)習(xí)等前沿科技。
在WebSocket API尚未被眾多瀏覽器實現(xiàn)和發(fā)布的時期,開發(fā)者在開發(fā)需要接收來自服務(wù)器的實時通知應(yīng)用程序時,不得不求助于一些“hacks”來模擬實時連接以實現(xiàn)實時通信,最流行的一種方式是長輪詢?。?長輪詢主要是發(fā)出一個HTTP請求到服務(wù)器,然后保持連接打開以允許服務(wù)器在稍后的時間響應(yīng)(由服務(wù)器確定)。為了這個連接有效地工作,許多技術(shù)需要被用于確保消息不錯過,如需要在服務(wù)器端緩存和記錄多個的連接信息(每個客戶)。雖然長輪詢是可以解決這一問題的,但它會耗費更多的資源,如CPU、內(nèi)存和帶寬等,要想很好的解決實時通信問題就需要設(shè)計和發(fā)布一種新的協(xié)議。
WebSocket 是伴隨HTML5發(fā)布的一種新協(xié)議。它實現(xiàn)了瀏覽器與服務(wù)器全雙工通信(full-duplex),可以傳輸基于消息的文本和二進制數(shù)據(jù)。WebSocket 是瀏覽器中最靠近套接字的API,除最初建立連接時需要借助于現(xiàn)有的HTTP協(xié)議,其他時候直接基于TCP完成通信。它是瀏覽器中最通用、最靈活的一個傳輸機制,其極簡的API 可以讓我們在客戶端和服務(wù)器之間以數(shù)據(jù)流的形式實現(xiàn)各種應(yīng)用數(shù)據(jù)交換(包括JSON 及自定義的二進制消息格式),而且兩端都可以隨時向另一端發(fā)送數(shù)據(jù)。在這個簡單的API 之后隱藏了很多的復(fù)雜性,而且還提供了更多服務(wù),如:
所幸,瀏覽器替我們完成了上述工作,我們只需要簡單的調(diào)用即可。任何事物都不是完美的,設(shè)計限制和性能權(quán)衡始終會有,利用WebSocket 也不例外,在提供自定義數(shù)據(jù)交換協(xié)議同時,也不再享有在一些本由瀏覽器提供的服務(wù)和優(yōu)化,如狀態(tài)管理、壓縮、緩存等。
隨著HTML5的發(fā)布,越來越多的瀏覽器開始支持WebSocket,如果你的應(yīng)用還在使用長輪詢,那就可以考慮切換了。下面的圖表顯示了在一種常見的使用案例下,WebSocket和長輪詢之間的帶寬消耗差異:
WebSocket 對象提供了一組 API,用于創(chuàng)建和管理 WebSocket 連接,以及通過連接發(fā)送和接收數(shù)據(jù)。瀏覽器提供的WebSocket API很簡潔,調(diào)用示例如下:
var ws = new WebSocket('wss://example.com/socket'); // 創(chuàng)建安全WebSocket 連接(wss)ws.onerror = function (error) { ... } // 錯誤處理ws.onclose = function () { ... } // 關(guān)閉時調(diào)用ws.onopen = function () { // 連接建立時調(diào)用ws.send("Connection established. Hello server!"); // 向服務(wù)端發(fā)送消息}ws.onmessage = function(msg) { // 接收服務(wù)端發(fā)送的消息if(msg.data instanceof Blob) { // 處理二進制信息processBlob(msg.data);} else {processText(msg.data); // 處理文本信息}}
WebSocket提供了極簡的API,開發(fā)者可以輕松的調(diào)用,瀏覽器會為我們完成緩沖、解析、重建接收到的數(shù)據(jù)等工作。應(yīng)用只需監(jiān)聽onmessage事件,用回調(diào)處理返回數(shù)據(jù)即可。 WebSocket支持文本和二進制數(shù)據(jù)傳輸,瀏覽器如果接收到文本數(shù)據(jù),會將其轉(zhuǎn)換為DOMString 對象,如果是二進制數(shù)據(jù)或Blob 對象,可直接將其轉(zhuǎn)交給應(yīng)用或?qū)⑵滢D(zhuǎn)化為ArrayBuffer,由應(yīng)用對其進行進一步處理。從內(nèi)部看,協(xié)議只關(guān)注消息的兩個信息:凈荷長度和類型(前者是一個可變長度字段),據(jù)以區(qū)別UTF-8 數(shù)據(jù)和二進制數(shù)據(jù)。示例如下:
var wss = new WebSocket('wss://example.com/socket');ws.binaryType = "arraybuffer"; // 接收數(shù)據(jù)wss.onmessage = function(msg) {if(msg.data instanceof ArrayBuffer) {processArrayBuffer(msg.data);} else {processText(msg.data);}}// 發(fā)送數(shù)據(jù)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 對象是包含有只讀原始數(shù)據(jù)的類文件對象,可存儲二進制數(shù)據(jù),它會被寫入磁盤;ArrayBuffer (緩沖數(shù)組)是一種用于呈現(xiàn)通用、固定長度的二進制數(shù)據(jù)的類型,作為內(nèi)存區(qū)域可以存放多種類型的數(shù)據(jù)。
對于將要傳輸?shù)亩M制數(shù)據(jù),開發(fā)者可以決定以何種方式處理,可以更好的處理數(shù)據(jù)流,Blob 對象一般用來表示一個不可變文件對象或原始數(shù)據(jù),如果你不需要修改它或者不需要把它切分成更小的塊,那這種格式是理想的;如果你還需要再處理接收到的二進制數(shù)據(jù),那么選擇ArrayBuffer 應(yīng)該更合適。
WebSocket 提供的信道是全雙工的,在同一個TCP 連接上,可以雙向傳輸文本信息和二進制數(shù)據(jù),通過數(shù)據(jù)幀中的一位(bit)來區(qū)分二進制或者文本。WebSocket 只提供了最基礎(chǔ)的文本和二進制數(shù)據(jù)傳輸功能,如果需要傳輸其他類型的數(shù)據(jù),就需要通過額外的機制進行協(xié)商。WebSocket 中的send( ) 方法是異步的:提供的數(shù)據(jù)會在客戶端排隊,而函數(shù)則立即返回。在傳輸大文件時,不要因為回調(diào)已經(jīng)執(zhí)行,就錯誤地以為數(shù)據(jù)已經(jīng)發(fā)送出去了,數(shù)據(jù)很可能還在排隊。要監(jiān)控在瀏覽器中排隊的數(shù)據(jù)量,可以查詢套接字的bufferedAmount 屬性:
var ws = new WebSocket('wss://example.com/socket');ws.onopen = function () {subscribeToApplicationUpdates(function(evt) { if (ws.bufferedAmount == 0) ws.send(evt.data); });};
前面的例子是向服務(wù)器發(fā)送應(yīng)用數(shù)據(jù),所有WebSocket 消息都會按照它們在客戶端排隊的次序逐個發(fā)送。因此,大量排隊的消息,甚至一個大消息,都可能導(dǎo)致排在它后面的消息延遲——隊首阻塞!為解決這個問題,應(yīng)用可以將大消息切分成小塊,通過監(jiān)控bufferedAmount 的值來避免隊首阻塞。甚至還可以實現(xiàn)自己的優(yōu)先隊列,而不是盲目都把它們送到套接字上排隊。要實現(xiàn)最優(yōu)化傳輸,應(yīng)用必須關(guān)心任意時刻在套接字上排隊的是什么消息!
在以往使用HTTP 或XHR 協(xié)議來傳輸數(shù)據(jù)時,它們可以通過每次請求和響應(yīng)的HTTP 首部來溝通元數(shù)據(jù),以進一步確定傳輸?shù)臄?shù)據(jù)格式,而WebSocket 并沒有提供等價的機制。上文已經(jīng)提到WebSocket只提供最基礎(chǔ)的文本和二進制數(shù)據(jù)傳輸,對消息的具體內(nèi)容格式是未知的。因此,如果WebSocket需要溝通關(guān)于消息的元數(shù)據(jù),客戶端和服務(wù)器必須達成溝通這一數(shù)據(jù)的子協(xié)議,進而間接地實現(xiàn)其他格式數(shù)據(jù)的傳輸。下面是一些可能策略的介紹:
上面介紹了一些可能的策略來實現(xiàn)其他格式數(shù)據(jù)的傳輸,確定了消息的串行格式化,但怎么確保客戶端和服務(wù)端是按照約定發(fā)送和處理數(shù)據(jù),這個約定客戶端和服務(wù)端是如何協(xié)商的呢?這就需要WebSocket 提供一個機制來協(xié)商,這時WebSocket構(gòu)造器方法的第二個可選參數(shù)就派上用場了,通過這個參數(shù)客戶端和服務(wù)端就可以根據(jù)約定好的方式處理發(fā)送及接收到的數(shù)據(jù)。
WebSocket構(gòu)造器方法如下所示:
WebSocket WebSocket(in DOMString url, // 表示要連接的URL。這個URL應(yīng)該為響應(yīng)WebSocket的地址。in optional DOMString protocols // 可以是一個單個的協(xié)議名字字符串或者包含多個協(xié)議名字字符串的數(shù)組。默認設(shè)為一個空字符串。);
通過上述WebSocket構(gòu)造器方法的第二個參數(shù),客戶端可以在初次連接握手時,可以告知服務(wù)器自己支持哪種協(xié)議。如下所示:
var ws = new WebSocket('wss://example.com/socket',['appProtocol', 'appProtocol-v2']);ws.onopen = function () {if (ws.protocol == 'appProtocol-v2') { ...} else {...}}
如上所示,WebSocket 構(gòu)造函數(shù)接受了一個可選的子協(xié)議名字的數(shù)組,通過這個數(shù)組,客戶端可以向服務(wù)器通告自己能夠理解或希望服務(wù)器接受的協(xié)議。當(dāng)服務(wù)器接收到該請求后,會根據(jù)自身的支持情況,返回相應(yīng)信息。
WebSocket 資源URI采用了自定義模式:ws 表示純文本通信( 如ws://example.com/socket),wss 表示使用加密信道通信(TCP+TLS)。為什么不使用http而要自定義呢?
WebSocket 的主要目的,是在瀏覽器中的應(yīng)用與服務(wù)器之間提供優(yōu)化的、雙向通信機制??墒牵琖ebSocket 的連接協(xié)議也可以用于瀏覽器之外的場景,可以通過非HTTP協(xié)商機制交換數(shù)據(jù)??紤]到這一點,HyBi Working Group 就選擇采用了自定義的URI模式:
各自的URI如下:
ws-URI = "ws:" "http://" host [ ":" port ] path [ "?" query ]wss-URI = "wss:" "http://" host [ ":" port ] path [ "?" query ]
很多現(xiàn)有的HTTP 中間設(shè)備可能不理解新的WebSocket 協(xié)議,而這可能導(dǎo)致各種問題:盲目的連接升級、意外緩沖WebSocket 幀、不明就里地修改內(nèi)容、把WebSocket 流量誤當(dāng)作不完整的HTTP 通信,等等。這時WSS就提供了一種不錯的解決方案,它建立一條端到端的安全通道,這個端到端的加密隧道對中間設(shè)備模糊了數(shù)據(jù),因此中間設(shè)備就不能再感知到數(shù)據(jù)內(nèi)容,也就無法再對請求做特殊處理。
HyBi Working Group 制定的WebSocket 通信協(xié)議(RFC 6455)包含兩個高層組件:開放性HTTP 握手用于協(xié)商連接參數(shù),二進制消息分幀機制用于支持低開銷的基于消息的文本和二進制數(shù)據(jù)傳輸。WebSocket 協(xié)議嘗試在既有HTTP 基礎(chǔ)設(shè)施中實現(xiàn)雙向HTTP 通信,因此也使用HTTP 的80 和443 端口。不過,這個設(shè)計不限于通過HTTP 實現(xiàn)WebSocket 通信,未來的實現(xiàn)可以在某個專用端口上使用更簡單的握手,而不必重新定義一個協(xié)議。WebSocket 協(xié)議是一個獨立完善的協(xié)議,可以在瀏覽器之外實現(xiàn)。不過,它的主要應(yīng)用目標還是實現(xiàn)瀏覽器應(yīng)用的雙向通信。
WebSocket 使用了自定義的二進制分幀格式,把每個應(yīng)用消息切分成一或多個幀,發(fā)送到目的地之后再組裝起來,等到接收到完整的消息后再通知接收端?;镜某蓭瑓f(xié)議定義了幀類型有操作碼、有效載荷的長度,指定位置的Extension data和Application data,統(tǒng)稱為Payload data,保留了一些特殊位和操作碼供后期擴展。在打開握手完成后,終端發(fā)送一個關(guān)閉幀之前的任何時間里,數(shù)據(jù)幀可能由客戶端或服務(wù)器的任何一方發(fā)送。具體的幀格式如下所示:
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 ... |+---------------------------------------------------------------+
幀:最小的通信單位,包含可變長度的幀首部和凈荷部分,凈荷可能包含完整或部分應(yīng)用消息。
消息:一系列幀,與應(yīng)用消息對等。
是否把消息分幀由客戶端和服務(wù)器實現(xiàn)決定,應(yīng)用并不需要關(guān)注WebSocket幀和如何分幀,因為客戶端(如瀏覽器)和服務(wù)端為完成該工作。那么客戶端和服務(wù)端是按照什么規(guī)則進行分幀的呢?RFC 6455規(guī)定的分幀規(guī)則如下:
在遵循了上述分幀規(guī)則之后,一個消息的所有幀屬于同樣的類型,由第一個幀的opcdoe指定。由于控制幀不能分幀,消息的所有幀的類型要么是文本、二進制數(shù)據(jù)或保留的操作碼中的一個。
雖然客戶端和服務(wù)端都遵循同樣的分幀規(guī)則,但也是有些差異的。在客戶端往服務(wù)端發(fā)送數(shù)據(jù)時,為防止客戶端中運行的惡意腳本對不支持WebSocket 的中間設(shè)備進行緩存投毒攻擊(cache poisoning attack),發(fā)送幀的凈荷都要使用幀首部中指定的值加掩碼。被標記的幀必須設(shè)置MASK域為1,Masking-key必須完整包含在幀里,它用于標記Payload data。Masking-key是由客戶端隨機選擇的32位值,標記鍵應(yīng)該是不可預(yù)測的,給定幀的Masking-key必須不能簡單到服務(wù)器或代理可以預(yù)測Masking-key是用于一序列幀的,不可預(yù)測的Masking-key是阻止惡意應(yīng)用的作者從wire上獲取數(shù)據(jù)的關(guān)鍵。由于客戶端發(fā)送到服務(wù)端的信息需要進行掩碼處理,所以客戶端發(fā)送數(shù)據(jù)的分幀開銷要大于服務(wù)端發(fā)送數(shù)據(jù)的開銷,服務(wù)端的分幀開銷是2~10 Byte,客戶端是則是6~14 Byte。
控制幀由操作碼標識,操作碼的最高位是1。當(dāng)前為控制幀定義的操作碼有0x8(關(guān)閉)、0x9(Ping)和0xA(Pong),操作碼0xB-0xF是保留的,未定義??刂茙脕斫涣鱓ebSocket的狀態(tài),能夠插入到消息的多個幀的中間。所有的控制幀必須有一個小于等于125字節(jié)的有效載荷長度,必須不能被分幀。
關(guān)閉:操作碼為0x8。關(guān)閉幀可能包含一個主體(幀的應(yīng)用數(shù)據(jù)部分)指明關(guān)閉的原因,如終端關(guān)閉,終端接收到的幀太大,或終端接收到的幀不符合終端的預(yù)期格式。從客戶端發(fā)送到服務(wù)器的關(guān)閉幀必須標記,在發(fā)送關(guān)閉幀后,應(yīng)用程序必須不再發(fā)送任何數(shù)據(jù)。如果終端接收到一個關(guān)閉幀,且先前沒有發(fā)送關(guān)閉幀,終端必須發(fā)送一個關(guān)閉幀作為響應(yīng)。終端可能延遲發(fā)送關(guān)閉幀,直到它的當(dāng)前消息發(fā)送完成。在發(fā)送和接收到關(guān)閉消息后,終端認為WebSocket連接已關(guān)閉,必須關(guān)閉底層的TCP連接。服務(wù)器必須立即關(guān)閉底層的TCP連接;客戶端應(yīng)該等待服務(wù)器關(guān)閉連接,但并非必須等到接收關(guān)閉消息后才關(guān)閉,如果它在合理的時間間隔內(nèi)沒有收到反饋,也可以將TCP關(guān)閉。如果客戶端和服務(wù)器同時發(fā)送關(guān)閉消息,兩端都已發(fā)送和接收到關(guān)閉消息,應(yīng)該認為WebSocket連接已關(guān)閉,并關(guān)閉底層TCP連接。
Ping:操作碼為0x9。一個Ping幀可能包含應(yīng)用程序數(shù)據(jù)。當(dāng)接收到Ping幀,終端必須發(fā)送一個Pong幀響應(yīng),除非它已經(jīng)接收到一個關(guān)閉幀。它應(yīng)該盡快返回Pong幀作為響應(yīng)。終端可能在連接建立后、關(guān)閉前的任意時間內(nèi)發(fā)送Ping幀。注意:Ping幀可作為keepalive或作為驗證遠程終端是否可響應(yīng)的手段。
Pong:操作碼為0xA。Pong 幀必須包含與被響應(yīng)Ping幀的應(yīng)用程序數(shù)據(jù)完全相同的數(shù)據(jù)。如果終端接收到一個Ping 幀,且還沒有對之前的Ping幀發(fā)送Pong 響應(yīng),終端可能選擇發(fā)送一個Pong 幀給最近處理的Ping幀。一個Pong 幀可能被主動發(fā)送,這作為單向心跳。對主動發(fā)送的Pong 幀的響應(yīng)是不希望的。
數(shù)據(jù)幀攜帶需要發(fā)送的目標數(shù)據(jù),由操作碼標識,操作碼的最高位是0。當(dāng)前為數(shù)據(jù)幀定義的(文本),0x2(二進制),操作碼0x3-0x7為以后的非控制幀保留,未定義。
操作碼決定了數(shù)據(jù)的解釋:
從上述的數(shù)據(jù)分幀格式可以知道,有很多擴展位預(yù)留,WebSocket 規(guī)范允許對協(xié)議進行擴展,可以使用這些預(yù)留位在基本的WebSocket 分幀層之上實現(xiàn)更多的功能。
下面是負責(zé)制定WebSocket 規(guī)范的HyBi Working Group進行的兩項擴展:
要使用擴展,客戶端必須在第一次的Upgrade 握手中通知服務(wù)器,服務(wù)器必須選擇并確認要在商定連接中使用的擴展。下面就是對升級協(xié)商的介紹。
從上面的介紹可知,WebSocket具有很大的靈活性,提供了很多強大的特性:基于消息的通信、自定義的二進制分幀層、子協(xié)議協(xié)商、可選的協(xié)議擴展等等。上面也講到,客戶端和服務(wù)端需先通過HTTP方式協(xié)商適當(dāng)?shù)膮?shù)后才可建立連接,完成協(xié)商之后,所有信息的發(fā)送和接收不再和HTTP相關(guān),全由WebSocket自身的機制處理。當(dāng)然,完成最初的連接參數(shù)協(xié)商并非必須使用HTTP協(xié)議,它只是一種實現(xiàn)方案,可以有其他選擇。但使用HTTP協(xié)議完成最初的協(xié)商,有以下好處:讓W(xué)ebSockets 與現(xiàn)有HTTP 基礎(chǔ)設(shè)施兼容:WebSocket 服務(wù)器可以運行在80 和443 端口上,這通常是對客戶端唯一開放的端口;可以重用并擴展HTTP 的Upgrade 流,為其添加自定義的WebSocket 首部,以完成協(xié)商。
在協(xié)商過程中,用到的一些頭域如下:
Sec-WebSocket-Version:客戶端發(fā)送,表示它想使用的WebSocket 協(xié)議版本(13表示RFC 6455)。如果服務(wù)器不支持這個版本,必須回應(yīng)自己支持的版本。
Sec-WebSocket-Key:客戶端發(fā)送,自動生成的一個鍵,作為一個對服務(wù)器的“挑戰(zhàn)”,以驗證服務(wù)器支持請求的協(xié)議版本;
Sec-WebSocket-Accept:服務(wù)器響應(yīng),包含Sec-WebSocket-Key 的簽名值,證明它支持請求的協(xié)議版本;
Sec-WebSocket-Protocol:用于協(xié)商應(yīng)用子協(xié)議:客戶端發(fā)送支持的協(xié)議列表,服務(wù)器必須只回應(yīng)一個協(xié)議名;
Sec-WebSocket-Extensions:用于協(xié)商本次連接要使用的WebSocket 擴展:客戶端發(fā)送支持的擴展,服務(wù)器通過返回相同的首部確認自己支持一或多個擴展。
在進行HTTP Upgrade之前,客戶端會根據(jù)給定的URI、子協(xié)議、擴展和在瀏覽器情況下的origin,先打開一個TCP連接,隨后再發(fā)起升級協(xié)商。升級協(xié)商具體如下:
GET /socket HTTP/1.1 // 請求的方法必須是GET,HTTP版本必須至少是1.1Host: thirdparty.comOrigin: http://example.comConnection: Upgrade Upgrade: websocket // 請求升級到WebSocket 協(xié)議Sec-WebSocket-Version: 13 // 客戶端使用的WebSocket 協(xié)議版本Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // 自動生成的鍵,以驗證服務(wù)器對協(xié)議的支持,其值必須是nonce組成的隨機選擇的16字節(jié)的被base64編碼后的值Sec-WebSocket-Protocol: appProtocol, appProtocol-v2 // 可選的應(yīng)用指定的子協(xié)議列表Sec-WebSocket-Extensions: x-webkit-deflate-message, x-custom-extension // 可選的客戶端支持的協(xié)議擴展列表,指示了客戶端希望使用的協(xié)議級別的擴展
在安全工程中,Nonce是一個在加密通信只能使用一次的數(shù)字。在認證協(xié)議中,它往往是一個隨機或偽隨機數(shù),以避免重放攻擊。Nonce也用于流密碼以確保安全。如果需要使用相同的密鑰加密一個以上的消息,就需要Nonce來確保不同的消息與該密鑰加密的密鑰流不同。
與瀏覽器中客戶端發(fā)起的任何連接一樣,WebSocket 請求也必須遵守同源策略:瀏覽器會自動在升級握手請求中追加Origin 首部,遠程服務(wù)器可能使用CORS 判斷接受或拒絕跨源請求。要完成握手,服務(wù)器必須返回一個成功的“Switching Protocols”(切換協(xié)議)響應(yīng),具體如下:
HTTP/1.1 101 Switching Protocols // 101 響應(yīng)碼確認升級到WebSocket 協(xié)議Upgrade: websocketConnection: UpgradeAccess-Control-Allow-Origin: http://example.com // CORS 首部表示選擇同意跨源連接Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // 簽名的鍵值驗證協(xié)議支持Sec-WebSocket-Protocol: appProtocol-v2 // 服務(wù)器選擇的應(yīng)用子協(xié)議Sec-WebSocket-Extensions: x-custom-extension // 服務(wù)器選擇的WebSocket 擴展
所有兼容RFC 6455 的WebSocket 服務(wù)器都使用相同的算法計算客戶端挑戰(zhàn)的答案:將Sec-WebSocket-Key 的內(nèi)容與標準定義的唯一GUID 字符串拼接起來,計算出SHA1 散列值,結(jié)果是一個base-64 編碼的字符串,把這個字符串發(fā)給客戶端即可。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 | "+" | "/"
如果客戶端發(fā)送的key值為:dGhlIHNhbXBsZSBub25jZQ==
,服務(wù)端將把258EAFA5-E914-47DA-95CA-C5AB0DC85B11
這個唯一的GUID與它拼接起來,就是dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CAC5AB0DC85B11
,然后對其進行SHA-1哈希,結(jié)果為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 握手必須是客戶端發(fā)送協(xié)議版本和自動生成的挑戰(zhàn)值,服務(wù)器返回101 HTTP 響應(yīng)碼(Switching Protocols)和散列形式的挑戰(zhàn)答案,確認選擇的協(xié)議版本。
一旦客戶端打開握手發(fā)送出去,在發(fā)送任何數(shù)據(jù)之前,客戶端必須等待服務(wù)器的響應(yīng)??蛻舳吮仨毎慈缦虏襟E驗證響應(yīng):
如果客戶端完成了對服務(wù)端響應(yīng)的升級協(xié)商驗證,該連接就可以用作雙向通信信道交換WebSocket 消息。從此以后,客戶端與服務(wù)器之間不會再發(fā)生HTTP 通信,一切由WebSocket 協(xié)議接管。
Websocket協(xié)議具有極簡的API,開發(fā)者可以很簡便的調(diào)用,而且提供了二進制分幀、可擴展性以及子協(xié)議協(xié)商等強大特性,使得WebSocket 成為在瀏覽器中采用自定義應(yīng)用協(xié)議的最佳選擇。但,在計算機世界里,任何技術(shù)和理論一般都是為解決特定問題而生的,并不是普世化的解決方案,WebSocket亦是如此。WebSocket 不能取代XHR 或SSE,何時以及如何使用,毋庸置疑會對性能產(chǎn)生巨大影響,要獲得最佳性能,我們必須善于利用它的長處!下面將對現(xiàn)有的一些協(xié)議與WebSocket 對比進行一個大致介紹。
XHR 是專門為“事務(wù)型”請求/ 響應(yīng)通信而優(yōu)化的:客戶端向服務(wù)器發(fā)送完整的、格式良好的HTTP 請求,服務(wù)器返回完整的響應(yīng)。這里不支持請求流,在Streams API 可用之前,沒有可靠的跨瀏覽器響應(yīng)流API。 SSE 可以實現(xiàn)服務(wù)器到客戶端的高效、低延遲的文本數(shù)據(jù)流:客戶端發(fā)起 SSE 連接,服務(wù)器使用事件源協(xié)議將更新流式發(fā)送給客戶端。客戶端在初次握手后,不能向服務(wù)器發(fā)送任何數(shù)據(jù)。 WebSocket 是唯一一個能通過同一個TCP 連接實現(xiàn)雙向通信的機制,客戶端和服務(wù)器隨時可以交換數(shù)據(jù)。因此,WebSocket 在兩個方向上都能保證文本和二進制應(yīng)用數(shù)據(jù)的低延遲交付??蛻舳说椒?wù)端傳遞消息的總時延由以下四個部分構(gòu)成:
無論是什么樣的傳輸機制,都不會減少客戶端與服務(wù)器間的往返次數(shù),數(shù)據(jù)包的傳播延遲都一樣。但,采用不同的傳輸機制可以有不同的排隊延遲。對XHR 輪詢而言,排隊延遲就是客戶端輪詢間隔:服務(wù)器上的消息可用之后,必須等到下一次客戶端XHR 請求才能發(fā)送。相對來說,SSE 和WebSocket 使用持久連接,這樣服務(wù)器(和客戶端——如果是WebSocket)就可以在消息可用時立即發(fā)送它,消除了消息的排隊延遲,也就使得總的傳輸延遲更小。
在完成最初的升級協(xié)商之后,客戶端和服務(wù)器即可通過WebSocket 協(xié)議雙向交換數(shù)據(jù),消息分幀之后每幀會添加2~14 字節(jié)的開銷;SSE 會給每個 消息添加 5 字節(jié),但僅限于 UTF-8 內(nèi)容(SSE 不是為傳輸二進制載荷而設(shè)計的!如果有必要,可以把二進制對象編碼為base64 形式,然后再使用SSE); HTTP 1.x 請求(XHR 及其他常規(guī)請求)會攜帶 500~800 字節(jié)的 HTTP 元數(shù)據(jù),加上cookie; HTTP 2.0 壓縮 HTTP 元數(shù)據(jù),可以顯著減少開銷,如果請求都不修改首部,那么開銷可以低至8 字節(jié)。WebSocket專門為雙向通信而設(shè)計,開銷很小,在實時通知應(yīng)用開發(fā)中是不錯的選擇。
上述開銷不包括IP、TCP 和TLS 分幀的開銷,后者一共會給每個消息增加60~100 字節(jié),無論使用的是什么應(yīng)用協(xié)議。
在使用HTTP協(xié)議傳輸數(shù)據(jù)時,每個請求都可以協(xié)商最優(yōu)的傳輸編碼格式(如對文本數(shù)據(jù)采用gzip 壓縮);SSE 只能傳輸UTF-8 格式數(shù)據(jù),事件流數(shù)據(jù)可以在整個會話期間使用gzip 壓縮;WebSocket 可以傳輸文本和二進制數(shù)據(jù),壓縮整個會話行不通,二進制的凈荷也可能已經(jīng)壓縮過了!
鑒于WebSocket的特殊性,它需要實現(xiàn)自己的壓縮機制,并針對每個消息選擇應(yīng)用。HyBi 工作組正在為WebSocket 協(xié)議制定以消息為單位的壓縮擴展,但這個擴展尚未得到任何瀏覽器支持。目前來說,除非應(yīng)用通過細致優(yōu)化自己的二進制凈荷實現(xiàn)自己的壓縮邏輯,同時也針對文本消息實現(xiàn)自己的壓縮邏輯,否則傳輸數(shù)據(jù)過程中一定會產(chǎn)生很大的字節(jié)開銷!
HTTP已經(jīng)誕生了數(shù)十年,具有廣泛的應(yīng)用,各種優(yōu)化專門的優(yōu)化機制也已經(jīng)被瀏覽器及服務(wù)器等設(shè)備實施,XHR 請求自然而然就繼承了所有這些功能。然而,對于只使用HTTP協(xié)議完成升級協(xié)商的WebSocket來說,流式數(shù)據(jù)處理可以讓我們在客戶端和服務(wù)器間自定義協(xié)議,但也會錯過瀏覽器提供的很多服務(wù),應(yīng)用可能必須實現(xiàn)自已的邏輯來填充某些功能空白,比如緩存、狀態(tài)管理、元數(shù)據(jù)交付等等。
HTTP 是專為短時突發(fā)性傳輸設(shè)計的,很多服務(wù)器、代理和其他中間設(shè)備的HTTP 連接空閑超時設(shè)置都很激進。這就與WebSocket的長時連接、實時雙向通信相悖,部署時需要關(guān)注下面的三個方面:
鑒于用戶所處的網(wǎng)絡(luò)環(huán)境是各不相同的,不受開發(fā)者所控制。某些網(wǎng)絡(luò)甚至?xí)耆帘蜽ebSocket通信,有些設(shè)備也不支持WebSocket協(xié)議,這時就需要采用備用機制,使用其他技術(shù)來實現(xiàn)類似與WebSocket的通信(如socket.io等)。雖然,我們無法處理網(wǎng)絡(luò)中的中間設(shè)備,但對于處在我們自己掌控下的基礎(chǔ)設(shè)施還是可以做一些工作的,可以對通信路徑上的每一臺負載均衡器、路由器和Web 服務(wù)器針對長時連接進行調(diào)優(yōu)。然而,長時連接和空閑會話會占用所有中間設(shè)備及服務(wù)器的內(nèi)存和套接字資源,開銷很大,部署WebSocket、SSE及HTTP 2.0等賴于長時會話的協(xié)議都會對運維提出新的挑戰(zhàn)。在使用WebSocket的過程中,也需要做到優(yōu)化二進制凈荷和壓縮 UTF-8 內(nèi)容以最小化傳輸數(shù)據(jù)、監(jiān)控客戶端緩沖數(shù)據(jù)的量、切分應(yīng)用消息避免隊首阻塞、合用的情況下利用其他傳輸機制等。
WebSocket 協(xié)議為實時雙向通信而設(shè)計,提供高效、靈活的文本和二進制數(shù)據(jù)傳輸,同時也錯過了瀏覽器為HTTP提供的一些服務(wù),在使用時需要應(yīng)用自己實現(xiàn)。在進行應(yīng)用數(shù)據(jù)傳輸時,需要根據(jù)不同的場景選擇恰當(dāng)?shù)膮f(xié)議,WebSocket 并不能取代HTTP、XHR 或SSE,關(guān)鍵還是要利用這些機制的長處以求得最佳性能。
鑒于現(xiàn)在不同的平臺及瀏覽器版本對WebSocket支持的不同,有開發(fā)者做了一個叫做socket.io 的為實時應(yīng)用提供跨平臺實時通信的庫,我們可以使用它完成向WebSocket的切換。socket.io 旨在使實時應(yīng)用在每個瀏覽器和移動設(shè)備上成為可能,模糊不同的傳輸機制之間的差異。socket.io 的名字源于它使用了瀏覽器支持并采用的 HTML5 WebSocket 標準,因為并不是所有的瀏覽器都支持 WebSocket ,所以該庫支持一系列降級功能:
在大部分情境下,你都能通過這些功能選擇與瀏覽器保持類似長連接的功能。具體細節(jié)請看Socket.io。