WebSocket技术分享

3,129 阅读13分钟

在正式介绍WebSocket之前先跟大家科普一下以及讨论一下过去是如何实现Web双向通信的

科普一下通讯传输模式

  • 单工:只支持数据在一个方向上传输;例如:BP机
  • 半双工:允许数据在两个方向上传输,但是某一时刻只允许数据在一个方向上传输;例如:对讲机, 电报机
  • 全双工:同时在两个方向上传输,是两个单工通信的结合,要求发送设备和接收设备同时具有独立的接收和发送能力。 例如:手机

历史回顾

历史回顾示意图

HTTP 协议有一个缺陷:通信只能由客户端发起。举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。 在WebSocket协议之前,有三种实现双向通信的方式:轮询(polling)、长轮询(long-polling)和iframe流(streaming)。

轮询(polling)

轮询示意图
轮询示意图

轮询是客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。其缺点也很明显:连接数会很多,一个接受,一个发送。而且 每次发送请求都会有Http的Header,会很耗流量,也会消耗CPU的利用率 。

  • 优点:实现简单,无需做过多的更改
  • 缺点:轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担

实例

1.index.html

<!--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.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('/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.效果图

实例效果图

长轮询(long-polling)

长轮询示意图

长轮询是对轮询的改进版,客户端发送HTTP给服务器之后,看有没有新消息,如果没有新消息,就一直等待。当有新消息的时候,才会返回给客户端。在某种程度上减小了网络带宽和CPU利用率等问题。由于http数据包的头部数据量往往很大(通常有400多个字节),但是真正被服务器需要的数据却很少(有时只有10个字节左右),这样的数据包在网络上周期性的传输,难免 对网络带宽是一种浪费 。

  • 优点:比 Polling 做了优化,有较好的时效性
  • 缺点:保持连接会消耗资源; 服务器没有返回有效数据,程序超时。

实例

1.index.html

<!--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 客户端循环请求 1、实现简单 2、 支持跨域 1、浪费带宽和服务器资源 2、 一次请求信息大半是无用(完整http头信息) 3、有延迟 4、大部分无效请求 适于小型应用
长轮询Long-Polling client⇌server 服务器hold住连接,一直到有数据或者超时才返回,减少重复请求次数 1、实现简单 2、不会频繁发请求 3、节省流量 4、延迟低 1、服务器hold住连接,会消耗资源 2、一次请求信息大半是无用 WebQQ、Hi网页版、Facebook IM
长连接iframe server⇌client 在页面里嵌入一个隐蔵iframe,将这个 iframe 的 src 属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。 1、数据实时送达 2、不发无用请求,一次链接,多次“推送” 1、服务器增加开销 2、无法准确知道连接状态 3、IE、chrome等一直会处于loading状态 Gmail聊天
EventSource server→client new EventSource() 1、基于现有http协议,实现简单2、断开后自动重联,并可设置重联超时3、派发任意事件4、跨域并有相应的安全过滤 1、只能单向通信,服务器端向客户端推送事件2、事件流协议只能传输UTF-8数据,不支持二进制流。4、兼容性不高,IE 和 Edge下目前所有不支持EventSource服务器端需要保持 HTTP 连接,消耗一定的资源 股票报价、新闻推送、实时天气
WebSocket server⇌client new WebSocket() 1、支持双向通信,实时性更强 2、可发送二进制文件3、减少通信量 1、浏览器支持程度不一致 2、不支持断开重连 网络游戏、银行交互和支付

综上所述: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