阅读 339
WebSocket实战封装

WebSocket实战封装

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

1,为什么需要 WebSocket?

WebSocket是HTML5出的协议,有人会问,我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。而且,HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的

2,webSocket简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。 其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏
蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是`ws`(如果加密,则为`wss`),服务器网址就是 URL。
复制代码

3,webSocket优点

首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。 传统的HTTP的生命周期是通过Request来界定,也就是一个Request 一个Response,那么HTTP1.0,这次HTTP请求就结束了。

在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。

但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起

而Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手,在握手阶段是一样的

GET /chat HTTP/1.1 
Host: server.example.com 
Upgrade: websocket 
Connection: Upgrade 
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== 
Sec-WebSocket-Protocol: chat, superchat 
Sec-WebSocket-Version: 13 
Origin: http://example.com
复制代码

上面就是Websocket的核心了,告诉Apache、Nginx等服务器:注意啦,本次发起的是Websocket协议,请用webSocket协议处理

首先,Sec-WebSocket-Key  是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:我要验证是不是
真的是Websocket协议。

然后,Sec_WebSocket-Protocol  是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议

最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket协议版本,

然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!**
复制代码
HTTP/1.1 101 Switching Protocols 
Upgrade: websocket 
Connection: Upgrade 
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= 
Sec-WebSocket-Protocol: chat
复制代码

这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~,告诉客户端即将升级的是Websocket协议, 至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了

4,webSocket API

(1),新建 WebSocket 实例

var ws = new WebSocket('ws://localhost:8080');
复制代码

(2) webSocket.readyState

WebSocket的四种状态

CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
复制代码

(3) webSocket.onopen

指定连接成功后的回调函数。

ws.onopen = function () {
   ws.send('Hello Server!');
}
复制代码

(4) webSocket.onclose

指定连接关闭后的回调函数

ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  操作关闭事件
};
复制代码

(5) webSocket.onmessage

指定收到服务器数据后的回调函数

ws.onmessage = function(event) {
  var data = event.data;
  // 处理数据
};
复制代码

(6) webSocket.send

用于向服务器发送数据。

ws.send('your message');
复制代码

(7) webSocket.onerror

用于指定报错时的回调函数。

socket.onerror = function(event) {
  // handle error event
};
复制代码

5,webSocket实战封装

const Base64 = window.Base64
let timeoutObj = null // 心跳倒计时
let serverTimeoutObj = null // 心跳倒计时
let reconnetNum = 0 // 重连次数
let lockReconnect = false // 是否重新建立连接
let timeoutnum = null // 断开 重连倒计时
let receiveNews = [] // 接受到的消息
let isOrReceive = false // 是否在45秒内收到消息
let webSocketUrl = '' // websocket创建地址
let websocket = null
var wsModuleObj = '' // 所属模块
let wsSystem = '' // 所属系统
let messageId = '' // 消息ID
const sendObj = {
  load: {},
  path: 'internal/heart_beat',
  protocolId: 'PROTO_HEART_BEAT',
  sourceType: 'request',
  taskId: ''
}
/**
 * @description 心跳消息动态生成16位字符串
 */
function uuid() {
  function S4() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
  }
  return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4())
}

// 重新连接
function reconnect(wsUrls) {
  addLog(JSON.stringify('链接已经断开,' + new Date().toString()), 2004)
  Message({message: 'websocket链接已断开',type: 'error'})
  if (lockReconnect){
     return
  } 
  lockReconnect = true
  // 没连接上会一直重连,设置延迟避免请求过多
  timeoutnum && clearTimeout(timeoutnum)
  timeoutnum = setTimeout(function () {
    if (reconnetNum < 4) {
      reconnetNum++
      websocket = null
      createWebSocket(wsUrls)
      lockReconnect = false
    } else {
      timeoutObj && clearInterval(timeoutObj)
      timeoutnum && clearTimeout(timeoutnum)
      websocket.close()
      messageBox()
      return
    }
  }, 20000)
}

// 创建两个定时器
function heartCheck() {
  // 第一个定时器每隔20S给后端发送一个消息
  timeoutObj && clearInterval(timeoutObj)
  timeoutObj = setInterval(() => {
    websocketsend(sendObj)
  }, 20000)
  serverTimeoutObj && clearInterval(serverTimeoutObj)
  // 第二个定时器每隔45S监测 后端是否给我发送了心跳
  serverTimeoutObj = setInterval(() => {
    if(isOrReceive) {
      isOrReceive = false
    } else {
      reconnect(webSocketUrl)
    }
  },45000)
}
// 连接成功
function wsOnopen() {
  reconnetNum = 0
  heartCheck() // 开启心跳监测
}
// 发生异常
function wsOnerror(e) {
  reconnect(webSocketUrl) // 发生异常,重新链接
}
// 关闭链接
export function websocketclose(e) {
  websocket.close()
}
/**
 * @description 接收服务器推送的信息
 * @param {消息事件参数} e
 * @param {所属模块} moduleObj
 * @param {所属系统} system
 */
export function websocketonmessage(e, moduleObj, system) {
  if (e && e.data) { // 发送消息的模块
    const tempData = JSON.parse(e.data)
    const redata = tempData.load
    if (tempData.path === 'internal/heart_beat') { // 如果是心跳消息
      isOrReceive = true // 告诉45s定时器,收到了心跳消息
      sendObj.load = redata
    } else {
      for (let i = 0; i < moduleObj.length; i++) {
        if (tempData.path !== 'internal/heart_beat' ) {
        const logInfo = { 'messageId:': redata.msgId, 'content': redata.content, 'all': redata}
          receiveNews.unshift(redata)
          if (messageId !== redata.messageId) {
            moduleObj[i].val(redata)
            messageId = redata.messageId
          }
          receiveNews = []
        }
      }
    }
  }
}
/**
 * @description 向服务器发送信息
 * @param {消息内容}} msg
 */
function websocketsend(msg) {
  var jsonString = JSON.stringify(msg)
  if (websocket.readyState === 1) { // 如果连接正常
    websocket.send(jsonString)
  }
}

/**
 * @description webSocket 对外注册方法
 * @param {环境变量} env
 * @param {所属模块} moduleObj
 * @param {所属系统} system
 * @param {UM账号} um
 * @param {令牌身份}} token
 */
export function register(env, moduleObj, system, um) {
  addLog(JSON.stringify('初始化长链接:'+ um + ',' + system + ',' + moduleObj + ',' + env + ',' + 
  new Date().toString()), 2002)
  initWebSocket(env, moduleObj, system, um)

}
// 初始化创建webSocketUrl
function initWebSocket(env, moduleObj, system, umId) {
  wsModuleObj = moduleObj
  wsSystem = system || window.localStorage.getItem('systemId')
  let  wsUrls
  const um = umId || window.localStorage.getItem('umId')
  const base64 = new Base64(um.toUpperCase())
  let  token = base64.encode(um)
  if (env === 'stg') {
    wsUrls = 'ws://xxxxx'
  } else if (env === 'prd') {
    wsUrls = 'ws://xxxxx'
  } else if (env === 'dev') {
    wsUrls = 'ws://xxxxx'
  }
  wsUrls = wsUrls + '?token=' + token + '&system=' + wsSystem
  webSocketUrl = wsUrls
  createWebSocket(webSocketUrl)
}
/**
 * @description 创建长链接,并初始化方法
 * @param {长链接地址} url
 */
function createWebSocket(url) {
  if ('WebSocket' in window) { // 判断当前浏览器是否支持WebSocket
    if(!websocket) {
      websocket = new WebSocket(url) // 初始化webSocket
      init() // 初始化websocket的方法
    }
  } else {
    alert('当前浏览器 Not support websocket')
  }
}
/**
 * @description 初始化websocket事件
 */
function init() {
  sendObj.taskId = uuid() // 初始化taskId
  websocket.onerror = wsOnerror   // 链接错误
  websocket.onclose = websocketclose   // 链接关闭
  websocket.onopen = wsOnopen   // 链接成功
  websocket.onmessage = (e) => { websocketonmessage(e, wsModuleObj, wsSystem) }   // 接受信息
}
复制代码
文章分类
前端
文章标签