封装一个简单的 WebSocket 库

·  阅读 3488

WebSocket 是什么

WebSocket 是一种在客户端与服务器之间保持TCP长连接的网络协议,这样它们就可以随时进行信息交换。提供了一个双向通讯的功能。

WebSocket 解决了什么

WebSocket 解决了传统的 Ajax 只能单向通讯的问题

WebSocket 的使用

【客户端】

  const ws = new WebSocket('ws://localhost:6050', 'echo-protocol')
  ws.onopen = () => {
    this.send('connect success')
  }
  ws.onmessage = (event) => {
    console.log(event, 'event')
  }
复制代码

【服务端】

可以参考 npm version](badge.fury.io/js/websocke…)

WebSocket 的封装

WebSocket 封装的考虑点

  • 以类的方式封装
  • 异常情况下的断开重连、用户手动断开则不重连
  • 消息发送失败的处理,下次连接成功时发送之前失败的内容
  • 订阅消息、取消订阅(需要结合发布订阅者模式)
  • 根据不同的类型,处理不通的消息
  • 销毁(这一步处理的不是很好、各位大佬有什么建议、可以教教小弟)

WebSocket 封装的实现

ws.js

class Ws {
  // 要连接的URL
  url
  // 一个协议字符串或一个协议字符串数组。
  // 这些字符串用来指定子协议,这样一个服务器就可以实现多个WebSocket子协议
  protocols
  // WebSocket 实例
  ws
  // 是否在重连中
  isReconnectionLoading = false
  // 延时重连的 id
  timeId = null
  // 是否是用户手动关闭连接
  isCustomClose = false
  // 错误消息队列
  errorStack = []
  // 消息管理中心
  eventCenter = new EventCenter()

  constructor(url, protocols) {
    this.url = url
    this.protocols = protocols
    this.createWs()
  }

  createWs() {
    if ('WebSocket' in window) {
      // 实例化
      this.ws = new WebSocket(this.url, this.protocols)
      // 监听事件
      this.onopen()
      this.onerror()
      this.onclose()
      this.onmessage()
    } else {
      console.log('你的浏览器不支持 WebSocket')
    }
  }

  // 监听成功
  onopen() {
    this.ws.onopen = () => {
      console.log(this.ws, 'onopen')
      // 发送成功连接之前所发送失败的消息
      this.errorStack.forEach(message => {
        this.send(message)
      })
      this.errorStack = []
      this.isReconnectionLoading = false
    }
  }

  // 监听错误
  onerror() {
    this.ws.onerror = (err) => {
      console.log(err, 'onerror')
      this.reconnection()
      this.isReconnectionLoading = false
    }
  }

  // 监听关闭
  onclose() {
    this.ws.onclose = () => {
      console.log('onclose')

      // 用户手动关闭的不重连
      if (this.isCustomClose) return

      this.reconnection()
      this.isReconnectionLoading = false
    }
  }

  // 接收 WebSocket 消息
  async onmessage() {
    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data)
        this.eventCenter.emit(data.type, data.data)
      } catch (error) {
        console.log(error, 'error')
      }
    }
  }

  // 重连
  reconnection() {
    // 防止重复
    if (this.isReconnectionLoading) return

    this.isReconnectionLoading = true
    clearTimeout(this.timeId)
    this.timeId = setTimeout(() => {
      this.createWs()
    }, 3000)
  }

  // 发送消息
  send(message) {
    // 连接失败时的处理
    if (this.ws.readyState !== 1) {
      this.errorStack.push(message)
      return
    }

    this.ws.send(message)
  }

  // 手动关闭
  close() {
    this.isCustomClose = true
    this.ws.close()
  }

  // 手动开启
  start() {
    this.isCustomClose = false
    this.reconnection()
  }

  // 订阅
  subscribe(eventName, cb) {
    this.eventCenter.on(eventName, cb)
  }

  // 取消订阅
  unsubscribe(eventName, cb) {
    this.eventCenter.off(eventName, cb)
  }

  // 销毁
  destroy() {
    this.close()
    this.ws = null
    this.errorStack = null
    this.eventCenter = null
  }
}
复制代码

事件管理中心(发布订阅者模式)

eventCenter.js

class EventCenter {
  // 通过事件类型作为属性来管理不通的事件回调
  eventStack = {}

  constructor() {
    this.eventStack = {}
  }

  on(eventName, cb) {
    const { eventStack } = this
    const eventValue = eventStack[eventName]

    eventValue ? eventValue.push(cb) : eventStack[eventName] = [cb]
  }

  once(eventName, cb) {
    const { eventStack } = this
    const eventValue = eventStack[eventName]
    // 利用闭包的形式 来模拟一次性监听的功能函数
    const tempCb = () => {
      let isOutOfDate = false

      return () => {
        if (isOutOfDate) return
        cb()
        isOutOfDate = true
      }
    }

    eventValue ? eventValue.push(tempCb()) : eventStack[eventName] = [tempCb()]
  }

  off(eventName, cb) {
    const { eventStack } = this
    const eventValue = eventStack[eventName]

    if (!eventValue) return

    (eventValue || []).forEach((eventCb, index) => {
      if (eventCb === cb) {
        eventValue.splice(index, 1)
      }
    })
  }

  emit(eventName, data) {
    const { eventStack } = this
    const eventValue = eventStack[eventName]

    if (!eventValue) return

    (eventValue || []).forEach(eventCb => {
      eventCb(data)
    })
  }
}
复制代码

使用

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    {{ msg }}
    <button @click="handleClose">close</button>
    <button @click="handleStart">start</button>
    
    <button @click="handleUnsubscribe">unsubscribe</button>
    <button @click="handleDestroy">destroy</button>

  </div>

  <script src="./eventCenter.js"></script>
  <script src="./ws.js"></script>
  <script>
    // 第一个连接
    const WS = new Ws('ws://localhost:6050', 'echo-protocol')
    WS.send('conent success')
    const handleChat = data => console.log(data)
    WS.subscribe('chat', handleChat)
    
    // 第二个连接
    const WS2 = new Ws('ws://localhost:6050', 'echo-protocol')
    WS2.send('conent success')
    const handleChat2 = data => console.log(data)
    WS2.subscribe('chat', handleChat2)

    let count = 0
    let timeId = setInterval(() => {
      ++count
      WS.send(`count: ${count}`)
    }, 1000)

    let count2 = 0
    let timeId2 = setInterval(() => {
      ++count2
      WS2.send(`count2: ${count2}`)

      if (count2 === 20) {
        WS2.unsubscribe('chat', handleChat2)
      }
    }, 1000)
    
    const APP = new Vue({
      el: '#app',
      data: {
        msg: '...'
      },
      methods: {
        handleClose() {
          clearInterval(timeId)
          WS.close()
        },
        handleStart() {
          WS.start()
        },
        handleUnsubscribe() {
          WS.unsubscribe('chat', handleChat)
        },
        handleDestroy() {
          clearInterval(timeId)
          WS.destroy()
        }
      }
    })
  </script>
</body>
</html>
复制代码

服务端(没深入,复制的官方代码)

serve.js

const http = require('http')
var WebSocketServer = require('websocket').server

const app = http.createServer((req, res) => {
  console.log('...')
  res.setHeader("Content-type","application/json")
  res.end('success')
})

app.listen(6050, () => {
  console.log('listen port 6050')
})

const wsServer = new WebSocketServer({
  httpServer: app,
  // You should not use autoAcceptConnections for production
  // applications, as it defeats all standard cross-origin protection
  // facilities built into the protocol and the browser.  You should
  // *always* verify the connection's origin and decide whether or not
  // to accept it.
  autoAcceptConnections: false
});

function originIsAllowed(origin) {
// put logic here to detect whether the specified origin is allowed.
return true;
}

wsServer.on('request', function(request) {
  if (!originIsAllowed(request.origin)) {
    // Make sure we only accept requests from an allowed origin
    request.reject();
    console.log((new Date()) + ' Connection from origin ' + request.origin + ' rejected.');
    return;
  }
  
  var connection = request.accept('echo-protocol', request.origin);
  console.log((new Date()) + ' Connection accepted.');
  connection.on('message', function(message) {
      if (message.type === 'utf8') {
          console.log('Received Message: ' + message.utf8Data);
          connection.sendUTF(JSON.stringify({
            type: 'chat',
            data: message.utf8Data
          }));
      }
      else if (message.type === 'binary') {
          console.log('Received Binary Message of ' + message.binaryData.length + ' bytes');
          connection.sendBytes(message.binaryData);
      }
  });
  connection.on('close', function(reasonCode, description) {
      console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.');
  });
});
复制代码

博文推荐

【笔记不易,如对您有帮助,请点赞,谢谢】

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改