一、纯前端
可能有很多的同学有用 setinterval 控制 ajax 不断向服务端请求最新数据的经历看下面的代码:
setinterval(function() { $.get('/get/data-list', function(data, status) { console.log(data) }) }, 5000)
这样每隔5秒前端会向后台请求一次数据,实现上看起来很简单但是有个很重要的问题,就是我们没办法控制网速的稳定,不能保证在下次发请求的时候上一次的请求结果已经顺利返回,这样势必会有隐患,有聪明的同学马上会想到用 settimeout 配合递归看下面的代码:
function poll() { settimeout(function() { $.get('/get/data-list', function(data, status) { console.log(data) poll() }) }, 5000) }
当结果返回之后再延时触发下一次的请求,这样虽然没办法保证两次请求之间的间隔时间完全一致但是至少可以保证数据返回的节奏是稳定的,看似已经实现了需求但是这么搞我们先不去管他的性能就代码结构也算不上优雅,为了解决这个问题可以让服务端长时间和客户端保持连接进行数据互通h5新增了 websocket 和 eventsource 用来实现长轮询,下面我们来分析一下这两者的特点以及使用场景。
二、服务器端
websocket
是什么: websocket是一种通讯手段,基于tcp协议,默认端口也是80和443,协议标识符是ws(加密为wss),它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据,不受跨域的限制(需后端配合)。
有什么用: websocket用来解决http不能持久连接的问题,因为可以双向通信所以可以用来实现聊天室,以及其他由服务端主动推送的功能例如 实时天气、股票报价、余票显示、消息通知等。
eventsource
是什么: eventsource的官方名称应该是 server-sent events(缩写sse)服务端派发事件,eventsource 基于http协议只是简单的单项通信,实现了服务端推的过程客户端无法通过eventsource向服务端发送数据。喜闻乐见的是ie并没有良好的兼容当然也有解决的办法比如 npm install event-source-polyfill
。虽然不能实现双向通信但是在功能设计上他也有一些优点比如可以自动重连接,event ids,以及发送随机事件的能力(websocket要借助第三方库比如socket.io可以实现重连。)
有什么用: 因为受单项通信的限制eventsource只能用来实现像股票报价、新闻推送、实时天气这些只需要服务器发送消息给客户端场景中。eventsource的使用更加便捷这也是他的优点。
websocket & eventsource 的区别
- websocket基于tcp协议,eventsource基于http协议。
- eventsource是单向通信,而websocket是双向通信。
- eventsource只能发送文本,而websocket支持发送二进制数据。
- 在实现上eventsource比websocket更简单。
- eventsource有自动重连接(不借助第三方)以及发送随机事件的能力。
- websocket的资源占用过大eventsource更轻量。
- websocket可以跨域,eventsource基于http跨域需要服务端设置请求头。
eventsource的实现案例
不知道大家有没有见过 content-type:text/event-stream
的请求头,这是 html5
中的 eventsource
是一项强大的 api
,通过服务器推送实现实时通信。
客户端代码
// 实例化 eventsource 参数是服务端监听的路由 var source = new eventsource('/eventsource-test') source.onopen = function (event) { // 与服务器连接成功回调 console.log('成功与服务器连接') } // 监听从服务器发送来的所有没有指定事件类型的消息(没有event字段的消息) source.onmessage = function (event) { // 监听未命名事件 console.log('未命名事件', event.data) } source.onerror = function (error) { // 监听错误 console.log('错误') } // 监听指定类型的事件(可以监听多个) source.addeventlistener("myeve", function (event) { console.log("myeve", event.data) })
服务端代码(node.js)
const fs = require('fs') const express = require('express') // npm install express const app = express() // 启动一个简易的本地server返回index.html app.get('/', (req, res) => { fs.stat('./index.html', (err, stats) => { if (!err && stats.isfile()) { res.writehead(200) fs.createreadstream('./index.html').pipe(res) } else { res.writehead(404) res.end('404 not found') } }) }) // 监听eventsource-test路由服务端返回事件流 app.get('/eventsource-test', (ewq, res) => { // 根据 eventsource 规范设置报头 res.writehead(200, { "content-type": "text/event-stream", // 规定把报头设置为 text/event-stream "cache-control": "no-cache" // 设置不对页面进行缓存 }) // 用write返回事件流,事件流仅仅是一个简单的文本数据流,每条消息以一个空行(\n)作为分割。 res.write(':注释' '\n\n') // 注释行 res.write('data:' '消息内容1' '\n\n') // 未命名事件 res.write( // 命名事件 'event: myeve' '\n' 'data:' '消息内容2' '\n' 'retry:' '2000' '\n' 'id:' '12345' '\n\n' ) setinterval(() => { // 定时事件 res.write('data:' '定时消息' '\n\n') }, 2000) }) // 监听 6788 app.listen(6788, () => { console.log(`server runing on port 6788 ...`) })
客户端访问 http://127.0.0.1:6788/
会看到如下的输出:
来总结一下相关的api,客户端的api很简单都在注释里了,服务端有一些要注意的地方:
事件流格式?
事件流仅仅是一个简单的文本数据流,文本应该使用utf-8格式的编码。每条消息后面都由一个空行作为分隔符。以冒号开头的行为注释行,会被忽略。
注释有何用?
注释行可以用来防止连接超时,服务器可以定期发送一条消息注释行,以保持连接不断。
eventsource规范中规定了那些字段?
event:
事件类型,如果指定了该字段,则在客户端接收到该条消息时,会在当前的eventsource对象上触发一个事件,事件类型就是该字段的字段值,你可以使用addeventlistener()方法在当前eventsource对象上监听任意类型的命名事件,如果该条消息没有event字段,则会触发onmessage属性上的事件处理函数。data:
消息的数据字段,如果该条消息包含多个data字段,则客户端会用换行符把它们连接成一个字符串来作为字段值。id:
事件id,会成为当前eventsource对象的内部属性"最后一个事件id"的属性值。retry:
一个整数值,指定了重新连接的时间(单位为毫秒),如果该字段值不是整数,则会被忽略。
重连是干什么的?
上文提过retry字段是用来指定重连时间的,那为什么要重连呢,我们拿node来说,大家知道node的特点是单线程异步io,单线程就意味着如果server端报错那么服务就会停掉,当然在node开发的过程中会处理这些异常,但是一旦服务停掉了这时就需要用pm2之类的工具去做重启操作,这时候server虽然正常了,但是客户端的eventsource链接还是断开的这时候就用到了重连。
为什么案例中消息要用\n结尾?
\n是换行的转义字符,eventsource规范规定每条消息后面都由一个空行作为分隔符,结尾加一个\n表示一个字段结束,加两个\n表示一条消息结束。(两个\n表示换行之后又加了一个空行)
注: 如果一行文本中不包含冒号,则整行文本会被解析成为字段名,其字段值为空。
websocket的实现案例
websocket的客户端原生api
// websocket 对象作为一个构造函数,用于新建 websocket 实例 var ws = new websocket('ws://localhost:8080') // 用于指定连接成功后的回调函数 ws.onopen = function(){} // 用于指定连接关闭后的回调函数 ws.onclose = function(){} // 用于指定收到服务器数据后的回调函数 ws.onmessage = function(){} // 实例对象的send()方法用于向服务器发送数据 ws.send('data') // 用于指定报错时的回调函数 socket.onerror = function(){}
服务端的websocket如何实现
npm上有很多包对websocket做了实现比如 socket.io、websocket-node、ws、还有很多,本文只对 socket.io以及ws 做简单的分析,细节还请查看官方文档。
socket.io和ws有什么不同
socket.io:
socket.io是一个websocket库,包括了客户端的js和服务器端的nodejs,它会自动根据浏览器从websocket、ajax长轮询、iframe流等等各种方式中选择最佳的方式来实现网络实时应用(不支持websocket的情况会降级到ajax轮询),非常方便和人性化,兼容性非常好,支持的浏览器最低达ie5.5。屏蔽了细节差异和兼容性问题,实现了跨浏览器/跨设备进行双向数据通信。
ws:
不像 socket.io 模块, ws 是一个单纯的websocket模块,不提供向上兼容,不需要在客户端挂额外的js文件。在客户端不需要使用二次封装的api使用浏览器的原生websocket api即可通信。
基于socket.io实现websocket双向通信
客户端代码