数据实时更新解决方案(长轮询以及WebSocket)

前端如何实时获得后端不断更新的数据?最容易实现的短轮询有如下优缺点。优点:开发简单。缺点:无用请求过多并却不能保证数据的实时性。
如果对于数据要求较高,这个时候短轮询就可以pass了。

下面我来介绍2种稍微高大上一点的方法,哈哈哈哈

一. Long Polling长轮询解决方案

什么是长轮询?客户端发起请求后,服务端发现当前没有新的数据,这个时候服务端没有立即返回请求,而是将请求挂起,在等待一段时间后(一般为30s或者是60s),发现还是没有数据更新的话,就返回一个空结果给客户端。客户端在收到服务端的回复后,立即再次向服务端发送新的请求。这次服务端在接收到客户端的请求后,同样等待了一段时间,这次好运的是服务端的数据发生了更新,服务端给客户端返回了最新的数据。客户端在拿到结果后再次发送下一个请求,如此反复。

实现过程如下

//polling.js==>使用node polling.js启动服务器
const http = require('http')
const url = require('url')
const events = []//后端不断变化的数据

let timers = new Set()
let subscribers = new Set()// 当前挂起的请求

const EventProducer = () => {
  const event = {
    id: Date.now(),
    timestamp: Date.now()
  }
  events.push(event)
  
  // 数据发生变化,通知所有挂起的请求,将变化的最新数据返回
  subscribers.forEach(subscriber => {
    subscriber.resp.write(JSON.stringify(events.filter(event => event.timestamp > subscriber.timestamp)))
    subscriber.resp.end()
  })
  // 此时所有挂起的请求已经处理完毕,清空subscribers
  subscribers.clear()
  // 取消请求的超时回调
  timers.forEach(timer => clearTimeout(timer))
  timers.clear()  // 重置timers
}

// 10秒生成一个事件
setInterval(() => {
  EventProducer()
}, 10000)

const server = http.createServer((req, resp) => {
  const urlParsed = url.parse(req.url, true)
  resp.setHeader('Access-Control-Allow-Origin', '*')
  resp.setHeader('Origin', '*')
  if (urlParsed.pathname == '/list') {
    // 发送服务端现存事件
    resp.write(JSON.stringify(events))
    resp.end()
  } else if (urlParsed.pathname == '/subscribe') {
    const timestamp = parseInt(urlParsed.query['timestamp'])
    const subscriber = {
      timestamp,
      resp
    }
    // 新建的连接挂起来
    subscribers.add(subscriber)
    // 30s超时,自动关闭连接
    const timer = setTimeout(() => {
      resp.end()
      timers.delete(timer)
    }, 30000)
    
    // 客户端主动断开连接
    req.on('close', () => {
      subscribers.delete(subscriber)
      clearTimeout(timer)
    })
    timers.add(timer)
  }
})
server.listen(8090, () => {
  console.log('server is up')
})
 const fetchLatestEvents = async (timestamp) => {
        const body = await fetch(`http://localhost:8090/subscribe?timestamp=${timestamp}`)
        if (body.ok) {
            const json = await body.json()
            return json
        } else {
            console.error('failed to fetch')
        }
    }

    const listEvents = async () => {
        const body = await fetch(`http://localhost:8090/list`)
        if (body.ok) {
            const json = await body.json()
            return json
        } else {
            console.error('failed to fetch')
        }
    }
    var timestampRef = { timestamp: 0 }
    var eventsRef = []
    const fetchTask = async () => {
        if (timestampRef.timestamp === 0) {
            // 初次加载
            const currentEvents = await listEvents()
            timestampRef.timestamp = currentEvents[currentEvents.length - 1].timestamp
            eventsRef = [...eventsRef, ...currentEvents]
        }
        //注意latestEvents数据是后端新生成的数据,前端需要自己拼上老数据
        const latestEvents = await fetchLatestEvents(timestampRef.timestamp)
        if (latestEvents && latestEvents.length) {
            timestampRef = latestEvents[latestEvents.length - 1]
            eventsRef = [...eventsRef, ...latestEvents]
        }
    }
  function start() {
        fetchTask()
            .catch(err => {
                console.error(err)
            }).finally(() => {
                // 触发下次加载
                start()
            })
    }
    start()

值得注意的是,这个时候,我们打开浏览器的调试工具可以发现浏览器每一次发出的请求都不会立马收到回复,而是pending一段时间后(大概是10秒)才会有结果,并且结果里面都是有数据的
数据实时更新解决方案(长轮询以及WebSocket)_第1张图片

长轮询优缺点 优点:避免了客户端大量的重复请求。再者客户端在收到服务端的返回后,马上发送下一个请求,这就保证了更好的数据实时性。
缺点:服务端资源大量消耗,服务端会一直hold住客户端的请求,这部分请求会占用服务器的资源。难以处理数据更新频繁的情况:
如果数据更新频繁,会有大量的连接创建和重建过程,这部分消耗很大。

二. WebSocket解决方案

什么是WebSocket? 客户端和服务器之间建立一个持久的长连接,这个连接是双工的,客户端和服务端都可以实时地给对方发送消息。

实现过程如下

// websocket.js   
const WebSocket = require('ws')
const events = []
let latestTimestamp = Date.now()
const clients = new Set()//连接着的socket数组

const EventProducer = () => {
  const event = {
    id: Date.now(),
    timestamp: Date.now()
  }
  events.push(event)
  latestTimestamp = event.timestamp
  
  // 推送给所有连接着的socket
  clients.forEach(client => {
    client.ws.send(JSON.stringify(events.filter(event => event.timestamp > client.timestamp)))
    client.timestamp = latestTimestamp
  })
}

// 每10秒生成一个新的事件
setInterval(() => {
  EventProducer()
}, 10000)

// 启动socket服务器
const wss = new WebSocket.Server({ port: 8080 })
wss.on('connection', (ws, req) => {
  console.log('client connected')
  // 首次连接,推送现存事件
  ws.send(JSON.stringify(events))
  const client = {
    timestamp: latestTimestamp,
    ws,
  }
  clients.add(client)//将连接放入数组
  ws.on('close', _ => {
    clients.delete(client)
  })
})
 var timestampRef={current:0}
    var eventsRef={current:[]}
     const ws = new WebSocket(`ws://localhost:8080/ws?timestamp=${timestampRef.current}`)
    ws.addEventListener('open', e => {
      console.log('successfully connected')
    })
    ws.addEventListener('close', e => {
      console.log('socket close')
    })
    ws.addEventListener('message', (ev) => {
      const latestEvents = JSON.parse(ev.data)
      if (latestEvents && latestEvents.length) {
        timestampRef.current = latestEvents[latestEvents.length - 1].timestamp
         //注意latestEvents数据是后端新生成的数据,前端需要自己拼上老数据
        eventsRef.current = [...eventsRef.current, ...latestEvents]
        console.log(eventsRef)
      }
    })

你会发现客户端和服务端只有一个websocket连接,它们所有的通信都是发生在这个连接上面的
数据实时更新解决方案(长轮询以及WebSocket)_第2张图片
数据实时更新解决方案(长轮询以及WebSocket)_第3张图片

WebSocket优缺点
优点:客户端和服务端建立连接的次数少。消息实时性高。双工通信,服务端和客户端都可以随时给对方发送消息。相对于长轮询适用于服务端数据频繁更新的场景,因为客户端在拿到信息后不需要重新建立连接或者发送请求。
缺点:系统设计较复杂。某些代理层软件(如Nginx)默认配置的长连接时间是有限制的,这个时候客户端需要自动重连,实现也会比较困难。

最后简单介绍一下另外一种方案:Server-Sent Events

什么是SSE?客户端向服务端发起一个持久化的HTTP连接,服务端接收到请求后,会挂起客户端的请求,有新消息时,再通过这个连接将数据推送给客户端。这里需要指出的是和WebSocket长连接不同,SSE的连接是单向的,也就是说它不允许客户端向服务端发送消息。
优点:和webSocket类似,连接数少。数据实时性高。缺点:SSE长连接是单向的,不允许客户端给服务端推送数据。和WebSocket一样会遇到代理层配置的问题

你可能感兴趣的:(js,websocket,前端,node.js)