websocket 原理及封装

实时通信的轮询方案

1,ajax短连接:客户端每隔一秒钟发一次请求,服务器收到请求后会立刻返回结果,不管有没有新数据。
2,ajax长连接:客户端发送一次请求,服务器端收到请求后查询有没有新数据,如果没有新数据就阻塞这个请求,直到有新数据或者超时为止。客户端每次收到请求返回结果后立刻再发一次请求。comet貌似就是这个原理。
3,WebSocket:这就不是一个HTTP协议了,而是一个tcp协议,而且Socket这个玩意顾名思义就是一个流了,可以双向操作。缺点是有些浏览器不支持。

对比延迟:
假设网络延迟是m毫秒,那么ajax短连接的延迟在m到1000毫秒之间,另外两种基本只有m毫秒的延迟。
对比资源占用:
应该是1>2>3。但是1和2的比较要看情况,如果两次请求间隔时间很长的话应该是2>1>3。

实时通信技术在股票价格、新闻报道、余票查询、交通情况等领域中有着广泛的应用,

但是目前的实时Web应用的实现方式,都是基于HTTP协议,围绕着轮询和其他服务器推送技术展开的,所以不可避免的产生大量的额外的报头数据,造成传输时延。HTML5中的WebSocket协议是基于浏览器与服务器全双工通信的新理论。客户端通过WebSocket与服务器进行通信时,只有第一次握手交互信息比较复杂,在握手成功后便进入全双工的数据传输阶段,降低了数据流量与传输时延。

流程图

WebSocketObj类

使用单例模式创建websocket连接,暴露出手动开启/关闭/重连的函数,内部实现断线重连和心跳检测机制,通过onMessage状态钩子触发接收下行消息,发送该下行type对应的事件通知,由SocketEvent类实现调用对应的事件数组。

import { PING, ROOMID, SESSION, TIME_SPACE, EV_TYPE } from './constant'
import { tpToType } from './mapHandle'
import SocketEvent from './socketEvent'
import Api from 'globals/api/getCommon';

let wsUrl = null; // ws链接地址
let ws = null; // 初始化ws实例
let lockReconnect = false; // 避免重复连接

// ws对象
class WebSocketObj {
  // 获取ws地址
  async getWsUrl() {
    const params = {
      roomid: ROOMID,
      session: SESSION
    }
    const { dm_error, url } = await Api.getWebsocketUrl(params);

    if (+dm_error === 0) {
      console.log('获取到长链接地址')
      console.log(url)
      wsUrl = url
    }
  }

  // 创建websocket连接
  async createWebSocket() {
    !wsUrl && await this.getWsUrl();
    this.initWebSocket();
    this.socketHandle();
  }

  // 确保只有一个Websocket实例
  initWebSocket() {
    if (!ws) {
      if (!wsUrl) return
      ws = new WebSocket(wsUrl);
    } else {
      console.log('已有websocket实例')
    }
    return ws;
  }

  // websocket 钩子
  socketHandle = () => {
    // 创建成功
    const self = this;
    if (!ws) return
    ws.onopen = function () {
      var dt = new Date();
      var str = dt.getFullYear() + '-' + (dt.getMonth() + 1) + '-' + dt.getDate() + ' ' + dt.getHours() + ':' + dt.getMinutes() + ':' + dt.getSeconds();
      // 重置心跳检测
      self.heartbeatCheck.init();
      self.heartbeatCheck.start();
      console.log(str)
      console.log('*************** websocket连接成功 ***************');
    };

    // 接收到推送消息
    ws.onmessage = function (evt) {
      var data = JSON.parse(evt.data);
      // 重置心跳检测
      self.heartbeatCheck.init();
      self.heartbeatCheck.start();
      const { b: { ev }, ms } = data;
      if (ev === EV_TYPE.PONG) {
        console.log('心跳正常')
      } else if (ev === EV_TYPE.SM) {
        console.log('------------ 收到推送消息 ------------')
        console.log(data)
        self.postMessage(ms)
      }
    };

    // 长链接被关闭
    ws.onclose = function (evt) {
      self.heartbeatCheck.init();
      console.log('------------ 长连接关闭 ------------')
      console.log(evt)
      // this.reconnect();
    };

    // 长链接报错
    ws.onerror = function (evt) {
      console.log('------------ 长连接报错 ------------')
      console.log(evt)
      this.reconnect();
    };
  }

  // 手动开启
  wsOpen() {
    this.createWebSocket();
  }

  // 手动关闭
  wsClose() {
    ws && ws.close();
    ws = null
  }

  // 更新websocket地址
  async updateWsUrl() {
    this.wsClose();
    await this.getWsUrl();
    this.initWebSocket();
    this.socketHandle();
  }

  // 上行ping消息
  wsSendPing() {
    console.log('------------ 心跳检测 ------------')
    ws && ws.send(JSON.stringify(PING))
  }

  // 重连机制
  reconnect = () => {
    // 重连中,不重复执行
    if (lockReconnect) {
      return
    }
    lockReconnect = true;
    // 没连接上会一直重连,设置延迟避免请求过多
    setTimeout(function () {
      this.createWebSocket();
      lockReconnect = false;
    }, 2000);
  }

  // 心跳检测
  heartbeatCheck = {
    sendTimer: null, // 定时发送ping

    receiveTimer: null, // timeSpace时间内接收pong

    // 初始化心跳
    init: () => {
      clearTimeout(this.sendTimer)
      clearTimeout(this.receiveTimer)
    },

    // 开启心跳检测
    start: () => {
      this.sendTimer = setTimeout(() => {
        this.wsSendPing(); // 发送ping

        this.receiveTimer = setTimeout(() => {
          console.log('心跳检测失败,正在尝试重连...')
          this.wsClose(); // 先关闭当前ws
          // this.wsOpen(); // 重新打开ws
          this.createWebSocket();
        }, TIME_SPACE)
      }, TIME_SPACE)
    }
  }

  // 发送事件
  postMessage(sm) {
    sm.forEach(data => {
      const { tp, bd } = data;
      const type = tpToType[tp]
      if (type) {
        SocketEvent.executeEvent(type, bd)
      } else {
        console.log('该长链接未定义:', tp)
      }
    });
  }
}

export default new WebSocketObj();

SocketEvent类

实现发布订阅模式,抛出addEvent添加事件,removeEvent移除事件和executeEvent执行事件的函数,各业务页面通过addEvent来订阅对应业务下行消息的type事件,可以通过removeEvent手动移除当前页面的指定订阅函数,在WebSocketObj类中接收到长链下行的时候触发对应type的回调函数来实现发布操作。

// 事件容器,放置事件数组
const eventHandlers = {};

class SocketEvent {
  // 添加事件
  addEvent(type, handler) {
    // 判断事件数组中是否已存在
    if (!(type in eventHandlers)) {
      eventHandlers[type] = [];
    }
    // 添加入事件数组
    eventHandlers[type].push(handler)
  }

  // 删除事件
  removeEvent(type, handler) {
    // 不存在当前事件类型
    if (!(type in eventHandlers)) {
      return;
    }
    // 未传入指定handle,删除type所有handler
    if (!handler) {
      delete eventHandlers[type]
    } else {
      const tempIndex = eventHandlers[type].findIndex(handlerItem => {
        return handlerItem === handler
      });
      if (tempIndex === -1) {
        // console.log('不存在事件handle:', handler);
      } else {
        eventHandlers[type].splice(tempIndex, 1)
        if (eventHandlers[type].length === 0) {
          delete eventHandlers[type]
        }
      }
    }
  }

  // 执行事件
  executeEvent(type, ...params) {
    if (!(type in eventHandlers)) {
      console.log('未注册该事件:', type)
      return;
    }
    eventHandlers[type].forEach(handlerItem => {
      handlerItem(...params)
    });
  }
}

export default new SocketEvent();

websocket Model

定义一个websocket的model,暴露出开关长链接和添加移除事件,方便业务组件通过model来调用。

import WebSocketObj from '@/webSocket/webSocket'
import SocketEvent from '@/webSocket/socketEvent'

export default {
  name: 'webSocket',

  state: {

  },

  effects: {
    // 创建长链接
    * createWebSocket({ payload }, { call, put, select }) {
      yield WebSocketObj.createWebSocket()
    },

    // 更新长链接地址
    * updateWebSocket({ payload }, { call, put, select }) {
      yield WebSocketObj.updateWsUrl();
    },

    // 添加事件
    * addEvent({ payload }, { call, put, select }) {
      const { type, handle } = payload;
      yield SocketEvent.addEvent(type, handle)
    },

    // 移除事件
    * removeEvent({ payload }, { call, put, select }) {
      const { type, handle } = payload;
      yield SocketEvent.removeEvent(type, handle);
    },
  },

  reducers: {
    closeWebSocket(state) {
      WebSocketObj.wsClose();
      return {
        ...state,
      };
    },
  },

  subscriptions: {

  },
};

mapHandle类

作为一个映射类,将服务端下行消息的type类型映射到对应的事件名称

// 定义事件列表
export const typeList = {
  LIVE_PREVIEW_CHANGE_EVENT: 'LivePreviewChangeEvent',
}

// 定义下行消息对应的事件
export const tpToType = {
  'c.xxxxxxxxxxxxx': typeList.LIVE_PREVIEW_CHANGE_EVENT, // 下行事件
}

// 开发环境检测type值是否重复定义
if (process.env.NODE_ENV === 'development') {
  const memo = new Set()
  Object.values(typeList).forEach(item => {
    if (!memo.has(item)) {
      memo.add(item)
    } else {
      throw new Error('重复定义tp:', item)
    }
  })
}

业务组件使用例子

import React, { useState, useEffect } from 'react'
import { history, useDispatch } from 'umi'
import { typeList } from '@/webSocket/mapHandle'
import $common from '@/utils/common';
import Header from 'common-comp/Header';

const WebSocketTest = props => {
  const [eventData, setEventData] = useState()
  const [canAdd1, setCanAdd1] = useState(true)
  const [canAdd2, setCanAdd2] = useState(true)
  const dispatch = useDispatch()

  const testFun1 = (data) => {
    console.log('testFun1=========', data)
    setEventData(data)
  }

  const testFun2 = (data) => {
    console.log('testFun2=========', data)
  }

  useEffect(() => {
    window.reloadNewUrl = url => {
      const oldUrlParams = $common.getUrlParams(window.location.href);
      const newUrlParams = $common.getUrlParams(url);
      if (oldUrlParams.tourist !== newUrlParams.tourist) {
        if (window.history.replaceState) {
          window.history.replaceState(null, document.title, url);
        }
        const params = $common.getUrlParams(url)
        const {
          uid, sid, tourist, smid
        } = params;
        // 更新cookie
        dispatch({
          type: 'user/appUpdateLogin',
          payload: {
            uid, sid, smid, tourist
          }
        })
      }
    };

    return () => {
      delete window.reloadNewUrl;
    }
  }, [])

  useEffect(() => {
    // 添加响应事件
    if (canAdd1) {
      dispatch({
        type: 'webSocket/addEvent',
        payload: {
          type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
          handle: testFun1
        }
      })
    }
    if (canAdd2) {
      dispatch({
        type: 'webSocket/addEvent',
        payload: {
          type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
          handle: testFun2
        }
      })
    }

    // 移除响应事件
    return () => {
      dispatch({
        type: 'webSocket/removeEvent',
        payload: {
          type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
          handle: testFun1
        }
      })
      dispatch({
        type: 'webSocket/removeEvent',
        payload: {
          type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
          handle: testFun2
        }
      })
    }
  }, [testFun1, testFun2])

  return <div>
    <Header
      leftIcon="home"
      rightContent={[
        {
          icon: 'shop-car',
          handleClick: () => history.push('/shoppingCart'),
        },
        {
          icon: 'user',
        },
      ]}
    />
    <div
      style={{ backgroundColor: 'gray', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
      onClick={() => {
        $common.goLoginPage();
      }}
    >
      登录
    </div>
    <div
      style={{ backgroundColor: 'pink', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
      onClick={() => {
        dispatch({
          type: 'webSocket/createWebSocket',
        })
      }}
    >
      手动开启
    </div>
    <div
      style={{ backgroundColor: 'yellow', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
      onClick={() => {
        dispatch({
          type: 'webSocket/closeWebSocket',
        })
      }}
    >
      手动关闭
    </div>
    <div
      style={{ backgroundColor: 'red', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
      onClick={() => {
        setCanAdd1(false)
        dispatch({
          type: 'webSocket/removeEvent',
          payload: {
            type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
            handle: testFun1
          }
        })
      }}
    >移除事件回调1</div>
    <div
      style={{ backgroundColor: 'red', marginBottom: '20px', padding: '20px', cursor: 'pointer' }}
      onClick={() => {
        setCanAdd2(false);
        dispatch({
          type: 'webSocket/removeEvent',
          payload: {
            type: typeList.LIVE_PREVIEW_CHANGE_EVENT,
            handle: testFun2
          }
        })
      }}
    >移除事件回调2</div>
    <div>
      下行数据回显:{eventData?.preview_list[0]?.img}
    </div>
  </div>
}

export default WebSocketTest;

从layout入口开启全局的长连接

运行页面

疑难点:

为什么还要手动实现心跳检测?

http协议和websocket协议都是应用层协议,都是依赖于TCP连接,通过TCP协议来传输的,而TCP协议本身是有KeepAlive保活机制,但是Keepalive 技术只是TCP协议中的一个可选项。因为不当的配置可能会引起一些问题,所以默认是关闭的,而且TCP协议默认2小时的KeepAlive基本不可能长连接保活,所有需要手动实现心跳检测

参考文章:彻底搞懂TCP协议层的KeepAlive保活机制

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值