背景
遇到一个奇怪的问题:通过 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 可以接收到那条消息,aHook 的 useWebSocket 不能接收到那条消息,但是源码也没过滤呀! 那问题到底在哪里呢?
重新审查代码
再去审查一下自己的代码。
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。应该是它把消息丢了吧。
这个问题主要可能有几个原因:
- React 的批量更新机制可能会合并多个快速到达的消息更新,导致某些消息被跳过
- useEffect 的执行是异步的,当消息来得太快时,可能会跳过一些消息的处理
- 提前 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]);
不错不错,终于解决问题了。
主要改进:
- 使用消息队列缓存所有接收到的消息
- 在 WebSocket 的 onMessage 回调中直接处理消息,而不是依赖 Effect
- 使用处理锁避免消息处理的并发问题
- 移除了提前返回的逻辑,确保所有消息都能被处理
- 分离了连接状态的监听
小结
问题的根源在于 React 状态更新机制和 useEffect 异步执行带来的隐患。当 WebSocket 消息到达速度较快时,可能出现部分消息未能及时处理的情况。通过引入消息队列、直接在回调中处理消息以及避免过早 return,我们解决了消息丢失的问题。
此次经历提醒我们,在处理高频消息推送时,合理设计消息缓存和处理流程是保证数据完整性的重要手段,同时也反映了在 React 中对状态更新和副作用处理要格外谨慎。