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

相关推荐
洁洁!10 分钟前
数据采集助力AI大模型训练
前端·人工智能·easyui
关山月11 分钟前
6 个常见的 React 反模式,正在损害你的代码质量
react.js
MiyueFE19 分钟前
bpmn-js 源码篇9:Moddle - 对象格式的标准化定义库
前端·javascript
excel25 分钟前
webpack 核心编译器 七 节
前端
一只月月鸟呀32 分钟前
HTML中数字和字母不换行显示
前端·html·css3
色的归属感41 分钟前
wireshark抓包分析数据怎么看 wireshark使用教程_wireshark怎么看
websocket·网络协议·tcp/ip·http·网络安全·https·udp
天下代码客1 小时前
【八股】介绍Promise(ES6引入)
前端·ecmascript·es6
lb29171 小时前
CSS 3D变换,transform:translateZ()
前端·css·3d
鹅肝手握高V五色1 小时前
App Usage v5.57 Pro版 追踪手机及应用使用情况
websocket·网络协议·tcp/ip·http·网络安全·https·udp
啊阿狸不会拉杆1 小时前
第二十二章:Python-NLTK库:自然语言处理
前端·python·自然语言处理