先后端消息推送

在浏览某些网页的时候,例如 WebQQ、京东在线客服服务、CSDN私信消息等相似的状况下,咱们能够在网页上进行在线聊天,或者即时消息的收取与回复,可见,这种功能的需求由来已久,而且应用普遍。javascript

网上关于这方面的文章也能搜到一大堆,不过基本上都是理论,真正可以运行的代码不多,原理性的东西我就不当搬运工了,本文主要是贴示例代码,最多在代码中穿插一点便于理解,本文主要的示例代码基于 javascript,服务端基于 nodejs 的 koa(1/2)框架实现。html


模拟推送

Web端 常见的消息推送实际上大多数都是模拟推送,之因此是模拟推送,是由于这种实现并非服务器主动推送,本质依旧是客户端发起请求,服务端返回数据,起主动做用的是客户端。前端


短轮询

实现上最简单的一种模拟推送方法,原理就是客户端不断地向服务端发请求,若是服务端数据有更新,服务端就把数据发送回来,客户端就能接收到新数据了。vue

一种实现的示例以下:html5

  1.  
    const loadXMLDoc = (url, callback) => {
  2.  
    let xmlhttp
  3.  
    if(window.XMLHttpRequest) {
  4.  
    // IE7+ Firefox Chrome Safari 等现代浏览器执行的代码
  5.  
    xmlhttp = new XMLHttpRequest()
  6.  
    } else {
  7.  
    // IE5 IE6浏览器等老旧浏览器执行的代码
  8.  
    xmlhttp = new ActiveXObject('Microsoft.XMLHTTP')
  9.  
    }
  10.  
     
  11.  
    xmlhttp. onreadystatechange = () => {
  12.  
    if(xmlhttp.readyState === 4 && xmlhttp.status === 200) {
  13.  
    document.getElementById('box1').innerHTML = xmlhttp.responseText
  14.  
    callback && callback()
  15.  
    }
  16.  
    }
  17.  
    // 打开连接发送请求
  18.  
    xmlhttp.open( 'GET', 'http://127.0.0.1:3000/' + url, true)
  19.  
    xmlhttp.send()
  20.  
    }
  21.  
     
  22.  
    // 轮询
  23.  
    setInterval( function() {
  24.  
    loadXMLDoc( 'fetchMsg')
  25.  
    }, 2000)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

上述代码,设置定时任务,每隔 2s使用 ajax发起一次请求,客户端根据服务端返回的数据来进行决定执行对应的操做,除了发送 ajax,你还可使用 fetchjava

  1.  
    fetch( 'localhost:3000/fetchMsg', {
  2.  
    headers: {
  3.  
    'Accept': 'application/json, text/plain, */*'
  4.  
    }
  5.  
    }
  • 1
  • 2
  • 3
  • 4
  • 5

引伸:fetch目前的浏览器支持度还很低,因此在实际生产环境中使用的时候,最好添加一些 polyfill,一种垫片使用顺序示例以下: 
es5 的 polyfill — es5-shim 
Promise 的 polyfill — es6-promise - IE8+ 
fetch 的 polyfill — fetch - IE10+node

若是你在使用某种框架,例如 vue 或者 angular,那么你一样可使用这些框架自带的请求方法,总之基于页面的友好访问性,在发送请求的同时不要刷新页面就好了。git

这里写图片描述

优势:es6

先后端程序都很容易编写,没什么技术难度github

缺点:

这种方法由于须要对服务器进行持续不断的请求,就算你设置的请求间隔时间很长,但在用户访问量比较大的状况下,也很容易给服务器带来很大的压力,并且绝大部分状况下都是无效请求,浪费带宽和服务器资源,通常不会用于实际生产环境的,本身知道一下就好了。


长轮询

相比于上一种实现,长轮询一样是客户端发起请求,服务端返回数据,只不过不一样的是,在长轮询的状况下,服务器端在接到客户端请求以后,若是发现数据库中的数据并无更新或者不符合要求,那么就不会当即响应客户端,而是 hold住此次请求,直到符合要求的数据到达或者由于超时等缘由才会关闭链接,客户端在接收到新数据或者链接被关闭后,再次发起新的请求。

为了节约资源,一次长轮询的周期时间最好在 10s ~ 25s左右,长链接也是实际生产环境中,被普遍运用于实时通讯的技术。

客户端代码以下:

  1.  
    function getData() {
  2.  
    loadXMLDoc ('holdFetchMsg', ()=>{
  3.  
    getData()
  4.  
    })
  5.  
    }
  6.  
    getData()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

想要在链接断开或发生错误的时候,再次发起请求链接,实现也很简单,如下问使用 fetch 实现示例:

  1.  
    function getData() {
  2.  
    let result = fetch('http://127.0.0.1:3000/holdFetchMsg', {
  3.  
    headers: {
  4.  
    'Accept': 'application/json, text/plain, */*'
  5.  
    }
  6.  
    })
  7.  
    result. then(res => {
  8.  
    return res.text()
  9.  
    }). then(data => {
  10.  
    document.getElementById('box1').innerHTML = data
  11.  
    }). catch(e => {
  12.  
    console.log('Catch Error:', e)
  13.  
    }). then(() => {
  14.  
    getData()
  15.  
    })
  16.  
    }
  17.  
    getData()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

一种较为直观的服务器 hold住链接的实现以下:

  1.  
    router. get('/holdFetchMsg', (ctx, next)=> {
  2.  
    let i = 0
  3.  
    while(true) {
  4.  
    // 这里的条件在实际环境中能够换成是到数据库查询数据的操做
  5.  
    // 若是查询到了符合要求的数据,再 break
  6.  
    // 不过这种可能会致使服务器进行例如疯狂查询数据库的操做,很是不友好
  7.  
    if(++i > 2222222222) {
  8.  
    ctx.body = '作个人狗吧'
  9.  
    break
  10.  
    }
  11.  
    }
  12.  
    })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

还有一种方法,不过这种纯粹是为了 hold住而 hold住,能够做为上一种方法的辅助,解决诸如服务端进行疯狂查询数据库的操做,相似于 Java中的 Thread.sleep()操做

  1.  
    let delay = 2000, i = 0
  2.  
    while(true) {
  3.  
    let startTime = new Date().getTime()
  4.  
    // 这里的条件在实际环境中能够换成是到数据库查询数据的操做
  5.  
    if(++i > 3) {
  6.  
    ctx.body = '作个人狗吧'
  7.  
    break
  8.  
    } else {
  9.  
    // 休息会,别那么频繁地进行诸如查询数据库的操做
  10.  
    // delay 为每次查询后 sleep的时间
  11.  
    while(new Date().getTime() < startTime + delay);
  12.  
    }
  13.  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

若是你如今的 Nodejs版本支持 ES6中的 Generator的话,那么还能够这样(koa1环境, Generator写法):

  1.  
    app.use( function* (next){
  2.  
    let i = 0
  3.  
    const sleep = ms => {
  4.  
    return new Promise(function timer(resolve){
  5.  
    setTimeout (()=>{
  6.  
    if(++i > 3) {
  7.  
    resolve()
  8.  
    } else {
  9.  
    timer(resolve)
  10.  
    }
  11.  
    }, ms)
  12.  
    })
  13.  
    }
  14.  
    yield sleep(2000)
  15.  
    this.body = '作个人狗吧'
  16.  
    })
  17.  
     
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

若是你如今的 Nodejs版本支持 ES7中的 async/await的话,,那么还有一种 hold住链接的方法可供选择(koa2环境):

  1.  
    router.get ('/holdFestchMsg', async(ctx, next) => {
  2.  
    let i = 0
  3.  
    const sleep = ms => {
  4.  
    return new Promise(function timer(resolve) {
  5.  
    setTimeout(async()=>{
  6.  
    // 这里的条件在实际环境中能够换成是到数据库查询数据的操做
  7.  
    if(++i > 3) {
  8.  
    resolve()
  9.  
    } else {
  10.  
    timer(resolve)
  11.  
    }
  12.  
    }, ms)
  13.  
    })
  14.  
    }
  15.  
    await sleep(2000)
  16.  
    ctx.body = '作个人狗吧'
  17.  
    })
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

这里写图片描述

优势:

尽管长轮询不可能作到每一次的响应都是有用的数据,由于服务器超时或者客户端网络环境的变化,以及服务端为了更好的分配资源而自动在一个心跳周期的末尾断掉链接等缘由,而致使长轮询不可能一直存在,必需要不断地进行断开和链接操做,但不管如何,相比于短轮询来讲,长轮询耗费资源明显小了不少

缺点:

服务器 hold链接依旧会消耗很多的资源,特别是当链接数很大的时候,返回数据顺序无保证,难于管理维护。


长链接

这种是基于 iframe 或者 script实现的,主要原理大概就是在主页面中插入一个隐藏的 iframe(script),而后这个 iframe(script)的 src属性指向服务端获取数据的接口,由于是iframe(script)是隐藏的,并且 iframe(script)的 刷新也不会致使 主页面刷新,因此能够为这个 iframe(script)设置一个定时器,让其每隔一段时间就朝服务器发送一次请求,这样就能得到服务端的最新数据了。

先说一下 利用 script的长链接:

前端实现:

  1.  
    <script>
  2.  
    function callback(msg) {
  3.  
    // 获得后端返回的数据
  4.  
    console.log(msg);
  5.  
    }
  6.  
    function createScript() {
  7.  
    let script = document.createElement('script')
  8.  
    script.src = 'http://127.0.0.1:3000/fetchMsg'
  9.  
    document.body.appendChild(script)
  10.  
    document.body.removeChild(script)
  11.  
    }
  12.  
    </script>
  13.  
    <script>
  14.  
    window.onload = function() {
  15.  
    setInterval( ()=>{
  16.  
    createScript()
  17.  
    }, 3000)
  18.  
    }
  19.  
    </script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

后端实现:

  1.  
    router.get ('/fetchMsg', (ctx, next)=> {
  2.  
    ctx.body = 'callback("作个人狗吧")'
  3.  
    })
  • 1
  • 2
  • 3

主要是在前端,一共两条 script脚本,大体左右就是在必定的时间间隔内(示例为 3s)就动态地在页面中增删一个连接为用于请求后端数据的 script脚本。

后端则返回一段字符串,这段字符串在返回前端时,有一个 callback字段调用前端的代码,相似于 jsonp的请求。

注意:修改一个已经执行过的 script脚本的 src属性是没什么卵用的,修改以后,最多在页面的 DOM上发生一些变化,而浏览器既不会发请求,也不会执行脚本,因此这里采用动态增删整个 script标签的作法。

能够看到,这种方法其实与短轮询没什么区别,惟一的区别在于短轮询保证每次请求都能收到响应,但上述示例的长链接不必定每次都能获得响应,若是下一次长链接开始请求,上一次链接还没获得响应,则上一次链接将被终止。

固然,若是你想长链接每次也都能保证获得响应也是能够的,大体作法就是在页面中插入不止一条 script标签,每条标签对应一个请求,等到当前请求到达再决定是否移除当前 script标签。

若是想要获得有序的数据响应,则还能够将 setInterval换成递归调用,例如:

  1.  
    function createScript() {
  2.  
    let script = document.createElement('script')
  3.  
    script.src = 'http://127.0.0.1:3000/fetchMsg'
  4.  
    document.body.appendChild(script)
  5.  
    script. onload = ()=> {
  6.  
    document.body.removeChild(script)
  7.  
    // 约束轮询的频率
  8.  
    setTimeout (()=>{
  9.  
    createScript()
  10.  
    }, 2000)
  11.  
    }
  12.  
    }
  13.  
     
  14.  
    window.onload = function() {
  15.  
    createScript()
  16.  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

使用 iframe的方式与此相似,就不赘述了,不过须要注意的是, iframe可能存在跨域的状况,可能会比 script方式麻烦一些。


WebSocket

WebSoket是 HTML5新增的 API,具体介绍以下(来源w3c菜鸟教程

WebSocket是HTML5开始提供的一种在单个 TCP 链接上进行全双工通信的协议。

在WebSocket API中,浏览器和服务器只须要作一个握手的动做,而后,浏览器和服务器之间就造成了一条快速通道。二者之间就直接能够数据互相传送。

浏览器经过 JavaScript 向服务器发出创建 WebSocket 链接的请求,链接创建之后,客户端和服务器端就能够经过 TCP 链接直接交换数据。

当你获取 Web Socket 链接后,你能够经过 send() 方法来向服务器发送数据,并经过 onmessage 事件来接收服务器返回的数据。

上面所提到的短轮询、长轮询、长链接,本质都是单向通讯,客户端主动发起请求,服务端被动响应请求,但 WebSocket则已是全双工通信了,也就是说不管是客户端仍是服务端都能主动向对方发起响应,服务器具有了真正的 推送能力。

一段简单的 客户端 WebSocket代码示例以下:

  1.  
    function myWebSocket() {
  2.  
    let ws = new WebSocket('ws://localhost:3000')
  3.  
    ws. onopen = ()=> {
  4.  
    console.log('send data')
  5.  
    ws.send( 'client send data')
  6.  
    }
  7.  
     
  8.  
    ws. onmessage = (e)=> {
  9.  
    let receiveMsg = e.data
  10.  
    console.log('client get data')
  11.  
    }
  12.  
     
  13.  
    ws. onerror = (e)=>{
  14.  
    console.log('Catch Error:', e)
  15.  
    }
  16.  
     
  17.  
    ws. onclose = ()=> {
  18.  
    console.log('ws close')
  19.  
    }
  20.  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

想要让客户端的 WebSocket可以链接上服务器,服务端必需要具有可以响应 WebSocket类型的请求才行,通常的服务器是没有自带这种能力的,因此必需要对服务器端程序代码作出些改变。

本身封装服务器端响应 WebSocket的代码可能会涉及到很底层的东西,因此通常都是使用第三方封装好的库,基于nodejs的 WebSocket库有不少,ws 功能简单, API形式更贴近于原生,大名鼎鼎的 socket.io 是与 Nodejs联手开发,功能齐全,被普遍运用于游戏、实时通信等应用。

如下给出一种基于 socket.io 实现 简单客户端和服务端通讯的示例:

客户端:

  1.  
    // HTML
  2.  
    <body>
  3.  
    <ul id="messages"></ul>
  4.  
    <form action="" id="msgForm">
  5.  
    <input id="m" autocomplete="off" /><input type="submit" class="submit" value="submit">
  6.  
    </form>
  7.  
    </body>
  8.  
     
  9.  
    // 引入 socket.io
  10.  
    <script src='/socket.io/socket.io.js'></script>
  11.  
    <script>
  12.  
    function appendEle(parent, childValue, position = 'appendChild') {
  13.  
    let child = document.createElement('li')
  14.  
    child.innerHTML = childValue
  15.  
    parent[position](child)
  16.  
    }
  17.  
     
  18.  
    function socketIO(msgForm, msgBox, msgList) {
  19.  
    const socket = io()
  20.  
    msgForm.addEventListener( 'submit', (e)=>{
  21.  
    e.preventDefault()
  22.  
    socket.emit( 'chat message', msgBox.value)
  23.  
    appendEle(msgList, '<b>Client: </b>' + msgBox.value)
  24.  
    msgBox.value = ''
  25.  
    })
  26.  
     
  27.  
    socket.on( 'chat message', (msg)=>{
  28.  
    appendEle(msgList, msg)
  29.  
    })
  30.  
    }
  31.  
     
  32.  
    window.onload = ()=>{
  33.  
    let msgForm = document.querySelector('#msgForm')
  34.  
    let msgBox = document.querySelector('#m')
  35.  
    let msgList = document.querySelector('#messages')
  36.  
    socketIO(msgForm, msgBox, msgList)
  37.  
    }
  38.  
    </script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

服务端实现:

  1.  
    const app = require('express')()
  2.  
    const http = require('http').Server(app)
  3.  
    const io = require('socket.io')(http)
  4.  
     
  5.  
    app.get ('/', (req, res)=> {
  6.  
    res.sendFile(__dirname + '/index.html')
  7.  
    })
  8.  
     
  9.  
    io.on('connection', socket=>{
  10.  
    console.log('a user connected')
  11.  
    socket.on('disconnect', ()=>{
  12.  
    console.log('user disconnect')
  13.  
    })
  14.  
    socket.on('chat message', (msg)=>{
  15.  
    console.log('clien get message: ', msg)
  16.  
    setTimeout(()=>{
  17.  
    io.emit('chat message', '<b>Server:</b>' + ' Are you Sure? -- Come from your father')
  18.  
    }, 1500)
  19.  
    })
  20.  
    })
  21.  
     
  22.  
    http.listen(3000, ()=> {
  23.  
    console.log('Server running at 3000.')
  24.  
    })
  25.  
     
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

效果以下:

这里写图片描述

注、websocket是javaweb实现即时消息推送最佳方案,可是须要服务器jdk在版本7以上支持,低版本浏览器还不支持,因此要支持低版本即时消息推送还须要选择另一种方法。

使用反向ajax框架DWR

 

DWR(Direct Web RemoTIng)是一个Web远程调用AJAX扩展框架,经过DWR客户端的JavaScript能够直接调用Web服务器上的JavaBean类的方法,解决了原有AJAX应用必需请求HTTP控制组件(如Servlet,Struts的AcTIon等)才能调用服务器端业务类的方法,从而简化了AJAX应用的开发。使用DWR能够不须要编写复杂的控制层组件。

  1.2 DWR反向AJAX技术

  正常状况下,DWR调用服务器端的JavaBean对象方法使用正向请求/响应模式,也称为拉模式(Pull Model),由客户端JavaScript调用JavaBean方法,返回结果经过回调方法更新页面上的HTML元素,实现监控数据的显示。这种正向模式符合通常的管理系统应用,但对监控系统实时性要求较高的应用却力不从心。而反向模式即推模式(Push Model),是适应监控系统的最佳方式,由服务器组件将取得的监控数据推送到Web客户端,不须要客户端主动请求,而是被动接收。于是无需进行Web层进行页面刷新,便可实现数据更新显示。

  最新版本的DWR 2.X增长了反向(Reverse AJAX)功能,经过反向AJAX功能,服务器端的JavaBean对象能够将取得的数据直接推送到指定的客户端页面,写到指定的HTML元素内,这个过程不须要客户端进行任何的请求操做。

相关文章
相关标签/搜索