WebSocket技術分享

在正式介紹WebSocket以前先跟你們科普一下以及討論一下過去是如何實現Web雙向通訊的javascript

科普一下通信傳輸模式

  • 單工:只支持數據在一個方向上傳輸;例如:BP機
  • 半雙工:容許數據在兩個方向上傳輸,可是某一時刻只容許數據在一個方向上傳輸;例如:對講機, 電報機
  • 全雙工:同時在兩個方向上傳輸,是兩個單工通訊的結合,要求發送設備和接收設備同時具備獨立的接收和發送能力。 例如:手機

歷史回顧

歷史回顧示意圖

HTTP 協議有一個缺陷:通訊只能由客戶端發起。舉例來講,咱們想了解今天的天氣,只能是客戶端向服務器發出請求,服務器返回查詢結果。HTTP 協議作不到服務器主動向客戶端推送信息。這種單向請求的特色,註定了若是服務器有連續的狀態變化,客戶端要獲知就很是麻煩。 在WebSocket協議以前,有三種實現雙向通訊的方式:輪詢(polling)、長輪詢(long-polling)和iframe流(streaming)。html

輪詢(polling)

輪詢示意圖
輪詢示意圖

輪詢是客戶端和服務器之間會一直進行鏈接,每隔一段時間就詢問一次。其缺點也很明顯:鏈接數會不少,一個接受,一個發送。並且 每次發送請求都會有Http的Header,會很耗流量,也會消耗CPU的利用率 。vue

  • 優勢:實現簡單,無需作過多的更改
  • 缺點:輪詢的間隔過長,會致使用戶不能及時接收到更新的數據;輪詢的間隔太短,會致使查詢請求過多,增長服務器端的負擔

實例html5

1.index.htmljava

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
    <script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
    <title>polling</title>
</head>
<body>
<div id="app">
    <button @click="polling">http 輪詢</button>
    <button @click="stopPolling">中止輪詢</button>
    <p>{{time}}</p>
</div>
<script> window.onload=function(){ let vm=new Vue({ el:'#app', data:{ time: '', timer: null }, mounted() { }, methods: { polling() { this.stopPolling() this.timer = setInterval(this.getTime, 1000) }, stopPolling() { clearInterval(this.timer) this.timer = null }, getTime(){ window.axios.get('/polling').then(res => { this.time = res.data }) } } }); }; </script>
</body>
</html>
複製代碼

2.server.jsnode

// server.js
const port = 8001
let path = require('path');
let express = require('express'), //引入express模塊
   app = express(),
   server = require('http').createServer(app);
app.use(express.static(path.join(__dirname, 'static'))); //指定靜態HTML文件的位置
app.get('/polling',function(req,res){
    res.end(new Date().toLocaleString());
});
server.listen(port);
server.setTimeout(0);   //設置不超時,因此服務端不會主動關閉鏈接
console.log('server started', 'http://127.0.0.1:' + port);
複製代碼

3.效果圖 webpack

實例效果圖

長輪詢(long-polling)

長輪詢示意圖

長輪詢是對輪詢的改進版,客戶端發送HTTP給服務器以後,看有沒有新消息,若是沒有新消息,就一直等待。當有新消息的時候,纔會返回給客戶端。在某種程度上減少了網絡帶寬和CPU利用率等問題。因爲http數據包的頭部數據量每每很大(一般有400多個字節),可是真正被服務器須要的數據卻不多(有時只有10個字節左右),這樣的數據包在網絡上週期性的傳輸,不免 對網絡帶寬是一種浪費 。ios

  • 優勢:比 Polling 作了優化,有較好的時效性
  • 缺點:保持鏈接會消耗資源; 服務器沒有返回有效數據,程序超時。

實例git

1.index.htmlgithub

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="https://unpkg.com/axios@0.18.0/dist/axios.min.js"></script>
    <script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
    <title>long-polling</title>
</head>
<body>
<div id="app">
    <button @click="longPolling">http 長輪詢</button>
    <button @click="stopPolling">中止輪詢</button>
    <p>{{time}}</p>
</div>
<script> window.onload=function(){ let vm=new Vue({ el:'#app', data:{ time: '', timer: null }, methods: { stopPolling() { this.timer = null }, longPolling() { if(!this.timer){ this.timer = true this.getTime() } }, getTime(){ window.axios.get('/longPolling', {timeout: 5000}).then(res => { this.time = res.data this.timer && this.getTime() }).catch(err => { console.log(err) this.timer && this.getTime() }) } } }); }; </script>
</body>
</html>

複製代碼

2.server.js

// server.js
const port = 8001
let path = require('path');
let express = require('express'), //引入express模塊
   app = express(),
   server = require('http').createServer(app);
app.use(express.static(path.join(__dirname, 'static'))); //指定靜態HTML文件的位置
app.get('/longPolling',function(req,res){
    setTimeout(_ => {
        res.end(new Date().toLocaleString());
    }, 1000)
});
server.listen(port);
server.setTimeout(0);   //設置不超時,因此服務端不會主動關閉鏈接
console.log('server started', 'http://127.0.0.1:' + port);
複製代碼

3.效果圖

實例效果圖

長鏈接

長鏈接示意圖

iframe流(streaming)

iframe流方式是在頁面中插入一個隱藏的iframe,利用其src屬性在服務器和客戶端之間建立一條長鏈接,服務器向iframe傳輸數據(一般是HTML,內有負責插入信息的javascript),來實時更新頁面。

  • 優勢:消息可以實時到達;瀏覽器兼容好
  • 缺點:服務器維護一個長鏈接會增長開銷;非動態設置iframe.srec時IE、chrome、Firefox會顯示加載沒有完成,圖標會不停旋轉,見下面兩圖

實例

1.index.html

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>longConnection</title>
</head>
<body>
<div>
    <button onclick="longConnection()">http 長鏈接</button>
    <button onclick="stopLongConnection()">關閉長鏈接</button>
    <p id="longConnection"></p>
    <iframe id="iframe" src="" style="display:none"></iframe>
</div>
<script> var iframe = document.getElementById('iframe') function longConnection() { iframe.src='/longConnection2' console.log(iframe) } function stopLongConnection() { iframe.src='/' } </script>
</body>
</html>
複製代碼

2.server.js

// server.js
const port = 8001
let path = require('path');
let express = require('express'), //引入express模塊
   app = express(),
   server = require('http').createServer(app);
app.use(express.static(path.join(__dirname, 'static'))); //指定靜態HTML文件的位置
app.get('/longConnection2',function(req,res){
    let count = 0
    let longConnectionTimer = null
    clearInterval(longConnectionTimer)
    longConnectionTimer = setInterval(_ => {
        if (res.socket._handle) {
            console.log('longConnection2-' + count++)
            let date = new Date().toLocaleString()
            res.write(` <script type="text/javascript"> parent.document.getElementById('longConnection').innerHTML = "${date}";//改變父窗口dom元素 </script> `)
        } else {
            console.log('longConnection2-stop')
            clearInterval(longConnectionTimer)
            longConnectionTimer = null
        }
    }, 1000)
});
server.listen(port);
server.setTimeout(0);   //設置不超時,因此服務端不會主動關閉鏈接
console.log('server started', 'http://127.0.0.1:' + port);
複製代碼

3.效果圖

實例效果圖1
實例效果圖2

事件流 EventSource(SSE - Server-Sent Events,不能算做歷史技術,屬於H5範圍)

EventSource的官方名稱應該是Server-sent events (SSE)服務端派發事件,EventSource 基於http協議只是簡單的單項通訊,實現了服務端推的過程客戶端沒法經過EventSource向服務端發送數據。雖然不能實現雙向通訊可是在功能設計上他也有一些優勢好比能夠自動重鏈接,event-IDs,以及發送隨機事件的能力(WebSocket要藉助第三方庫好比socket.io能夠實現重連。) 實例

1.index.html

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="https://unpkg.com/vue@2.6.10/dist/vue.min.js"></script>
    <title>polling</title>
</head>
<body>
<div id="app">
    <button @click="longConnection">http 長鏈接</button>
    <button @click="stopLongConnection">關閉長鏈接</button>
    <p>{{time}}</p>
</div>
<script> window.onload=function(){ let vm=new Vue({ el:'#app', data:{ time: '', eventSource: null }, methods: { stopLongConnection() { this.close() }, longConnection() { this.getTime() }, getTime(){ // 實例化 EventSource 對象,並指定一個 URL 地址 this.eventSource = new EventSource('/longConnection'); // 使用 addEventListener() 方法監聽事件 console.log("當前狀態0", this.eventSource.readyState); this.eventSource.onopen = this.onopen this.eventSource.onmessage = this.onmessage this.eventSource.onerror = this.onerror }, onopen(){ console.log("連接成功."); console.log("當前狀態1", this.eventSource.readyState); }, onmessage(res){ this.time = res.data }, onerror(err){ console.log(err) }, close(){ this.eventSource && this.eventSource.close() console.log("當前狀態2", this.eventSource.readyState); } } }); }; </script>
</body>
</html>
複製代碼

2.server.js

// server.js
const port = 8001
let path = require('path');
let express = require('express'), //引入express模塊
   app = express(),
   server = require('http').createServer(app);
app.use(express.static(path.join(__dirname, 'static'))); //指定靜態HTML文件的位置
app.get('/longConnection',function(req,res){
    let count = 0
    let longConnectionTimer = null
    clearInterval(longConnectionTimer)
    res.writeHead(200, {
        'Content-Type': "text/event-stream",
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    })
    longConnectionTimer = setInterval(_ => {
        if(res.socket._handle){
            console.log('longConnection-' + count++)
            const data = { timeStamp: Date.now() };
            res.write(`data: ${new Date().toLocaleString()}\n\n`);
        } else {
            console.log('longConnection-stop')
            clearInterval(longConnectionTimer)
            longConnectionTimer = null
            res.end('stop');
        }
    }, 1000)
});
server.listen(port);
server.setTimeout(0);   //設置不超時,因此服務端不會主動關閉鏈接
console.log('server started', 'http://127.0.0.1:' + port);
複製代碼

3.效果圖

實例效果圖

有什麼用: 由於受單項通訊的限制EventSource很是適應於後端數據更新頻繁且對實時性要求較高而又不須要客戶端向服務端通訊的場景下。好比來實現像股票報價、新聞推送、實時天氣這些只須要服務器發送消息給客戶端場景中。EventSource的使用更加便捷這也是他的優勢。

EventSource的應用,webpack-hot-middleware原理

  • 優勢
    1. 基於現有http協議,實現簡單
    2. 斷開後自動重聯,並可設置重聯超時
    3. 派發任意事件
    4. 跨域並有相應的安全過濾
  • 缺點
    1. 只能單向通訊,服務器端向客戶端推送事件
    2. 事件流協議只能傳輸UTF-8數據,不支持二進制流。
    3. 兼容性不高,IE 和 Edge 下目前全部不支持EventSource
    4. 服務器端須要保持 HTTP 鏈接,消耗必定的資源

EventSource實例的readyState屬性,代表鏈接的當前狀態。該屬性只讀,能夠取如下值。

  • 0:至關於常量EventSource.CONNECTING,表示鏈接還未創建,或者斷線正在重連。
  • 1:至關於常量EventSource.OPEN,表示鏈接已經創建,能夠接受數據。
  • 2:至關於常量EventSource.CLOSED,表示鏈接已斷,且不會重連。

注意:

  1. EventSource是一種服務端推送技術。
  2. 通常來講,網頁都是經過發送請求從服務端獲取數據,而服務端推送技術 使服務器隨時能夠向客戶端發送數據。
  3. EventSource基於http長連接
    • 客戶端須要建立一個EventSource對象,服務端URI爲參數
    • 服務端返回的響應報文的Content-Type須爲text/event-stream。

Flash Socket

在頁面中內嵌入一個使用了Socket類的Flash程序JavaScript經過調用此Flash程序提供的Socket接口與服務器端的Socket接口進行通訊,JavaScript在收到服務器端傳送的信息後控制頁面的顯示。

  • 優勢:實現真正的即時通訊,而不是僞即時。
  • 缺點:客戶端必須安裝Flash插件;非HTTP協議,沒法自動穿越防火牆。
  • 實例:網絡互動遊戲。

==Flash 不懂也不說太多了,再多說都是瞎編了==

以上demo源碼地址:github.com/liliuzhu/pe…

WebSocket

WebSocket是HTML5開始提供的一種在單個TCP鏈接上進行全雙工通信的協議。

WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。

在WebSocket API中,瀏覽器和服務器只須要作一個握手的動做,而後,瀏覽器和服務器之間就造成了一條快速通道。二者之間就直接能夠數據互相傳送。

HTML5 定義的 WebSocket協議,能更好的節省服務器資源和帶寬,而且可以更實時地進行通信;解決了輪詢以及其餘長鏈接的不少缺點。

對比示意圖

如何使用 WebSocket

// WebSocket的客戶端原生api
var Socket = new WebSocket('ws://localhost:8080') // WebSocket 對象做爲一個構造函數,用於新建 WebSocket 實例。
Socket.onopen = function(){} // 鏈接創建時觸發
Socket.onclose = function(){}  // 鏈接關閉時觸發
Socket.onmessage = function(){} // 客戶端接收服務端數據時觸發
Socket.send('data') // 實例對象的send()方法用於向服務器發送數據
Socket.close() // 關閉鏈接
socket.onerror = function(){} // 通訊發生錯誤時觸發
複製代碼

Socket.readyState 表示鏈接狀態,能夠是如下值

  • 0 - 表示鏈接還沒有創建。
  • 1 - 表示鏈接已創建,能夠進行通訊。
  • 2 - 表示鏈接正在進行關閉。
  • 3 - 表示鏈接已經關閉或者鏈接不能打開。

注意:
Websocket 使用ws或wss的統一資源標誌符,相似於HTTPS,其中wss表示在TLS之上的Websocket

Websocket 使用和HTTP相同的TCP端口,能夠繞過大多數防火牆的限制。默認狀況下,Websocket 協議使用 80 端口;運行在 TLS 之上時,默認使用 443 端口。

雖然 WebSocketServer 可使用別的端口,可是統一端口仍是更優的選擇

// 服務器數據多是文本,也多是二進制數據(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);
};

// 發送 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);

// 實例對象的bufferedAmount屬性,表示還有多少字節的二進制數據沒有發送出去。它能夠用來判斷髮送是否結束。
var data = new ArrayBuffer(10000000);
socket.send(data);
if (socket.bufferedAmount === 0) {
  // 發送完畢
} else {
  // 發送還沒結束
}
複製代碼

WebSocket & EventSource 的區別

EventSource和WebSocket同樣都是HTML5中的新技術,不過二者在定位上有很大的差異。

  1. WebSocket基於TCP協議,EventSource基於http協議。
  2. EventSource是單向通訊,而websocket是雙向通訊。
  3. EventSource只能發送文本,而websocket支持發送二進制數據。
  4. 在實現上EventSource比websocket更簡單。
  5. EventSource有自動重鏈接(不借助第三方)以及發送隨機事件的能力。
  6. websocket的資源佔用過大EventSource更輕量。
  7. websocket能夠跨域,EventSource基於http跨域須要服務端設置請求頭。

WebSocket 協議本質上是一個基於 TCP 的協議。

爲了創建一個 WebSocket鏈接,客戶端瀏覽器首先要向服務器發起一個HTTP請求,這個請求和一般的HTTP請求不一樣,包含了一些附加頭信息,其中附加頭信息"Upgrade:WebSocket"代表這是一個申請協議升級的 HTTP 請求,服務器端解析這些附加的頭信息而後產生應答信息返回給客戶端,客戶端和服務器端的 WebSocket 鏈接就創建起來了,雙方就能夠經過這個鏈接通道自由的傳遞信息,而且這個鏈接會持續存在直到客戶端或者服務器端的某一方主動的關閉鏈接。

Websocket 實際上是一個新協議,跟HTTP協議基本沒有關係,只是爲了兼容現有瀏覽器的握手規範而已,也就是說它是 HTTP 協議上的一種補充。

說明示意圖

廢話不說上案例

demo效果圖
以上demo源碼地址: github.com/liliuzhu/pe…

Web 實時推送技術的比較

方式 類型 技術實現 優勢 缺點 適用場景
輪詢Polling client⇌server 客戶端循環請求 一、實現簡單 二、 支持跨域 一、浪費帶寬和服務器資源 二、 一次請求信息大半是無用(完整http頭信息) 三、有延遲 四、大部分無效請求 適於小型應用
長輪詢Long-Polling client⇌server 服務器hold住鏈接,一直到有數據或者超時才返回,減小重複請求次數 一、實現簡單 二、不會頻繁發請求 三、節省流量 四、延遲低 一、服務器hold住鏈接,會消耗資源 二、一次請求信息大半是無用 WebQQ、Hi網頁版、Facebook IM
長鏈接iframe server⇌client 在頁面裏嵌入一個隱蔵iframe,將這個 iframe 的 src 屬性設爲對一個長鏈接的請求,服務器端就能源源不斷地往客戶端輸入數據。 一、數據實時送達 二、不發無用請求,一次連接,屢次「推送」 一、服務器增長開銷 二、沒法準確知道鏈接狀態 三、IE、chrome等一直會處於loading狀態 Gmail聊天
EventSource server→client new EventSource() 一、基於現有http協議,實現簡單二、斷開後自動重聯,並可設置重聯超時三、派發任意事件四、跨域並有相應的安全過濾 一、只能單向通訊,服務器端向客戶端推送事件二、事件流協議只能傳輸UTF-8數據,不支持二進制流。四、兼容性不高,IE 和 Edge下目前全部不支持EventSource服務器端須要保持 HTTP 鏈接,消耗必定的資源 股票報價、新聞推送、實時天氣
WebSocket server⇌client new WebSocket() 一、支持雙向通訊,實時性更強 二、可發送二進制文件三、減小通訊量 一、瀏覽器支持程度不一致 二、不支持斷開重連 網絡遊戲、銀行交互和支付

綜上所述:Websocket協議不只解決了HTTP協議中服務端的被動性,即通訊只能由客戶端發起,也解決了數據同步有延遲的問題,同時還帶來了明顯的性能優點,因此websocket是Web 實時推送技術的比較理想的方案,但若是要兼容低版本瀏覽器,能夠考慮用輪詢來實現。

服務端的WebSocket

npm上有不少包對websocket作了實現好比 socket.io、WebSocket-Node、ws、nodejs-websocket還有不少

  1. Socket.io: Socket.io是一個WebSocket庫,包括了客戶端的js和服務器端的nodejs,它會自動根據瀏覽器從WebSocket、AJAX長輪詢、Iframe流等等各類方式中選擇最佳的方式來實現網絡實時應用(不支持WebSocket的狀況會降級到AJAX輪詢),很是方便和人性化,兼容性很是好,支持的瀏覽器最低達IE5.5。屏蔽了細節差別和兼容性問題,實現了跨瀏覽器/跨設備進行雙向數據通訊。

  2. ws: 不像 socket.io 模塊,ws是一個單純的websocket模塊,不提供向上兼容,不須要在客戶端掛額外的js文件。在客戶端不須要使用二次封裝的api使用瀏覽器的原生Websocket API便可通訊。

參考文獻

  1. blog.csdn.net/yhb241/arti…
  2. www.tuicool.com/articles/FF…
  3. developer.mozilla.org/zh-CN/docs/…
  4. www.jianshu.com/p/958eba34a…
  5. www.runoob.com/html/html5-…

本文首發於我的技術博客 liliuzhu.gitee.io/blog

相關文章
相關標籤/搜索