WebSocket 原理及单例组件封装

4,094 阅读6分钟

简介

WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此API,您可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。

image.png

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

发展历程

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

  • HTTP1.0:一个HTTP请求的生命周期:客户端发起,服务端响应,断开连接。
    • 缺点:无法复用、三次握手、慢启动
  • HTTP1.1:管道化,默认开启长链接(Connection:keep-alive),服务器处理后不会立即关闭连接会等待。
    • 缺点:无法区分回包对应请求,顺序返回导致线头阻塞;不支持服务端推送
  • HTTP2.0:多路复用。实现服务器推送,使用帧、流等方式解决了线头阻塞。HTTP2.0在一个tcp连接中,可以同时发送多个HTTP请求,每个请求是一个流,一个流可以分成很多帧,有了标记号,服务器可以随便发送回包,客户端收到后,根据标记,重新组装就可以。

以上是关于HTTP协议关于请求的一些发展,而WebSocket就服务端推送提供了另一种解决方案-双向通信协议。

解决了哪些问题?

解决了HTTP的几个难题:

  • 服务端可以主动向客户端推送
  • 双向通讯
  • 高效连接
  • 节约带宽
  • 节省服务器资源

特点及作用

特点

  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

作用

  • 解决服务端长时间处理等待超时问题
  • 解决获取服务端实时状态无法高效推送问题
  • 解决高频推送建立链接高耗时问题

与其他技术比对

  • Ajax轮询 让浏览器隔个几秒就发送一次请求,询问服务端有没有新消息。 需要服务器有很快的处理速度和资源
  • 长轮询 也采用轮询的方式,不过采取的是阻塞模型,直到返回Response后,再次建立连接。 需要有很高的并发能力
  • WebSocket 一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,避免了HTTP的非状态性

优缺点

  • 优点
    • WebSocket是一种双向通讯协议
    • 实时应用都在使用WebSocket在单个通道上接收数据
    • 经常更新的应用程序都应该使用WebSocket,比HTTP更快
    • 是HTML5的技术之一,具有巨大的应用前景
  • 缺点
    • 不兼容低版本的IE
    • 要获取旧数据,或者只想获取一次数据供应用程序使用,则应该使用HTTP协议
    • 如果仅加载一次数据,则RestFul Web服务足以从服务器获取数据。

核心原理

WebSocket 协议

WebSocket协议是通过HTTP协议升级而来。只需要在HTTP协议基础上增加两次握手,即可建立WebSocket连接。本质上是在TCP协议上封装的另一种应用层协议(WebSocket协议)。因为他是基于TCP的,所以服务端可以自己推送。协议升级过程如下:

  1. 客户端发送协议升级的请求。在HTTP请求加上,如图一的HTTP头。
  2. 服务器如果支持WebSocket协议的话,会返回101状态码表示同意协议升级,并且支持各种配置(如果服务器不支持某些功能或版本,或告诉客户端,客户端可以再次发送协议升级的请求)。服务会返回形如图二的HTTP头。
  3. 这样就完成了协议的升级,后续的数据通信,就是基于tcp连接之上,使用WebSocket协议封装的数据包。

image.png

【图一:客户端发送协议升级】

image.png 【图二:服务端同意协议升级】

技术应用

  • 实时推送订单结果消息
  • 文件上传解析结果通知
  • websocket社交订阅
  • websocket多玩家游戏
  • websocket协同编辑/编程
  • websocket收集点击流数据
  • 股票基金报价
  • 体育实况更新
  • 多媒体聊天
  • 在线教育
  • 基于位置的应用
  • 论坛的消息广播

组件封装

封装单例组件,并支持多个服务端webSocket连接。 image.png

// import handleMsgMap from './handleReceiveMessage';

let websocketObj = {};
const newWebSocket = async (url, openCallback, messageCallback, closeCallback) => {
  console.log('new webSocket.....', websocketObj[url]);
  if (websocketObj[url]) return websocketObj[url];

  // 判断当前环境是否支持websocket
  if (window.WebSocket) {
    // wss: https协议使用,ws:http协议使用
    websocketObj[url] = new WebSocket(url);
    // 连接成功建立的回调方法
    websocketObj[url].onopen = function () {
      // 成功建立连接后,重置心跳检测
      console.log('websocket connect');
      if (openCallback) {
        openCallback(websocketObj[url]);
      } else {
        const content = JSON.stringify({ content: '你好啊!', type: '0' });
        websocketObj[url].send(content);
      }
      // heartCheck.reset().start(url, openCallback, messageCallback, closeCallback);
      console.log('connected successfully');
    };

    // 接受到消息的回调方法
    websocketObj[url].onmessage = function (e) {
      const message = JSON.parse(e.data);
      const { isSuccess, data, msgType, respCode } = message;
      if (respCode && handleRespCodeStrategy[respCode]) {
        handleRespCodeStrategy[respCode](url, openCallback, messageCallback, closeCallback);
        return;
      }

      if (messageCallback) {
        messageCallback(e);
        return;
      }
      if (isSuccess && data) {
        // 执行接收到消息的操作,分发actions执行操作
        handleMsgMap[msgType](data);
      }
    };

    // 接受到服务端关闭连接时的回调方法
    websocketObj[url].onclose = function (e) {
      console.log('service onclose');
      closeCallback && closeCallback(e);
    };

    // 连接发生错误,连接错误时会继续尝试发起连接,间隔30s
    websocketObj[url].onerror = function () {
      closeWebsocket(url);
      setTimeout(() => {
        newWebSocket(url, openCallback, messageCallback, closeCallback);
      }, 30000);
    };

    // 监听窗口事件,当窗口关闭时,主动断开websocket连接,防止连接没断开就关闭窗口,server端报错
    window.onbeforeunload = () => {
      console.log('window onclose');
      return websocketObj[url] && websocketObj[url].close();
    };
    return websocketObj[url];
  } else {
    console.log('not support websocket');
    return null;
  }
};

// 心跳检测, 每隔一段时间检测连接状态,如果处于连接中,就向server端主动发送消息,来重置server端与客户端的最大连接时间,如果已经断开了,发起重连。
const heartCheck = {
  timeout: 10000, // 心跳,比server端设置的连接时间稍微小一点,在接近断开的情况下以通信的方式去重置连接时间。
  serverTimeoutObj: null,
  reset: function () {
    clearInterval(this.serverTimeoutObj);
    this.serverTimeoutObj = null;
    return this;
  },
  start: function (url, openCallback, messageCallback, closeCallback) {
    this.serverTimeoutObj = setInterval(() => {
      if (websocketObj[url].readyState === 1) {
        const content = JSON.stringify({ msgType: 1 });
        websocketObj[url].send(content);
      } else {
        closeWebsocket(url);
        newWebSocket(url, openCallback, messageCallback, closeCallback);
      }
    }, this.timeout);
  }
};
// 处理无网状态 断开重连机制
window.addEventListener('online', () => {
  if (!websocketObj[url]) {
    newWebSocket(url, openCallback, messageCallback, closeCallback);
  }
});
window.addEventListener('offline', () => {
  if (websocketObj[url]) {
    closeWebsocket(url);
  }
});
const closeWebsocket = (url) => {
  heartCheck.reset();
  websocketObj[url] && websocketObj[url].close();
  websocketObj[url] = null;
};
// 1001:重新连接   1002:关闭心跳定时器  1003:重新登录 跳转login  1004:关闭websocket   9999:失败保持不动
const handleRespCodeStrategy = {
  1001: function (url, openCallback, messageCallback, closeCallback) {
    websocketObj[url] && closeWebsocket(url) && newWebSocket(url, openCallback, messageCallback, closeCallback);
  },
  1002: function () {
    heartCheck.reset();
  },
  1003: function (url) {
    closeWebsocket(url);
    window.location.href = window.location.origin + window.location.pathname + '#/login'; // 被挤掉时,有弹窗存在,处理弹窗问题
    window.location.reload();
  },
  1004: function (url) {
    closeWebsocket(url);
  },
  9999: () => {}
};
// export default newWebSocket;