在正式介绍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 协议上的一种补充。
废话不说上案例
方式 | 类型 | 技术实现 | 优势 | 缺点 | 适用场景 |
---|---|---|---|---|---|
轮询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