在正式介紹WebSocket以前先跟你們科普一下以及討論一下過去是如何實現Web雙向通訊的javascript
HTTP 協議有一個缺陷:通訊只能由客戶端發起。舉例來講,咱們想了解今天的天氣,只能是客戶端向服務器發出請求,服務器返回查詢結果。HTTP 協議作不到服務器主動向客戶端推送信息。這種單向請求的特色,註定了若是服務器有連續的狀態變化,客戶端要獲知就很是麻煩。 在WebSocket協議以前,有三種實現雙向通訊的方式:輪詢(polling)、長輪詢(long-polling)和iframe流(streaming)。html
輪詢是客戶端和服務器之間會一直進行鏈接,每隔一段時間就詢問一次。其缺點也很明顯:鏈接數會不少,一個接受,一個發送。並且 每次發送請求都會有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
長輪詢是對輪詢的改進版,客戶端發送HTTP給服務器以後,看有沒有新消息,若是沒有新消息,就一直等待。當有新消息的時候,纔會返回給客戶端。在某種程度上減少了網絡帶寬和CPU利用率等問題。因爲http數據包的頭部數據量每每很大(一般有400多個字節),可是真正被服務器須要的數據卻不多(有時只有10個字節左右),這樣的數據包在網絡上週期性的傳輸,不免 對網絡帶寬是一種浪費 。ios
實例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流方式是在頁面中插入一個隱藏的iframe,利用其src屬性在服務器和客戶端之間建立一條長鏈接,服務器向iframe傳輸數據(一般是HTML,內有負責插入信息的javascript),來實時更新頁面。
實例
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.效果圖
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原理
EventSource實例的readyState屬性,代表鏈接的當前狀態。該屬性只讀,能夠取如下值。
注意:
在頁面中內嵌入一個使用了Socket類的Flash程序JavaScript經過調用此Flash程序提供的Socket接口與服務器端的Socket接口進行通訊,JavaScript在收到服務器端傳送的信息後控制頁面的顯示。
==Flash 不懂也不說太多了,再多說都是瞎編了==
以上demo源碼地址:github.com/liliuzhu/pe…
WebSocket是HTML5開始提供的一種在單個TCP鏈接上進行全雙工通信的協議。
WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。
在WebSocket API中,瀏覽器和服務器只須要作一個握手的動做,而後,瀏覽器和服務器之間就造成了一條快速通道。二者之間就直接能夠數據互相傳送。
HTML5 定義的 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 表示鏈接狀態,能夠是如下值
注意:
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 { // 發送還沒結束 } 複製代碼
EventSource和WebSocket同樣都是HTML5中的新技術,不過二者在定位上有很大的差異。
WebSocket 協議本質上是一個基於 TCP 的協議。
爲了創建一個 WebSocket鏈接,客戶端瀏覽器首先要向服務器發起一個HTTP請求,這個請求和一般的HTTP請求不一樣,包含了一些附加頭信息,其中附加頭信息"Upgrade:WebSocket"代表這是一個申請協議升級的 HTTP 請求,服務器端解析這些附加的頭信息而後產生應答信息返回給客戶端,客戶端和服務器端的 WebSocket 鏈接就創建起來了,雙方就能夠經過這個鏈接通道自由的傳遞信息,而且這個鏈接會持續存在直到客戶端或者服務器端的某一方主動的關閉鏈接。
Websocket 實際上是一個新協議,跟HTTP協議基本沒有關係,只是爲了兼容現有瀏覽器的握手規範而已,也就是說它是 HTTP 協議上的一種補充。
廢話不說上案例
以上demo源碼地址: github.com/liliuzhu/pe…方式 | 類型 | 技術實現 | 優勢 | 缺點 | 適用場景 |
---|---|---|---|---|---|
輪詢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 實時推送技術的比較理想的方案,但若是要兼容低版本瀏覽器,能夠考慮用輪詢來實現。
npm上有不少包對websocket作了實現好比 socket.io、WebSocket-Node、ws、nodejs-websocket還有不少
Socket.io: Socket.io是一個WebSocket庫,包括了客戶端的js和服務器端的nodejs,它會自動根據瀏覽器從WebSocket、AJAX長輪詢、Iframe流等等各類方式中選擇最佳的方式來實現網絡實時應用(不支持WebSocket的狀況會降級到AJAX輪詢),很是方便和人性化,兼容性很是好,支持的瀏覽器最低達IE5.5。屏蔽了細節差別和兼容性問題,實現了跨瀏覽器/跨設備進行雙向數據通訊。
ws: 不像 socket.io 模塊,ws是一個單純的websocket模塊,不提供向上兼容,不須要在客戶端掛額外的js文件。在客戶端不須要使用二次封裝的api使用瀏覽器的原生Websocket API便可通訊。
本文首發於我的技術博客 liliuzhu.gitee.io/blog