先後端消息推送

在瀏覽某些網頁的時候,例如 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元素內,這個過程不須要客戶端進行任何的請求操做。

相關文章
相關標籤/搜索