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 中对状态更新和副作用处理要格外谨慎。

相关推荐
漂流瓶jz16 分钟前
Webpack如何实现万物皆可import?loader的使用/配置/手写实践
前端·javascript·webpack
ZC跨境爬虫32 分钟前
跟着 MDN 学CSS day_41:显式轨道、隐式网格与区域命名放置
前端·javascript·css·ui·交互
weelinking41 分钟前
【产品】12_接入数据库——让数据永久保存
jvm·数据库·python·react.js·数据挖掘·前端框架·产品经理
修己xj1 小时前
告别手动存图!这款叫 Fatkun 的浏览器插件,简直是素材收集神器
前端
TickDB2 小时前
美股行情 API 接入避坑:REST 快照、WebSocket 推送、盘前盘后数据的边界
人工智能·python·websocket·行情数据 api
袋鼠云数栈2 小时前
从前端到基础设施,ACOS 如何打通企业全链路可观测
运维·前端·人工智能·数据治理·数据智能
AskHarries2 小时前
系统提示词、开发者指令和用户输入的优先级
java·前端·数据库
Moment2 小时前
长上下文会最终杀死 Rag 吗?
前端·javascript·后端
qcx233 小时前
【系统学AI】25 论文导读 ①:两篇改变 AI 的开山之作——Attention Is All You Need & ReAct
前端·人工智能·react.js·transformer
kyriewen4 小时前
大文件上传最全指南:分片、断点续传、秒传,一篇就够了
前端·javascript·面试