websocket 推送的数据丢了,怎么回事?

背景

遇到一个奇怪的问题:通过 websocket 推送给前端的消息,在渲染时竟然少了一条。与其他数据相比,唯一的不同是该数据量较大------难道是数据太大,被 websocket 过滤掉了?

排查过程

怀疑 websocket 过滤

websocket 用的是 useWebSocket

难道 useWebSocket 有设置参数。可以控制过滤大小?翻找半天文档 没找到!

难道源码里边限制了大小?翻看源码,也没找到!

自定义封装测试

那到底怎么回事??不用 useWebSocket 了,自己简易封装一个 websocket 试试。

ts 复制代码
import { useState, useEffect, useRef, useCallback } from 'react';

interface UseWebSocketOptions {
  url: string;
  onOpen?: (event: Event) => void;
  onClose?: (event: CloseEvent) => void;
  onError?: (event: Event) => void;
  onMessage?: (message: any) => void;
  reconnectLimit?: number;
  reconnectInterval?: number;
}

export const useWebSocket = (options: UseWebSocketOptions) => {
  const {
    url,
    onOpen,
    onClose,
    onError,
    onMessage,
    reconnectLimit = 3,
    reconnectInterval = 3000
  } = options;

  const [readyState, setReadyState] = useState<number>(WebSocket.CONNECTING);
  const [message, setMessage] = useState<any>(null);
  const [error, setError] = useState<Event | null>(null);
  
  const ws = useRef<WebSocket | null>(null);
  const reconnectCount = useRef(0);
  const reconnectTimer = useRef<NodeJS.Timeout>();
  const messageQueue = useRef<any[]>([]);

  const connect = useCallback(() => {
    try {
      if (ws.current?.readyState === WebSocket.OPEN) {
        console.warn('WebSocket 已经连接');
        return;
      }

      ws.current = new WebSocket(url);

      ws.current.onopen = (event) => {
        setReadyState(WebSocket.OPEN);
        reconnectCount.current = 0;
        onOpen?.(event);
      };

      ws.current.onmessage = (event) => {
        messageQueue.current.push(event.data);
        setMessage(event.data);
        onMessage?.(event.data);
      };

      ws.current.onclose = (event) => {
        setReadyState(WebSocket.CLOSED);
        onClose?.(event);
        
        // 自动重连
        if (reconnectCount.current < reconnectLimit) {
          reconnectTimer.current = setTimeout(() => {
            reconnectCount.current++;
            connect();
          }, reconnectInterval);
        }
      };

      ws.current.onerror = (event) => {
        setError(event);
        onError?.(event);
      };
    } catch (err) {
      console.error('WebSocket 连接失败:', err);
    }
  }, [url, onOpen, onClose, onError, onMessage, reconnectLimit, reconnectInterval]);

  const close = useCallback(() => {
    if (reconnectTimer.current) {
      clearTimeout(reconnectTimer.current);
    }
    ws.current?.close(1000);
    setReadyState(WebSocket.CLOSED);
  }, []);

  useEffect(() => {
    connect();
    return () => {
      close();
    };
  }, [connect, close]);

  return {
    readyState,
    message,
    error,
    sendMessage: useCallback((data: string | ArrayBuffer | Blob) => {
      if (ws.current?.readyState === WebSocket.OPEN) {
        ws.current.send(data);
      } else {
        throw new Error('WebSocket 未连接');
      }
    }, []),
    close,
    messageQueue: messageQueue.current,
  };
};

尝试了一下,接受到了,那条丢失的消息居然出来了。

自己封装 useWebSocket 可以接收到那条消息,aHookuseWebSocket 不能接收到那条消息,但是源码也没过滤呀! 那问题到底在哪里呢?

重新审查代码

再去审查一下自己的代码。

ts 复制代码
const { readyState, latestMessage, disconnect, connect } = useWebSocketAHook(
   endpoint_websocket
);

useEffect(() => {
    // console.log(readyState, latestMessage, disconnect, connect)
    if (readyState === ReadyState.Open) {
      console.log("WebSocket message size:", latestMessage?.data?.length, "bytes");
      if (latestMessage?.data?.length > 1024 * 10) { // 10KB
        console.warn("Large message received:", latestMessage?.data?.substring(0, 100) + "...");
      }

      if (!latestMessage?.data) return;
      const msg = latestMessage?.data as unknown as string;

      console.log("55555", msg.includes("fetch_source"));
      if (msg.includes("websocket-session-id")) {
        console.log("websocket-session-id:", msg);
        const sessionId = msg.split(":")[1].trim();
        websocketIdRef.current = sessionId;
        console.log("sessionId:", sessionId);
        setConnected(true);
        if (onWebsocketSessionId) {
          onWebsocketSessionId(sessionId);
        }
        return;
      }
      dealMsgRef.current && dealMsgRef.current(msg);
    } else {
      setConnected(false);
    }
 }, [readyState, latestMessage?.data]);

忽然恍然大悟,是我取消息的方式的问题 useEffect。应该是它把消息丢了吧。

这个问题主要可能有几个原因:

  1. React 的批量更新机制可能会合并多个快速到达的消息更新,导致某些消息被跳过
  2. useEffect 的执行是异步的,当消息来得太快时,可能会跳过一些消息的处理
  3. 提前 return 的逻辑会导致后续消息被丢弃

React 状态更新机制导致的

当 WebSocket 推送消息过快导致 React 无法正常接收或处理消息时,通常是由 消息处理性能瓶颈React 状态更新机制 导致。

最终优化方案

那就改一下:

ts 复制代码
  // 使用 ahooks 的 WebSocket Hook 创建连接
  const { readyState, latestMessage, disconnect, connect } = useWebSocketAHook(
    "endpoint_websocket",
    {
      manual: false,                // 是否手动控制连接
      reconnectLimit: 3,           // 重连次数限制
      reconnectInterval: 3000,     // 重连间隔时间(毫秒)
      onMessage: (event) => {      // 消息接收回调
        const msg = event.data as string;
        messageQueue.current.push(msg);  // 将消息加入队列
        processQueue();                  // 处理消息队列
      },
    }
  );

  // 存储 websocket 会话 ID
  const websocketIdRef = useRef<string>('');
  // 消息队列,用于缓存接收到的消息
  const messageQueue = useRef<string[]>([]);
  // 消息处理锁,防止并发处理
  const processingRef = useRef(false);

  // 处理单条消息的函数
  const processMessage = useCallback((msg: string) => {
    try {
      // 处理 websocket 会话 ID 消息
      if (msg.includes("websocket-session-id")) {
        const sessionId = msg.split(":")[1].trim();
        websocketIdRef.current = sessionId;
        setConnected(true);
        onWebsocketSessionId?.(sessionId);
      } else {
        // 处理其他类型的消息
        dealMsgRef.current?.(msg);
      }
    } catch (error) {
      console.error('处理消息出错:', error, msg);
    }
  }, [onWebsocketSessionId]);

  // 处理消息队列的函数
  const processQueue = useCallback(() => {
    // 如果正在处理或队列为空,则返回
    if (processingRef.current || messageQueue.current.length === 0) return;
    
    // 设置处理锁
    processingRef.current = true;
    // 循环处理队列中的所有消息
    while (messageQueue.current.length > 0) {
      const msg = messageQueue.current.shift();
      if (msg) {
        console.log("处理消息:", msg.substring(0, 100));
        processMessage(msg);
      }
    }
    // 释放处理锁
    processingRef.current = false;
  }, [processMessage]);

  // 监听连接状态变化
  useEffect(() => {
    if (readyState !== ReadyState.Open) {
      setConnected(false);  // 连接断开时更新状态
    }
  }, [readyState]);

不错不错,终于解决问题了。

主要改进:

  1. 使用消息队列缓存所有接收到的消息
  2. 在 WebSocket 的 onMessage 回调中直接处理消息,而不是依赖 Effect
  3. 使用处理锁避免消息处理的并发问题
  4. 移除了提前返回的逻辑,确保所有消息都能被处理
  5. 分离了连接状态的监听

小结

问题的根源在于 React 状态更新机制和 useEffect 异步执行带来的隐患。当 WebSocket 消息到达速度较快时,可能出现部分消息未能及时处理的情况。通过引入消息队列、直接在回调中处理消息以及避免过早 return,我们解决了消息丢失的问题。

此次经历提醒我们,在处理高频消息推送时,合理设计消息缓存和处理流程是保证数据完整性的重要手段,同时也反映了在 React 中对状态更新和副作用处理要格外谨慎。

相关推荐
qq_386322693 小时前
华为网路设备学习-21 IGP路由专题-路由过滤(filter-policy)
前端·网络·学习
蓝婷儿8 小时前
前端面试每日三题 - Day 32
前端·面试·职场和发展
2501_915918418 小时前
多账号管理与自动化中的浏览器指纹对抗方案
websocket·网络协议·tcp/ip·http·网络安全·https·udp
星空寻流年9 小时前
CSS3(BFC)
前端·microsoft·css3
九月TTS9 小时前
开源分享:TTS-Web-Vue系列:Vue3实现固定顶部与吸顶模式组件
前端·vue.js·开源
CodeCraft Studio9 小时前
数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能
前端·javascript·ui·甘特图
一把年纪学编程9 小时前
【牛马技巧】word统计每一段的字数接近“字数统计”
前端·数据库·word
llc的足迹10 小时前
el-menu 折叠后小箭头不会消失
前端·javascript·vue.js
九月TTS10 小时前
TTS-Web-Vue系列:移动端侧边栏与响应式布局深度优化
前端·javascript·vue.js
Johnstons10 小时前
AnaTraf:深度解析网络性能分析(NPM)
前端·网络·安全·web安全·npm·网络流量监控·网络流量分析