1. 程式人生 > >使用WebSocket構建實時WEB

使用WebSocket構建實時WEB

http://www.cnblogs.com/shijiaqi1066/p/3795075.html

1 WebSocket與傳統Web實時通訊技術

1.1 WebSocket

HTTP是一種典型的單工模式。即基於Request/Response的方式與伺服器進行互動。HTML5提供了瀏覽器與服務端的雙工通訊協議WebSocket。

1.2傳統Web實時通訊技術

  • 輪詢
  • Comet
  • 長輪詢
  • Flash XML Socket

輪詢,對伺服器壓力較大,實時性差,效率低下。

移動端對Flash支援的非常差。Flash經常全域性性的崩潰,很不穩定。

Comet與長輪詢應該是傳統方案中最好的Web實時互動的技術。這兩種技術本質是一種Hack技術。Comet與長輪詢相比有許多缺點。但這不影響它們在傳統實時Web中的應用。這兩項技術最大的問題在於每次互動的HTTP包的header內容過多,真正有實際意義的資訊可能很少,從而導致網路利用率非常低。

對於長輪詢技術,由於離不開請求/響應的模式,所以伺服器壓力也會較大。

2 WebSocket協議

WebSocket與HTTP協議都是基於TCP的,都是可靠的協議。WebSocket和Http協議一樣都屬於應用層的協議。WebSocket使用標準的80和443埠,這兩個埠都是防火牆的友好埠所以不需要防火牆的允許。

WebSocket協議的格式為 "ws://IP:Port" 或者"wss://IP:Port"。其中wss表示進行加密傳輸的WebSocket協議。

WebSocket協議需要進行"握手"。該"握手"階段是通過HTTP協議進行的,"握手"行為通過Request/Response的Header完成,只需要交換很少的資料,便可以建立基於TCP/IP協議的雙工通道。

wps_clip_image-32524

瀏覽器與伺服器通過TCP三次握手建立連線,如果連線建立失敗,則瀏覽器將收到錯誤訊息通知。

TCP建立連線成功後,瀏覽器通過HTTP協議傳送WebSocket支援的版本號,協議的字版本號,原始地址,主機地址等等一些列欄位給伺服器端。

2.1 WebSocket 握手

WebSocket協議握手協議非常簡單。

例項:執行Tomcat7提供的WebSocket Echo示例程式。當發生Connect時,Chrome中攔截到資訊與截包工具中所得的報文如下所示。

Chrome所示

wps_clip_image-25577

截包工具所示

請求報文

wps_clip_image-27367

說明:

Connection:Upgrade與Upgrade:WebSocket 表示本次請求是要進行WebSocket的握手動作。

Sec-WebSocket-Version首部的值,表示瀏覽器支援的WebSocket版本資訊。

Sec-WebSocket-Key首部的值,是一個由客戶端隨機生成的字串。

響應報文

wps_clip_image-13404

說明:

在伺服器響應的握手資訊中Sec-WebSocket-Accept的值為伺服器通過客戶端Header的Sec-WebSocket-Key的值進行計算並加密的結果。

伺服器的響應狀態為101,表示伺服器端已經理解了客戶端的需求,並且客戶端需要根據Upgrade中的協議型別,切換為新的協議來完成後續的通訊。

經過以上報文的通訊候,基於WebSocket的TCP/IP雙工通道就已經建立了。

2.2 握手驗證

最老的WebSocket草案標準中是沒有安全key。草案7.5、7.6中有兩個安全key。草案10中只有一個安全key。

7.5、7.6中HTTP頭中的"Sec-WebSocket-Key1"與"Sec-WebSocket-Key2"在10中合併為了一個"Sec-WebSocket-Key"。HTTP頭中Upgrade的值由"WebSocket"修改為了"websocket"。HTTP頭中的"-Origin"修改為了"Sec-WebSocket-Origin"。

增加了HTTP頭"Sec-WebSocket-Accept",用來返回原來草案7.5、7.6伺服器返回給客戶端的握手驗證,原來是以內容的形式返回,現在是放到了HTTP頭中。

伺服器生成驗證的方式變化較大。

舊版WebSocket

wps_clip_image-32548

舊版生成Token的方法如下:

取出Sec-WebSocket-Key1中的所有數字字元形成一個數值,這裡是1427964708,然後除以Key1中的空格數目,得到一個數值,保留該數值整數位,得到數值N1;對Sec-WebSocket-Key2採取同樣的演算法,得到第二個整數N2;把N1和N2按照Big-Endian字元序列連線起來,然後再與另外一個Key3連線,得到一個原始序列ser_key。Key3是指在握手請求最後,有一個8位元組的奇怪的字串“;”######”,這個就是Key3。然後對ser_key進行一次md5運算得出一個16位元組長的digest,這就是老版本協議需要的token,然後將這個token附在握手訊息的最後傳送回Client,即可完成握手。

新版WebSocket

wps_clip_image-8713

新版生成Token的方法如下:

首先伺服器將key(長度24)截取出來,如4tAjitqO9So2Wu8lkrsq3w==,用它和自定義的一個字串(長度36)258EAFA5-E914-47DA-95CA-C5AB0DC85B11連線起來,然後把這一字串進行SHA-1演算法加密,得到長度為20位元組的二進位制資料,再將這些資料經過Base64編碼,最終得到服務端的金鑰,也就是ser_key。伺服器將ser_key附在返回值Sec-WebSocket-Accept後,至此握手成功。

2.3 資料報文格式

舊版協議比較簡單,僅僅是在原始資料前加了個’\x00′,在最後面加了個’\xFF’,即假如Client傳送一個字串’test’,實際上WebSocket Server收到的資料是:’x00test\xFF’,所以只需要剝離掉首尾那兩個字元就可以了。

新版的協議對這部分規定比較複雜,以下是其格式標準:(下圖在Firefox可能會出現錯亂,請換用Chrome)

wps_clip_image-1427

FIN:1位,用來表明這是一個訊息的最後的訊息片斷,當然第一個訊息片斷也可能是最後的一個訊息片斷;

RSV1, RSV2, RSV3:分別都是1位,如果雙方之間沒有約定自定義協議,那麼這幾位的值都必須為0,否則必須斷掉WebSocket連線;

Opcode:4位操作碼,定義有效負載資料,如果收到了一個未知的操作碼,連線也必須斷掉,以下是定義的操作碼:

%x0 表示連續訊息片斷

%x1 表示文字訊息片斷

%x2 表未二進位制訊息片斷

%x3-7 為將來的非控制訊息片斷保留的操作碼

%x8 表示連線關閉

%x9 表示心跳檢查的ping

%xA 表示心跳檢查的pong

%xB-F 為將來的控制訊息片斷的保留操作碼

Mask:1位,定義傳輸的資料是否有加掩碼,如果設定為1,掩碼鍵必須放在masking-key區域,客戶端傳送給服務端的所有訊息,此位的值都是1;

Payload length:傳輸資料的長度,以位元組的形式表示:7位、7+16位、或者7+64位。如果這個值以位元組表示是0-125這個範圍,那這個值就表示傳輸資料的長度;如果這個值是126,則隨後的兩個位元組表示的是一個16進位制無符號數,用來表示傳輸資料的長度;如果這個值是127,則隨後的是8個位元組表示的一個64位無符合數,這個數用來表示傳輸資料的長度。多位元組長度的數量是以網路位元組的順序表示。負載資料的長度為擴充套件資料及應用資料之和,擴充套件資料的長度可能為0,因而此時負載資料的長度就為應用資料的長度。

Masking-key:0或4個位元組,客戶端傳送給服務端的資料,都是通過內嵌的一個32位值作為掩碼的;掩碼鍵只有在掩碼位設定為1的時候存在。 Payload data: (x+y)位,負載資料為擴充套件資料及應用資料長度之和。 Extension data:x位,如果客戶端與服務端之間沒有特殊約定,那麼擴充套件資料的長度始終為0,任何的擴充套件都必須指定擴充套件資料的長度,或者長度的計算方式,以及在握手時如何確定正確的握手方式。如果存在擴充套件資料,則擴充套件資料就會包括在負載資料的長度之內。 Application data:y位,任意的應用資料,放在擴充套件資料之後,應用資料的長度=負載資料的長度-擴充套件資料的長度。

2.4 WebSocket版本與瀏覽器支援

wps_clip_image-13432

3 Web前端相關的WebSocket

3.1 W3C API定義

Websocket的API非法簡單,以下是W3C的定義

wps_clip_image-8369

enum BinaryType { "blob", "arraybuffer" };
[Constructor(DOMString url, optional (DOMString or DOMString[]) protocols)]
interface WebSocket : EventTarget {
  readonly attribute DOMString url;
  // ready state
  const unsigned short CONNECTING = 0;
  const unsigned short OPEN = 1;
  const unsigned short CLOSING = 2;
  const unsigned short CLOSED = 3;<br>
  readonly attribute unsigned short readyState;
  readonly attribute unsigned long bufferedAmount;
  // networking
          attribute EventHandler onopen;
          attribute EventHandler onerror;
          attribute EventHandler onclose;
  readonly attribute DOMString extensions;
  readonly attribute DOMString protocol;
  void close([Clamp] optional unsigned shortcode, optional DOMString reason);
  // messaging
          attribute EventHandler onmessage;
          attribute BinaryType binaryType;
  void send(DOMString data);
  void send(Blob data);
  void send(ArrayBuffer data);
  voidsend(ArrayBufferView data);
};

3.2 實際使用的簡單例子

例1

var wsServer = 'ws://localhost:8888/Demo';
var websocket = new WebSocket(wsServer);

websocket.onopen = function (evt) { onOpen(evt) };
websocket.onclose = function (evt) { onClose(evt) };
websocket.onmessage = function (evt) { onMessage(evt) };
websocket.onerror = function (evt) { onError(evt) };

function onOpen(evt) {
    console.log("Connected to WebSocket server.");
}

function onClose(evt) {
    console.log("Disconnected");
}

function onMessage(evt) {
    console.log('Retrieved data from server: ' + evt.data);
}

function onError(evt) {
    console.log('Error occured: ' + evt.data);
}

例2

<!DOCTYPE html>
<meta charset="utf-8" />
<title>WebSocket Test</title>
<script language="javascript"type="text/javascript">

    var wsUri ="ws://echo.websocket.org/";
    var output;

    function init() {
        output = document.getElementById("output");
        testWebSocket();
    }

    function testWebSocket() {
        websocket = new WebSocket(wsUri);
        websocket.onopen = function(evt) {
            onOpen(evt)
        };

        websocket.onclose = function(evt) {
            onClose(evt)
        };

        websocket.onmessage = function(evt) {
            onMessage(evt)
        };

        websocket.onerror = function(evt) {
            onError(evt)
        };
    }

    function onOpen(evt) {
        writeToScreen("CONNECTED");
        doSend("WebSocket rocks");
    }

    function onClose(evt) {
        writeToScreen("DISCONNECTED");
    }

    function onMessage(evt) {
        writeToScreen('<span style="color: blue;">RESPONSE: '+ evt.data+'</span>');
        websocket.close();
    }

    function onError(evt) {
        writeToScreen('<span style="color: red;">ERROR:</span> '+ evt.data);
    }

    function doSend(message) {
        writeToScreen("SENT: " + message);
        websocket.send(message);
    }

    function writeToScreen(message) {
        var pre = document.createElement("p");
        pre.style.wordWrap = "break-word";
        pre.innerHTML = message;
        output.appendChild(pre);
    }

    window.addEventListener("load", init, false);

</script>
<h2>WebSocket Test</h2>
<div id="output"></div>
</html>

程式碼簡述

使用new建立WebSocket物件。

var wsUri ="ws://echo.websocket.org/";
websocket = new WebSocket(wsUri);

WebSocket物件一共支援四個事件響應方法 onopen, onmessage, oncloseonerror。這樣不會阻塞UI,得到更好的使用者體驗。

當瀏覽器和伺服器連線成功後,會觸發onopen事件。

websocket.onopen = function(evt) {};

如果連線失敗,傳送、接收資料失敗或者處理資料出現錯誤,會觸發onerror事件。

websocket.onerror = function(evt) {};

當瀏覽器接收到伺服器傳送過來的資料時,就會觸發onmessage事件。引數evt中包含伺服器傳輸過來的資料;

websocket.onmessage = function(evt) {};

當瀏覽器接收到伺服器傳送的關閉連線請求時,就會觸發onclose事件。

websocket.onclose = function(evt) {};

在實際應用中還需要考慮心跳包的問題。

4 WebSocket的缺點

最大的問題就是瀏覽器相容性問題。低版本IE瀏覽器不支援該技術,直到IE10才開始支援WebSocket技術。

當然,解決方案是對於低版本瀏覽器可以使用Flash來模擬WebSocket。

5 WebSocket的伺服器實現

WebSocket在網上有非常多實現的例子。以下列出個人會學習研究的伺服器實現。

  • Netty實現
  • Tomcat7實現
  • JavaEE7+Tomcat8實現
  • Node.js實現

參考資料

為了防止無良網站的爬蟲抓取文章,特此標識,轉載請註明文章出處。LaplaceDemon/SJQ。

http://www.cnblogs.com/shijiaqi1066/p/3795075.html