HTML5 webSocket

一、實時web應用的現狀

  在 WebSocket 規範出來之前,開發人員想實現這些實時的 Web 應用,不得不採用一些折衷的方案,其中最常用的就是輪詢 (Polling) 和 Comet 技術,而 Comet 技術實際上是輪詢技術的改進,又可細分為兩種實現方式,一種是長輪詢機制,一種稱為流技術。

  1.輪詢

    這是最早的一種實現實時 Web 應用的方案。客戶端以一定的時間間隔向服務端發出請求,以頻繁請求的方式來保持客戶端和伺服器端的同步。這種同步方案的最大問題是,當客戶端以固定頻率向伺服器發起請求的時候,伺服器端的數據可能並沒有更新,這樣會帶來很多無謂的網路傳輸,所以這是一種非常低效的實時方案。

  2.長輪詢

    長輪詢是對定時輪詢的改進和提高,目地是為了降低無效的網路傳輸。當伺服器端沒有數據更新的時候,連接會保持一段時間周期直到數據或狀態改變或者時間過期,通過這種機制來減少無效的客戶端和伺服器間的交互。當然,如果服務端的數據變更非常頻繁的話,這種機制和定時輪詢比較起來沒有本質上的性能的提高。

  3.流

    流技術方案通常就是在客戶端的頁面使用一個隱藏的窗口向服務端發出一個長連接的請求。伺服器端接到這個請求後作出回應並不斷更新連接狀態以保證客戶端和伺服器端的連接不過期。通過這種機制可以將伺服器端的資訊源源不斷地推向客戶端。這種機制在用戶體驗上有一點問題,需要針對不同的瀏覽器設計不同的方案來改進用戶體驗,同時這種機制在並發比較大的情況下,對伺服器端的資源是一個極大的考驗。

  4.上述方案暴露的問題

    綜合這幾種方案,您會發現這些目前我們所使用的所謂的實時技術並不是真正的實時技術,它們只是在用 Ajax 方式來模擬實時的效果,在每次客戶端和伺服器端交互的時候都是一次 HTTP 的請求和應答的過程,而每一次的 HTTP 請求和應答都帶有完整的 HTTP 頭資訊,這就增加了每次傳輸的數據量,而且這些方案中客戶端和伺服器端的編程實現都比較複雜,在實際的應用中,為了模擬比較真實的實時效果,開發人員往往需要構造兩個 HTTP 連接來模擬客戶端和伺服器之間的雙向通訊,一個連接用來處理客戶端到伺服器端的數據傳輸,一個連接用來處理伺服器端到客戶端的數據傳輸,這不可避免地增加了編程實現的複雜度,也增加了伺服器端的負載,制約了應用系統的擴展性。

二、什麼是webSocket

  WebSocket 協議在2008年誕生,2011年成為國際標準。現在所有瀏覽器都已經支援了。WebSocket 協議本質上是一個基於 TCP 的協議。為了建立一個 WebSocket 連接,客戶端瀏覽器首先要向伺服器發起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭資訊,其中附加頭資訊」Upgrade: WebSocket」表明這是一個申請協議升級的 HTTP 請求,伺服器端解析這些附加的頭資訊然後產生應答資訊返回給客戶端,客戶端和伺服器端的 WebSocket 連接就建立起來了,雙方就可以通過這個連接通道自由的傳遞資訊,並且這個連接會持續存在直到客戶端或者伺服器端的某一方主動的關閉連接。

三、webScoket的特點

  (1)建立在 TCP 協議之上,伺服器端的實現比較容易

  (2)與 HTTP 協議有著良好的兼容性。默認埠也是80和443,並且握手階段採用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理伺服器

  (3)數據格式比較輕量,性能開銷小,通訊高效

  (4)可以發送文本,也可以發送二進位數據

  (5)沒有同源限制,客戶端可以與任意伺服器通訊

  (6)協議標識符是ws(如果加密,則為wss),伺服器網址就是 URL

四、webSocket示例

  一個典型的 WebSocket 發起請求和得到響應的例子看起來如下

客戶端到服務端: 
GET /demo HTTP/1.1 
Host: example.com 
Connection: Upgrade 
Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 
Upgrade: WebSocket 
Sec-WebSocket-Key1: 4@1 46546xW%0l 1 5 
Origin: //example.com 
[8-byte security key] 
 
服務端到客戶端:
HTTP/1.1 101 WebSocket Protocol Handshake 
Upgrade: WebSocket 
Connection: Upgrade 
WebSocket-Origin: //example.com 
WebSocket-Location: ws://example.com/demo 
[16-byte hash response]

  」Upgrade:WebSocket」表示這是一個特殊的 HTTP 請求,請求的目的就是要將客戶端和伺服器端的通訊協議從 HTTP 協議升級到 WebSocket 協議。從客戶端到伺服器端請求的資訊里包含有」Sec-WebSocket-Key1」、「Sec-WebSocket-Key2」和」[8-byte securitykey]」這樣的頭資訊。這是客戶端瀏覽器需要向伺服器端提供的握手資訊,伺服器端解析這些頭資訊,並在握手的過程中依據這些資訊生成一個 16 位的安全密鑰並返回給客戶端,以表明伺服器端獲取了客戶端的請求,同意創建 WebSocket 連接。一旦連接建立,客戶端和伺服器端就可以通過這個通道雙向傳輸數據了。

五、客戶端API

  1.webSocket構造函數

    WebSocket 對象作為一個構造函數,用於新建 WebSocket 實例。執行下面語句之後,客戶端就會與伺服器進行連接。

var ws = new WebSocket('ws://localhost:8080');

  2.webSocket.readyState

    readyState屬性返回實例對象的當前狀態,共有四種。

      (1)connecting:值為0,表示正在連接。

      (2)open:值為1,表示連接成功,可以通訊了。

      (3)closing:值為2,表示連接正在關閉。

      (4)closed:值為3,表示連接已經關閉,或者打開連接失敗。

    示例:

switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // do something
    break;
  case WebSocket.OPEN:
    // do something
    break;
  case WebSocket.CLOSING:
    // do something
    break;
  case WebSocket.CLOSED:
    // do something
    break;
  default:
    // this never happens
    break;
}

  3.webSocket.onopen

    實例對象的onopen屬性,用於指定連接成功後的回調函數。

ws.onopen = function () {
  ws.send('Hello Server!');
}

    如果要指定多個回調函數,可以使用addEventListener方法。

ws.addEventListener('open', function (event) {
  ws.send('Hello Server!');
});

   4.webSocket.onclose

    實例對象的onclose屬性,用於指定連接關閉後的回調函數。

ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
};

ws.addEventListener("close", function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
});

   5.websocket.onmessage

    實例對象的onmessage屬性,用於指定收到伺服器數據後的回調函數。

ws.onmessage = function(event) {
  var data = event.data;
  // 處理數據
};

ws.addEventListener("message", function(event) {
  var data = event.data;
  // 處理數據
});

    注意,伺服器數據可能是文本,也可能是二進位數據(blob對象或Arraybuffer對象)。

ws.onmessage = function(event){
  if(typeof event.data === String) {
    console.log("Received data string");
  }

  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

    除了動態判斷收到的數據類型,也可以使用binaryType屬性,顯式指定收到的二進位數據類型。

// 收到的是 blob 數據
ws.binaryType = "blob";
ws.onmessage = function(e) {
  console.log(e.data.size);
};

// 收到的是 ArrayBuffer 數據
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};

   6.websocket.send()

    實例對象的send()方法用於向伺服器發送數據。   

發送文本的例子。
ws.send('your message');

發送 Blob 對象的例子。
var file = document
  .querySelector('input[type="file"]')
  .files[0];
ws.send(file);

發送 ArrayBuffer 對象的例子。
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);

  7.websocket.bufferedamount

    實例對象的bufferedAmount屬性,表示還有多少位元組的二進位數據沒有發送出去。它可以用來判斷發送是否結束。

var data = new ArrayBuffer(10000000);
socket.send(data);

if (socket.bufferedAmount === 0) {
  // 發送完畢
} else {
  // 發送還沒結束
}

  8.websocket.onerror

    實例對象的onerror屬性,用於指定報錯時的回調函數。

socket.onerror = function(event) {
  // handle error event
};

socket.addEventListener("error", function(event) {
  // handle error event
});

六、Node服務端示例

var WebSocketServer = require('websocket').server;
var http = require('http');

var server = http.createServer(function(request, response) {
    console.log((new Date()) + ' Received request for ' + request.url);
    response.writeHead(404);
    response.end();
});
server.listen(8080, function() {
    console.log((new Date()) + ' Server is listening on port 8080');
});

wsServer = new WebSocketServer({
    httpServer: server,
    // You should not use autoAcceptConnections for production
    // applications, as it defeats all standard cross-origin protection
    // facilities built into the protocol and the browser.  You should
    // *always* verify the connection's origin and decide whether or not
    // to accept it.
    autoAcceptConnections: false
});

function originIsAllowed(origin) {
  // put logic here to detect whether the specified origin is allowed.
  return true;
}

wsServer.on('request', function(request) {
    if (!originIsAllowed(request.origin)) {
      // Make sure we only accept requests from an allowed origin
      request.reject();
      console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
      return;
    }
    
    var connection = request.accept('echo-protocol', request.origin);
    console.log((new Date()) + ' Connection accepted.');
    connection.on('message', function(message) {
        if (message.type === 'utf8') {
            console.log('Received Message: ' + message.utf8Data);
            connection.sendUTF(message.utf8Data);
        }
        else if (message.type === 'binary') {
            console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
            connection.sendBytes(message.binaryData);
        }
    });
    connection.on('close', function(reasonCode, description) {
        console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
    });
});

參考文檔:1.//www.ruanyifeng.com/blog/2017/05/websocket.html

     2.//www.ibm.com/developerworks/cn/web/1112_huangxa_websocket/index.html

     3.//developer.mozilla.org/zh-CN/docs/Web/API/WebSocket

     4.//blog.csdn.net/li_jia_wei/article/details/81148053