# websocket

# 介绍

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

  • 在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

  • 浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

  • 当获取 Web Socket 连接后,可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。

  • 创建 WebSocket 对象:

    var Socket = new WebSocket(url, [protocol])
    

    以上代码中的第一个参数 url, 指定连接的 URL。第二个参数 protocol 是可选的,指定了可接受的子协议。

# 属性

属性 描述
Socket.readyState 只读属性 readyState 表示连接状态,可以是以下值:0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。
只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

# 事件

事件 事件处理程序 描述
open Socket.onopen 连接建立时触发
message Socket.onmessage 客户端接收服务端数据时触发
error Socket.onerror 通信发生错误时触发
close Socket.onclose 连接关闭时触发

# 方法

方法 描述
Socket.send() 使用连接发送数据
Socket.close() 关闭连接

# 数据传输

WebSocket 传输的数据都是以 Frame(帧)的形式实现的,就像 TCP/UDP 协议中的报文段 Segment。

# 连接过程

webSocket原理:利用 HTTP 请求产生握手,HTTP 头部中含有 WebSocket 协议的请求,所以握手之后,二者用 TCP 协议进行交流。

在用户使用的浏览器支持 websocket 的前提下,浏览器向服务器发送请求,并在消息头中添加 Connection: Upgrade 和 Upgrade: websocket,告诉服务器需要切换成 websoket 协议通信。当服务器支持 websocket 服务并允许客户端连接,则在响应的报文中返回 Upgrade 和 Connection 消息头域,同意浏览器使用 websocket 连接,同时返回 101 状态码表示请求还需要完成协议的切换。

images

发送请求

Upgrade: websocket
Connection: Upgrade
上面两个表示发起的是websocket协议
Sec-WebSocket-Key:   // 一个随机的经过base64编码的字符串,像密钥一样用于服务器和客户端的握手过程
Sec-WebSocket-Protocol:  // 用户定义的字符串
Sec-WebSocket-Version: // 协议版本

响应

Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:  // 加密后的

默认情况下,ws 协议使用 80 端口进行普通连接,加密的 TLS 连接默认使用 443 端口。

# 与 TCP/HTTP 关系

WebSocket 是基于 TCP 的独立的协议。和 HTTP 的唯一关联就是 HTTP 服务器需要发送一个“Upgrade”请求,即 101 Switching Protocol 到 HTTP 服务器,然后由服务器进行协议转换。

# 开发步骤

  1. 创建 socket 链接,返回一个 promise,包含注册/移除接收数据的事件、发送数据、关闭连接的方法。
async function socketIo(socketUrl) {
  let ws = null
  let pingTimer = null
  let reconnecTimer = null
  let params = null
  let lockSocket = false
  let hasParams = false
  const listener = {}

  function createSocket(socketUrl) {
    try {
      if ('WebSocket' in window) {
        ws = new WebSocket(socketUrl)
      } else if ('MozWebSocket' in window) {
        ws = new MozWebSocket(socketUrl)
      }
      initEvents()
    } catch (e) {
      pingTimer && clearInterval(pingTimer)
      lockSocket = true
      reconnect(socketUrl)
    }
  }
  // 创建socket连接
  await createSocket(socketUrl)

  return new Promise((resolve, reject) => {
    const data = {
      close: () => {
        ws.close()
      },
      addListener: (key, callback) => {
        listener[key] = callback
      },
      send: data => {
        params = data || ''
        hasParams = true
        sendMessage(data)
      },
      removeListener: key => {
        delete listener[key]
      }
    }
    resolve(data)
  })
}
  1. 初始事件,包括打开连接 open、接收数据 message、错误 error 和关闭 close 事件。
const initEvents = () => {
  ws.onopen = () => {
    console.log('\x1B[32m%s\x1b[0m', '---socket已连接---')
    lockSocket = false
    pingTimer && clearInterval(pingTimer)
    sendPing()
    if (ws.readyState === 1 && hasParams) {
      sendMessage(params)
    }
  }
  ws.onmessage = event => {
    const data = event.data
    Object.keys(listener).forEach(k => {
      if (typeof listener[k] === 'function') {
        listener[k](data)
      }
    })
  }
  ws.onerror = event => {
    console.log('\x1B[31m%s\x1b[0m', '---socket连接发生错误---')
    console.log(event)
    pingTimer && clearInterval(pingTimer)
    lockSocket = true
    reconnect(socketUrl)
  }
  ws.onclose = () => {
    console.log('\x1B[33m%s\x1b[0m', '---socket连接关闭---')
    pingTimer && clearInterval(pingTimer)
    lockSocket = true
    reconnect(socketUrl)
    ws.close()
  }
}
  1. 当成功连接 socket 后,就可以进行通信了
const sendMessage = data => {
  if (!ws || ws.readyState !== 1) return
  ws.send(data)
}
  1. 当 socket 突然断开,我们要有重连机制,确保因网络等各种因素导致连接断开而无法实时通信问题。
const reconnect = socketUrl => {
  if (!lockSocket) return
  if (reconnecTimer) clearTimeout(reconnecTimer)
  lockSocket = true
  reconnecTimer = setTimeout(() => {
    createSocket(socketUrl)
  }, 5000)
}
  1. 在与服务端正常通信后,就可以接收相应数据了。为了确保这个连接一直在,每隔一段时间发送一个心跳标识,服务端检测到该标志,说明该连接仍在使用。如果隔断时间没接收到,自动关闭连接,不再往客户端推送数据。
const sendPing = () => {
  pingTimer = setInterval(() => {
    sendMessage('123')
  }, 40000)
}
  1. 注册 socket
socketIo('ws://xxx').then(res => {
  Vue.prototype.$socket = res
})
  1. 添加接收数据事件
export default {
  mounted() {
    this.$socket.addListener('getRealTime', this.getRealTime)
  },
  methods: {
    getRealTime(data) {
      console.log(data)
    }
  }
}