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

相关文章
相关标签/搜索