WebSocket - ( 一.概述 )
說到 WebSocket,不得不提 HTML5,作為近年來Web技術領域最大的改進與變化,包含CSS3、離線與存儲、多媒體、連接性( Connectivity )等一系列領域,而即將介紹的 WebSocket 則是 HTML5 連接性領域( Connectivity )最值得稱道的改進。
1 HTTP通信的幾種方式
HTTP是用於文檔傳輸簡單同步請求的響應式協議,本質上是無狀態的應用層協議,半雙工的連接特性。傳輸層依然是傳輸控制協議( TCP )。
在介紹WebSocket之前,首先有必要介紹下在 WebSocket 未出現之前我們是怎樣實現 HTTP 服務器與客戶端交互通信。
1.1 輪詢( polling )
一種定時的同步調用。當頻率過低,信息不及時,當頻率過高,對服務器負擔大,會產生大量的不必要的連接開銷。
// client var time = 1000 * x; // 輪詢頻率 / ms setInterval(function(){ //ajax }, time);
1.2. 長輪詢( long polling )
客戶端向服務端請求,這個請求在有數據時返回,如果沒有數據,這個請求會一直被掛起,直到有數據返回或超時。結束完成再次向服務器請求。在HTML/1.1之后,瀏覽器默認連接改為長連接( keep-alive ),正是基於此,服務器才能長時間和客戶端保持連接直到返回或超時。
這種無需第三方插件僅依靠長連接維持客戶端與服務器交互的技術普遍稱為 comet 或 反向Ajax。
對於部分瀏覽器來說,同時建立過多的長連接會造成阻塞,例如IE,在打開兩個長連接之后,第三個HTTP請求會被阻塞。因為HTTP 1.1規范對長連接數有相應規定,不建議建立兩個以上的長連接,因為某些瀏覽器嚴格執行了這些規范。因此我們無論用哪種方式建立長連接的時候,如果要考慮應用程序性能體驗,都需要注意避免這種情況——避免IE給我們造成的麻煩。
1.3. 流化( streaming )
客戶端發送一個請求,服務器發送並維護一個持續更新和保持打開的開放響應,這個請求除非超時或主動關閉,否則會一直源源不斷向客戶端返回數據。
流化通常有兩種技術方案,第一種通過在HTML頁面添加一個隱藏的Iframe標簽,然后再這個Iframe的src屬性設置一個長連接請求,服務器據此不斷向客戶端發送數據。這個數據形式就是Javascript的函數調用。這種方式早期部分瀏覽器會一直處於loading狀態。第二種利用部分瀏覽器的multi-part標志,但這種的局限性也很明顯,受限於支持multi-part標志的瀏覽器,因為XMLHttpRequest在不同瀏覽器上有着不同的實現。
即便流的處理方式與長輪詢一樣具有較低延遲的特點,但仍要注意的是,很多流化的處理方式對於存在防火牆和代理網絡並不太友好,由於連接一直打開,代理或防火牆可能會緩存響應,增加信息交付延遲。
/** 使用 Forever IFrame */ // client <iframe id='hidden_iframe_polling' name='hidden_iframe_polling' style='display:none;' /> <script> var url=''; // 請求地址 var time = 1000 * x; // 輪詢頻率 / ms setInterval(function(){ document.getElementById('hidden_iframe_polling').src = url + "?t=" + new Date(); //時間戳保證每次都是最新請求 window.frames["hidden_iframe_polling"].location.reload(); }, time); </script>
/** 使用 multi-part */ // client var url = ''; // 請求地址 var type = ''; // 請求方式 post / get var xhr = new XMLHttpRequest(); xhr.multipart = true; // multi-part 標志設置為 true xhr.timeout = 1000 * x; // 超時 / ms xhr.onreadystatechange = state_change_call; xhr.open(type, url, true); xhr.send(null); function state_change_call(){ //回調 } // server // 建立長連接,設置content-type的值為multipart/mixed或multipart/x-mixed-replace // 例:multipart/x-mixed-replace;boundary="string類型數據"
因為服務器端語言不同,無法一致描述,但本質上解決方式還是一樣的。
1.4.其它
其它方式這邊不再討論,諸如捎帶輪詢( piggyback polling )、第三方插件( FlashSockets )之類。
直至今日,即便WebSocket有很多成熟的案例,但考慮到老版本瀏覽器兼容問題,以上部分技術( comet )依然在廣泛使用,即便是一款成熟的WebSocket組件,大部分都會提供降級服務。因此我們在學習開發一套基於WebSocket的組件時,如果想用於生產環境,為了應用程序的健壯、穩定、兼容性,最好也為老版本瀏覽器提供降級服務,在不支持WebSocket的瀏覽器上使用 comet 技術。當然,完成之后也別忘了必要的性能和可伸縮性測試。
2. WebSocket
WebSocket是全雙工、雙向、單套接字連接。WebSocket是一個低層網絡協議,可以在它的基礎上構建其它標准協議。
HTML5在Web端為我們帶來了革命性的變化,這些變化仿佛注入了一股強心劑,無論視覺還是用戶體驗,或甚是開發上簡潔簡約的體驗,相較之前,都有了質的飛躍,在連接領域 WebSocket 也印證了這一點。
說的直觀一點,WebSocket就是Http連接的升級版,當你需要進行WebSocket連接時,你就需要把即將要連接的Http請求的升級為WebSocket。
2.1. WebSocket Protocol
一旦建立起WebSocket請求,不需要客戶端發起,客戶端也能及時接收到來自服務端的數據。WebSocket使用起來相比傳統HTTP通信更加的簡潔、高效、直觀,它解決了HTTP通信的諸多不足之處,而且它真的足夠簡單,這往往是說服我們使用它的理由。
當連接前判斷瀏覽器是否支持原生WebSocket,只需要如下一行代碼即可:
if(window.WebSocket){ // 支持WebSocket }
WebSocket的連接都基於HTTP請求,那怎么區分這次請求是HTTP還是WebSocket呢?
只需要在請求頭當中包含一個Upgrade的請求頭,這是向服務器指定某種傳輸協議。例如指定WebSocket協議就是:
//
// -client // 瀏覽器發送一個請求到服務器,表示它想把HTTP協議轉為WebSocket。客戶端通過更新頭字段(Upgrade header)實現了這個目的 GET /echo HTTP/1.1 Sec-WebSocket-Key: xx Sec-WebSocket-Verson: xx Connection: Upgrade Upgrade: websocket // // -server // 如果服務器識別WebSocket協議,它通過Upgrade header接受協議轉換 Connection:Upgrade Sec-WebSocket-Accept: xx Upgrade: WebSocket
此時HTTP連接會被基於TCP/IP連接的WebSocket連接所取代。WebSocket連接默認使用和HTTP(80)或者HTTPS(443)一樣的端口,同樣,你可以像部署Web服務一樣使用其它端口。
另外,WebSocket為了完成握手,服務器必須響應一個計算出來的鍵值。這個響應說明服務器理解WebSocket協議。就像暗號一樣,只有對上暗號才是自己人。
那么話說回來,究竟是如何計算響應鍵值的呢?很簡單,響應函數從客戶端的Sec-WebSocket-Key請求頭中取得鍵值,並在Sec-WebSocket-Accept請求頭中返回根據客戶端通過SHA1計算出鍵值,通過BASE64返回字符串。
// -- node.js // 計算響應鍵值函數 var KEY_SUFFIX = ""; //協議規范中包含的一個固定鍵值后綴,服務器必須得知道這個值 var hashWebSocketKey = function(key){ var sha1_enc= crypto.createHash("sha1").update(key + KEY_SUFFIX, "utf8"); return sha1_enc.digest("base64"); }
在建立WebSocket連接握手成功之后,服務器會一直以幀( frame )的形式往客戶端發送數據。
WebSocket傳輸內容支持文本或二進制數據,這些數據的邊界靠幀( frame )來維護。我們不妨看一下WebSocket的幀特性。
這是官方標准協議提供的結構圖,接下來要做的就是解析這張圖。第一眼看到這張圖的時候,或許有點懵圈,仔細看就會明白。從第二行開始,居然是十進制的數值依次排開,其實就是第幾位( bit )。然后再往下的內容都是追述及說明。
WebSocket幀頭第一個字節第一位( bit )表示FIN碼,因為WebSocket可以多幀,當你需要多幀的時候,前面的所有幀FIN位設值為0,最后一幀設置為1,用來標識結束幀。第一個字節第二位( bit )到第四位都是RSV碼,分別占1bit,如果通信兩端沒有設置自定義協議,那么設置為0即可。第一個字節的后四位是操作碼( opcode ),這是一個十六位無符號整數。
操作碼 | 消息類型 | |
---|---|---|
%x0 | 0 | 附加數據幀 |
%x1 | 1 | 文本 |
%x2 | 2 | 二進制數據 |
%x8 | 8 | 連接關閉 |
%x9 | 9 | ping |
%xA | 10 | pong |
其它 | 一共16位,其余全部保留用作將來擴展 |
這里要提一句,WebSocket以文本傳輸的時候,都為UTF-8編碼,是WebSocket協議允許的唯一編碼。
第二個字節高1位( bit )為MASK掩碼,俗稱“屏蔽”,就是用來標識客戶端到服務端的數據是否加密混淆內容(payload)。
第二個字節低7位( bit )用來標識消息內容的長度( payload len )。上圖的Extended payload length是用來標識擴展的長度。這樣做的好處是使用可變位數來標示編碼長度能使消息更加緊湊。數據長度一共有三種情況,全都由低7位的值認定,如果取值在126以內,不包括126,則數據真實長度就是低7位的值。如果取值為126,則需要額外的兩個字節來表示數據的真實長度,16位的無符號整數。如果取值127,那么需要額外的8個字節表示數據的真實長度,64位的無符號整數。
之后就是Masking-key,一共4個字節,當然這是上一段說到的MASK掩碼設置為1的時候,且在客戶端到服務端發送消息時才會存在。那么當存在Masking-key時,服務器接收的每個數據包在處理之前都需要解除掩碼。
var data; //數據 var mask; //掩碼 // 解除掩碼 var content= new Buffer(data.length); for(var i= 0; i < data.length; i++){ content[i] = buffer[i] ^ mask[i%4]; } // content 就是解除掩碼之后的數據
再之后就是Payload數據了。由此也可看出,WebSocket最小的傳輸大小僅為2KB,譬如關閉( %x8 )幀。
這個圖表達更為形象,幀的所有內容幾乎都被囊括其中。到這里關於幀也不再贅述。
以上簡單敘述了握手、連接並以幀形式的傳輸數據包,那么接下來我們要看一下WebSocket的關閉握手。
WebSocket關閉時,不論客戶端還是服務端都會發送一個終止的數字代碼,以及一個表示關閉原因的字符串。當然,這些就像上面關於幀( frame )內容提到的一樣,關閉操作的數據邊界依然以幀的形式傳輸,操作符( opcode )為8,其中包含終止代碼和內容文本。
到這里其實要簡單敘述下WebSocket數字代碼的含義。
代碼 | 描述 | 場景 |
---|---|---|
0~999 | 禁止 | 1000以下代碼皆無效,不用於任何目的 |
1000~2999 | 保留 | 這些代碼都用於WebSocket的擴展和修訂版本 |
3000~3999 | 需要注冊 | 這些代碼用於"應用程序、程序庫、框架",可在IANA( 互聯網分配機構 )注冊 |
4000~4999 | 私有 | 可以用作自定義 |
而我們所說的關閉代碼正是在1000~2999之間,例如1000表示正常關閉,1001表示離開,1002表示協議錯誤,1007表示無效數據,1011表示意外情況等。
關於WebSocket協議,與TCP一樣可以異步發送消息,都是可以用作高級協議的傳輸層。這么說不是把WebSocket協議等同於TCP,盡管把WebSocket當作傳輸層使用,它的層次仍然在TCP之上。根據OSI的協議層次,IP在網絡層,TCP/UDP在傳輸層,HTTP位於應用層,與WebSocket協議一樣,同樣有着幀實現的SPDY( 由Google提出,增量性的提高HTTP的性能 ),位於會話層。
下面是各個協議之間對比:
TCP | HTTP | WebSocket | |
---|---|---|---|
尋址 | IP地址+端口 | URL | URL |
並發傳輸 | 全雙工 | 半雙工 | 全雙工 |
內容 | 字節 | MIMI消息 | 文本或二進制 |
消息定界 | 否 | 是 | 是 |
消息定向 | 是 | 否 | 是 |
另外,如果我們想對WebSocket協議擴展,可以使用Sec-WebSocket-Extensions請求頭,這個請求頭包含擴展的名稱。
2.2. WebScoket API
上面內容主要討論了WebSocket Protocol相關內容,對其作了一個簡單介紹。那么下面將介紹如何去使用WebSocket,如果要使用WebSocket,那么將用到WebSocket API。
WebSocket API用來控制WebSocket協議,響應服務器觸發的事件。利用這個API,可以用來打開、關閉、發送、接受和監聽服務器觸發的事件。
1.WebSocket構造函數
var url =""; //URL地址 var protocols = []; //協議數組 var ws = new WebSocket(url, protocols); //構造函數
WebSocket(url, protocols)構造函數接受一個或兩個參數。
第一個參數url指定要連接的url。這個url可能是ws:或wss:,類似於HTTP請求的http:或者https:。WebSocket也提供了傳輸層安全性的連接( TLS/SSL )。
第二個是一個協議數組,非必填項。如果它是一個字符串,它相當於一個數組組成的字符串,如果省略,它相當於空數組。其實在protocols參數指定的協議基本有三種類型:一、注冊協議,向注冊管理實體IANA正式注冊的標准協議;二、開放協議,廣泛使用的標准化協議,如XMPP或STOMP;三、自定義協議,自己編寫並和WebSocket一起使用的協議。例如,protocols有可能是簡單對象訪問協議( SOAP )或其它自定義協議。
當創建的WebSocket構造函數被調用時,會先解析URL參數,獲取主機、端口、資源名稱、安全。如果操作失敗,拋出SyntaxError異常並終止操作。如果存在一個安全組件,例如套接字安全協議https,但分析出這個安全是false,例如無效的安全證書,那么拋出一個SecurityError異常。如果protocols協議數組或字符串中Sec-WebSocket-Protocol頭字段定義的值超過一次不匹配,則拋出一個SyntaxError異常並中止。此時返回這個WebSocket的對象,但是后台依然會繼續這些操作。
2.事件
由於WebSocket應用程序監聽WebSocket對象上的事件,用於處理數據和連接狀態,WebSocket對象存在4個事件,包括open、message、error、close。
var socket = new WebSocket('ws://game.example.com:12010/updates'); // 打開事件 socket.onopen = function (e) { console.log("websocket 打開"); }; // 消息事件 socket.binaryType = ""; socket.onmessage = function (e) { if (typeof e.data === "string"){ console.log("處理文本格式數據.") ; if (event.data == 'on') { console.log("處理當數據等於on時.") ; } } }; //error 事件 socket.onerror = function(e){ console.log("正在處理錯誤"); } //關閉事件 socket.onclose = function(e){ console.log("連接關閉"); }
這里額外提一下,close事件有三個屬性,可用於處理和恢復:wasClean、code、reason。wasClean屬性為布爾類型,表示是否順利連接,如果接收到一個正常的close幀,則該屬性為true,如果因為其它原因關閉,該屬性為false。code和reason分別代表錯誤代碼和關閉原因,這個在下文介紹close()方法會具體闡述如何使用。
3.方法
WebSocket有兩個方法:send()和close()。
// 發送消息 var msg =""; //定義消息 socket.onopen = function(e){ socket.send(msg); //發送 } // OR function sendHandler(e){ if (ws.readState === WebSocket.Open){ socket.send(msg); } else { } } // 關閉方法 var code = ""; //定義代碼 var reason = ""; //關閉原因 socket.close(code,reason);
send()方法在WebSocket連接打開之后使用。
4.對象特性
readState,用於報告連接狀態。從下圖可以看到WebSocket的只讀狀態從連接到關閉的取值的整個對象生命周期。
常量 | 值 | 狀態 |
---|---|---|
WebSocket.CONNECTION | 0 | 正在握手請求中,還未完成連接 |
WebSocket.OPEN | 1 | 連接已打開 |
WebSocket.CLOSING | 2 | 連接正在關閉 |
WebSocket.CLOSED | 3 | 連接已關閉 |
bufferAmount,用於檢查發往服務器的緩沖數據量。調用send()方法能使我們立即往服務器發送數據,但是數據量較大、網絡帶寬有限的時候,我們就想知道網絡傳輸速率。這時候我們就可以用bufferedAmount檢查發送隊列中未發送到服務器的字節數。
// -client var info_size= 1024 * 100; //傳輸數據長度 var _url =""; //WebSocket服務器地址 var ws = new WebSocket(_url); ws.onopen = function(){ setInterval(function(){ if(ws.bufferedAmount < info_size){ //do something } },1000); }
protocol,用於服務器理解客戶端在WebSocket上使用的協議。只有在握手完成之后、關閉之前,且服務器選擇了客戶端提供的協議,這個特性才會存值。否則為空。
附:SSE ( Server-Send Event )
盡管這個並不屬於WebSocket,但是屬於HTML5規范一部分,放在這提一下是因為HTML5除了提供WebSocket這種強大特性之外,還提供這種加強了comet的技術。這樣的話,你依然可以不通過WebSocket實現某些業務需求。顧名思義,SSE主要功能是向客戶端廣播或推送消息。如果僅需使用到服務器單方面推送或廣播,並不需要雙向全雙工通信,那么SSE是一個很不錯的選擇。在應用發面,比如可以推送新聞、天氣等。