likes
comments
collection
share

websocket 实时通信实现

作者站长头像
站长
· 阅读数 16

轮询和websocket对比

开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式:

  1. 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果

    这种方式比较古老,但是兼容性强。

    缺点就是不断请求,耗费了大量的带宽和 CPU 资源,而且存在一定的延迟性

  2. websocket 长连接:全双工通信,客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,更加方便

websocket 实现

创建 websocket 连接

建立ws连接,有如下两种形式:

ws 代表明文,默认端口号为 80,例如ws://www.example.com:80, 类似http

wss 代表密文,默认端口号为 443,例如wss://www.example.com:443, 使用SSL/TLS加密,类似https

const useWebSocket = (params: wsItem) => {
  // 定义传参 url地址 phone手机号
  let { url = "", phone = "" } = params;
  const ws = (useRef < WebSocket) | (null > null);
  // ws数据
  const [wsData, setMessage] = (useState < wsDataItem) | (null > null);
  // ws状态
  const [readyState, setReadyState] =
    useState < any > { key: 0, value: "正在连接中" };
  // 是否在当前页
  const [isLocalPage, setIsLocalPage] = useState(true);

  // 创建Websocket
  const createWebSocket = () => {
    try {
      window.slWs = ws.current = new WebSocket(
        `wss://${url}/ws/message/${phone}`
      );
      // todo 全局定义发送函数
      window.slWs.sendMessage = sendMessage;
      // todo 准备初始化
      initWebSocket();
    } catch (error) {
      // 创建失败需要进行异常捕获
      slLog.error("ws创建失败", error);
      // todo 准备重连
      reconnect();
    }
  };

  return { isLocalPage, wsData, closeWebSocket, sendMessage };
};

初始化 websocket

当前的连接状态定义如下,使用常量数组控制:

const stateArr = [
  { key: 0, value: "正在连接中" },
  { key: 1, value: "已经连接并且可以通讯" },
  { key: 2, value: "连接正在关闭" },
  { key: 3, value: "连接已关闭或者没有连接成功" },
];

主要有四个事件,连接成功的回调函数(onopen)、连接关闭的回调函数(onclose)、连接失败的回调函数(onerror)、收到消息的回调函数(onmessage)

const initWebSocket = () => {
  ws.current.onopen = (evt) => {
    slLog.log("ws建立链接", evt);
    setReadyState(stateArr[ws.current?.readyState ?? 0]);
    // todo 心跳检查重置
    keepHeartbeat();
  };
  ws.current.onclose = (evt) => {
    slLog.log("ws链接已关闭", evt);
  };
  ws.current.onerror = (evt) => {
    slLog.log("ws链接错误", evt);
    setReadyState(stateArr[ws.current?.readyState ?? 0]);
    // todo 重连
    reconnect();
  };
  ws.current.onmessage = (evt) => {
    slLog.log("ws接受消息", evt.data);
    if (evt && evt.data) {
      setMessage({ ...JSON.parse(evt.data) });
    }
  };
};

websocket 实时通信实现

websocket 心跳机制

在使用 ws 过程中,可能因为网络异常或者网络比较差,导致 ws 断开链接了,此时 onclose 事件未执行,无法知道 ws 连接情况。就需要有一个心跳机制,监控 ws 连接情况,断开后,可以进行重连操作。

目前的实现方案就是:前端每隔 5s 发送一次心跳消息,服务端连续 1 分钟没收到心跳消息,就可以进行后续异常处理了

const timeout = 5000; // 心跳时间间隔
let timer = null; // 心跳定时器

// 保持心跳
const keepHeartbeat = () => {
  timer && clearInterval(timer);
  timer = setInterval(() => {
    if (ws.current?.readyState == 1) {
      // 发送心跳 消息接口可以自己定义
      sendMessage({
        cmd: "SL602",
        content: { type: "heartbeat", desc: "发送心跳维持" },
      });
    }
  }, timeout);
};

如下图所示,为浏览器控制台中的截图,可以查看ws连接请求及消息详情。

注意:正常情况下,是需要对消息进行加密的,最好不要明文传输。

websocket 实时通信实现

websocket 重连处理

let lockFlag = false; // 避免重复连接
// 重连
const reconnect = () => {
  try {
    if (lockFlag) {
      // 是否已经执行重连
      return;
    }
    lockFlag = true;
    // 没连接上会一直重连
    // 设置延迟避免请求过多
    lockTimer && clearTimeout(lockTimer);
    var lockTimer = setTimeout(() => {
      closeWebSocket();
      ws.current = null;
      createWebSocket();
      lockFlag = false;
    }, timer);
  } catch (err) {
    slLog.error("ws重连失败", err);
  }
};

websocket 关闭事件

关闭事件需要暴露出去,给外界控制

// 关闭 WebSocket
const closeWebSocket = () => {
  ws.current?.close();
};

websocket 发送数据

发送数据时,数据格式定义为对象形式,如{ cmd: '', content: '' }

// 发送数据
const sendMessage = (message) => {
  if (ws.current?.readyState === 1) {
    // 需要转一下处理
    ws.current?.send(JSON.stringify(message));
  }
};

页面可见性

监听页面切换到前台,还是后台,可以通过visibilitychange事件处理。

当页面长时间处于后台时,可以进行关闭或者异常的逻辑处理。

// 判断用户是否切换到后台
function visibleChange() {
  // 页面变为不可见时触发
  if (document.visibilityState === "hidden") {
    setIsLocalPage(false);
  }
  // 页面变为可见时触发
  if (document.visibilityState === "visible") {
    setIsLocalPage(true);
  }
}

useEffect(() => {
  // 监听事件
  document.addEventListener("visibilitychange", visibleChange);
  return () => {
    // 监听销毁事件
    document.removeEventListener("visibilitychange", visibleChange);
  };
}, []);

页面关闭

页面刷新或者是页面窗口关闭时,需要做一些销毁、清除的操作,可以通过如下事件执行:

beforeunload:当浏览器窗口关闭或者刷新时会触发该事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。

onunload:当文档或一个子资源正在被卸载时,触发该事件。beforeunload在其前面执行,如果点的浏览器取消按钮,不会执行到该处。

function beforeunload(ev) {
  const e = ev || window.event;
  // 阻止默认事件
  e.preventDefault();
  if (e) {
    e.returnValue = "关闭提示";
  }
  return "关闭提示";
}
function onunload() {
  // 执行关闭事件
  ws.current?.close();
}

useEffect(() => {
  // 初始化
  window.addEventListener("beforeunload", beforeunload);
  window.addEventListener("unload", onunload);
  return () => {
    // 销毁
    window.removeEventListener("beforeunload", beforeunload);
    window.removeEventListener("unload", onunload);
  };
}, []);

执行 beforeunload 事件时,会有如下取消、确认弹框

websocket 实时通信实现

参考文档:

转载自:https://juejin.cn/post/7249204284180086842
评论
请登录