前端日拱一卒D7——WEB即時通訊

DerekZ95發表於2018-07-25

前言

餘為前端菜鳥,感姿勢水平匱乏,難觀前端之大局。遂決定循前端知識之脈絡,以興趣為引,輔以幾分堅持,望於己能解惑致知、於同道能助力一二,豈不美哉。

本系列程式碼及文件均在 此處

常見實現方式

輪詢

最簡單的實現方式,由客戶端定時向服務端請求資料

簡單實現

// 設定定時器,每秒向伺服器請求資料
const xhr = new XMLHttpRequest()
setInterval(() => {
  xhr.open('GET','/test')
  xhr.onreadystatechange = () => {}
  xhr.send()
}, 1000)
複製程式碼

評價

最簡單,但是請求次數太多,每次都要建立連線,對伺服器壓力也很大,大部分時間資料是沒有更新的,浪費頻寬。

長輪詢

服務端接收客戶端請求後暫時掛起,等待資料更新,有資料更新則響應,否則等到達到服務端設定的時間限制後再響應。客戶端接收到響應後會再發出請求,重新建立連線,如此往復。

簡單實現

  • 前端請求
ajax = () => {
  const xhr = new XMLHttpRequest()
  xhr.open('GET', '/test')
  xhr.timeout = 10000
  xhr.onreadystatechange = () => {
    // 此時伺服器已返回資料
    if (xhr.readyState === 4) {
      const content = document.getElementById("message")
      content.innerHTML = `${content.innerHTML}\n${xhr.responseText}`
      // 重新建立連線
      ajax()
    }
  }
  xhr.send()
}
window.onload = () => {
  ajax()
}
複製程式碼
  • 傳送訊息
//
document.getElementById("sub").onclick = () => {
  const xhr = new XMLHttpRequest()
  const text = document.getElementById("text").value
  xhr.open('POST', '/message')
  xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  xhr.send(`message=${text}`)
}
複製程式碼
  • node server
// 使用EventEmitter進行事件監聽
const EventEmitter = require('events').EventEmitter
const messageBus = new EventEmitter()
messageBus.setMaxListeners(100)

app.use(async (ctx) => {
  if (ctx.request.url === '/test') {
    const result = await new Promise((resolve, reject) => {
      // 監聽message,長輪詢返回
      messageBus.on('message', function (data) {
        resolve(data)
      })
    })
    ctx.body = result
  }
  // 接收到message,觸發事件
  if (ctx.request.url === '/message') {
    messageBus.emit('message', ctx.request.body.message)
    ctx.body = 'done'
  }
})
複製程式碼
  • 成果圖

前端日拱一卒D7——WEB即時通訊

評價

減少了請求次數,但服務端掛起依然是資源浪費。輪詢與長輪詢都是服務被動型,都是由客戶端發起請求。

具體程式碼見 github

長連線

SSE(Server-Sent Events)是H5新增的功能,允許服務端主動向客戶端推送資料。

簡單實現

  • 客戶端
// 客戶端會在連線失敗後預設重連
const source = new EventSource('/sse')
// 預設為message,這裡的test1為自定義
source.addEventListener('test1', (res) => {
  console.log(res)
}, false)
source.onopen = () => {
  console.log('open sse')
}
source.onerror = (err) => {
  console.log(err)
}
// source.close(); // 用於關閉連線
複製程式碼
  • node server
const Readable = require('stream').Readable

// 建立自定義流
function RR() {}
RR.prototype = Object.create(new Readable());
RR.prototype._read = function (data) {}

if (ctx.request.url === '/sse') {
    // 設定響應頭
    ctx.set({
      // 型別必須為event-stream
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    })

    let stream = new RR()
    let count = 1
    stream.push(`event: test1\ndata: ${JSON.stringify({ count: count })}\n\n`)

    // 返回的訊息格式有要求,這裡返回流是因為koa特殊
    // 如果不是流會呼叫res.end(buffer),結束HTTP響應
    ctx.body = stream

    // 多次主動響應,共用一個連線
    const timer = setInterval(() => {
      stream.push(`event: test1\ndata: ${JSON.stringify({ count: ++count })}\n\n`)
      if (count > 5) {
        clearInterval(timer)
      }
      ctx.body = stream
    }, 2000)
  }
複製程式碼

返回的訊息格式應包含這幾個欄位

id: 1 // 事件id
event: test1 // 自定義事件,不設定則預設為message
data: {count: 1} // 資料
retry : 10000 // 重連時間
複製程式碼
  • 成果圖

前端日拱一卒D7——WEB即時通訊

評價

與前兩者一樣基於HTTP協議,相比於長輪詢,不需要客戶端後續請求,只需要維持一個請求,後續服務端主動推送,且實現也比較簡單。

具體程式碼見 github

webSocket

webSocket是有別於HTTP的一種新協議,誕生已有十年之久。webSocket握手階段採用HTTP協議,沒有同源限制,識別符號為ws

簡單實現

  • 客戶端
// 原生寫法
const ws = new WebSocket('ws://127.0.0.1:5001')
ws.readyState 0 正在連線 1 已連線 2 正在關閉 3 已關閉
ws.onopen = (evt) => {
  console.log('opened')
  ws.send('hello from client')
}
ws.onmessage = (evt) => {
  console.log(`from server: ${evt.data}`)
  ws.close()
}
// socket.io-client
// 服務端用的socket.io,客戶端不用相應的client會有問題
const ws = io('ws://127.0.0.1:5001');
ws.on('connect', (evt) => {
  console.log('opened')
  ws.send('hello from client')
})
ws.on('message', (evt) => {
  console.log(`from server: ${evt}`)
  ws.close()
});
ws.on('disconnect', () => { });
複製程式碼
  • node server
// with koa
const server = require('http').createServer(app.callback())
const io = require('socket.io')(server)
io.on('connection', (socket) => {
  console.log('connected')
  socket.on('message', (msg) => {
    console.log(msg)
    io.emit('message', 'hello from server');
  });
});
server.listen('5001')
複製程式碼

評價

使用起來非常簡單,在單個TCP連線上實現客戶端和服務端之間的全雙工通訊,效能在幾者中最好,後續想寫聊天室玩的時候再來搞搞。

總結

web即時通訊其實要解決的一個是效能問題,一個是效率問題。效能上像長輪詢和短輪詢都是比較差的,效率我理解體現在實時性和主動性上。長連線和websocket都可以實現服務端主動推送,websocket實現的是雙方你來我往的雙工通訊,更適用於即時通訊的場景。具體做這方面東西肯定會碰到一些坑的,這裡淺嘗輒止,以後有機會接觸再做深入。

雖發表於此,卻畢竟為一人之言,又是每日學有所得之筆記,內容未必詳實,看官老爺們還望海涵。

相關文章