WebSocket - ( 一.概述 )

說到 WebSocket,不得不提 HTML5,做爲近年來Web技術領域最大的改進與變化,包含CSS三、離線與存儲、多媒體、鏈接性( Connectivity )等一系列領域,而即將介紹的 WebSocket 則是 HTML5 鏈接性領域( Connectivity )最值得稱道的改進。javascript

1 HTTP通訊的幾種方式

HTTP是用於文檔傳輸簡單同步請求的響應式協議,本質上是無狀態的應用層協議,半雙工的鏈接特性。傳輸層依然是傳輸控制協議( TCP )。java

  在介紹WebSocket以前,首先有必要介紹下在 WebSocket 未出現以前咱們是怎樣實現 HTTP 服務器與客戶端交互通訊。node

1.1 輪詢( polling )

  一種定時的同步調用。當頻率太低,信息不及時,當頻率太高,對服務器負擔大,會產生大量的沒必要要的鏈接開銷。web

// client
var time = 1000 * x;  // 輪詢頻率 / ms
setInterval(function(){
    //ajax 
}, time);

1.2. 長輪詢( long polling )

  客戶端向服務端請求,這個請求在有數據時返回,若是沒有數據,這個請求會一直被掛起,直到有數據返回或超時。結束完成再次向服務器請求。在HTML/1.1以後,瀏覽器默認鏈接改成長鏈接( keep-alive ),正是基於此,服務器才能長時間和客戶端保持鏈接直到返回或超時。ajax

  這種無需第三方插件僅依靠長鏈接維持客戶端與服務器交互的技術廣泛稱爲 comet反向Ajaxapi

對於部分瀏覽器來講,同時創建過多的長鏈接會形成阻塞,例如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參數指定的協議基本有三種類型:1、註冊協議,向註冊管理實體IANA正式註冊的標準協議;2、開放協議,普遍使用的標準化協議,如XMPP或STOMP;3、自定義協議,本身編寫並和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是一個很不錯的選擇。在應用發麪,好比能夠推送新聞、天氣等。

相關文章
相關標籤/搜索