Commit 4eed88c9 authored by ligaowei's avatar ligaowei

修复WebSocket消息处理中的类型错误问题:rawData.trim is not a function

parent 19aa1f99
# WebSocket消息处理修复说明
## 问题描述
在前端WebSocket服务中,处理文本消息时出现了"rawData.trim is not a function"的错误。错误发生在以下代码段:
```typescript
const rawData = event.data as string;
// ...
if (!rawData || rawData.trim().length === 0) {
// ...
}
```
## 问题原因
1. **类型假设错误**:代码假设`event.data`总是字符串类型,但实际上WebSocket消息可以是多种类型:
- `string`(文本消息)
- `ArrayBuffer`(二进制消息)
- `Blob`(二进制大对象)
2. **类型转换不当**:直接使用`as string`进行类型断言,而不是进行类型检查和适当转换。
3. **方法调用错误**:当`event.data`实际上是`ArrayBuffer`或其他非字符串类型时,调用`.trim()`方法会导致运行时错误。
## 解决方案
修改WebSocket消息处理逻辑,增加类型检查和适当的类型转换:
```typescript
// 文本消息处理(兼容旧协议或错误消息)
const rawData = event.data;
// 确保rawData是字符串类型
let rawString: string;
if (typeof rawData === 'string') {
rawString = rawData;
} else {
// 如果不是字符串,尝试转换为字符串
rawString = String(rawData);
}
const dataLength = rawString ? rawString.length : 0;
addLog(`接收到文本消息,长度: ${dataLength} 字符(已弃用,应使用二进制协议)`, 'debug');
if (!rawString || rawString.trim().length === 0) {
addLog('接收到空的WebSocket消息,跳过处理', 'warn');
return;
}
// 尝试解析为JSON(仅用于兼容旧数据)
try {
const parsedData = JSON.parse(rawString);
addLog('文本消息JSON解析成功,数据类型: ' + parsedData.type, 'info');
if (this.options.onMessage) {
this.options.onMessage(parsedData);
}
} catch (parseError) {
const errorMsg = (parseError as Error).message;
addLog('文本消息JSON解析失败: ' + errorMsg + '(建议检查是否应使用二进制协议)', 'warn');
}
```
## 修复要点
1. **类型安全检查**:使用`typeof rawData === 'string'`检查实际类型
2. **安全类型转换**:对于非字符串类型,使用`String(rawData)`进行转换
3. **保持功能一致**:确保修复后的行为与原逻辑一致
4. **增强健壮性**:避免因类型不匹配导致的运行时错误
## 测试验证
修复后应验证以下场景:
1. 正常文本消息处理
2. 空消息处理
3. 二进制消息处理(应走二进制分支)
4. 其他类型消息的安全处理
## 相关文件
- `frontend/src/services/websocketService.ts`:主要修复文件
- `frontend/src/services/binaryMessageHandler.ts`:二进制消息处理相关文件
## 后续建议
1. 强化类型检查,在处理WebSocket消息时明确区分文本和二进制消息
2. 增加更详细的日志记录,便于调试不同类型的WebSocket消息
3. 考虑逐步淘汰文本协议,全面转向更高效的二进制协议
\ No newline at end of file
// WebSocket服务
import { addLog } from '@/utils/logUtils';
import { BinaryFragmentBuffer, handleBinaryMessage } from './binaryMessageHandler';
interface WebSocketServiceOptions {
onMessage?: (data: any) => void;
onOpen?: () => void;
onClose?: (event: CloseEvent) => void;
onError?: (error: any) => void;
}
export class WebSocketService {
private ws: WebSocket | null = null;
private url: string = '';
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 5;
private reconnectDelay: number = 3000;
private connectionTimeout: number | null = null;
private options: WebSocketServiceOptions;
private binaryFragmentBuffer: BinaryFragmentBuffer = new BinaryFragmentBuffer();
constructor(options: WebSocketServiceOptions = {}) {
this.options = options;
}
connect(url: string) {
// 避免重复连接
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
addLog('WebSocket连接已存在且处于打开状态', 'info');
return;
}
// 如果已有连接但处于其他状态,先关闭它
if (this.ws) {
try {
addLog(`WebSocket当前状态: ${this.ws.readyState}`, 'info');
this.ws.close();
} catch (e) {
addLog('关闭旧WebSocket连接时出错: ' + (e as Error).message, 'error');
}
this.ws = null;
}
this.url = url;
addLog('正在连接WebSocket: ' + this.url, 'info');
try {
const ws = new WebSocket(this.url);
this.ws = ws;
// 设置连接超时
this.connectionTimeout = window.setTimeout(() => {
if (ws.readyState === WebSocket.CONNECTING) {
addLog('WebSocket连接超时(30秒未建立连接)。可能的原因:', 'error');
addLog('1. 后端服务未启动或无法访问 (ws://localhost:8080/ws/dom-sync)', 'warn');
addLog('2. 防火墙或网络配置阻止WebSocket连接', 'warn');
addLog('3. 反向代理或负载均衡器未正确配置WebSocket支持', 'warn');
addLog('4. 握手拦截器验证失败(如JWT Token验证失败)', 'warn');
try {
ws.close();
} catch (closeError) {
addLog('关闭WebSocket连接时出错: ' + (closeError as Error).message, 'error');
}
// 尝试重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000);
addLog(`WebSocket连接超时,${delay}ms后进行第${this.reconnectAttempts}次重连...`, 'warn');
setTimeout(() => this.connect(this.url), delay);
} else {
addLog('WebSocket连接超时,已达到最大重连次数,停止重连', 'error');
addLog('请检查后端服务状态或查看后端日志了解更多信息', 'info');
}
}
}, 30000); // 增加到30秒超时,给连接更多时间
// 连接打开事件
ws.onopen = () => {
if (this.connectionTimeout) {
clearTimeout(this.connectionTimeout);
this.connectionTimeout = null;
}
this.reconnectAttempts = 0;
addLog('WebSocket连接已建立,使用二进制协议', 'info');
// 启动定期清理过期消息缓存
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
this.cleanupInterval = window.setInterval(() => {
this.binaryFragmentBuffer.cleanupExpired();
}, 30000); // 每30秒检查一次
if (this.options.onOpen) {
this.options.onOpen();
}
};
// 接收消息事件
ws.onmessage = (event) => {
try {
// 检查消息类型:二进制或文本
if (event.data instanceof ArrayBuffer) {
// 二进制消息处理(推荐使用)
addLog('接收到二进制消息,大小: ' + (event.data as ArrayBuffer).byteLength + ' 字节', 'debug');
const success = handleBinaryMessage(
event.data as ArrayBuffer,
this.binaryFragmentBuffer,
(decodedData: any, encoding: number) => {
// 消息完整接收并处理成功
if (this.options.onMessage) {
this.options.onMessage(decodedData);
}
}
);
if (!success) {
addLog('二进制消息处理失败', 'warn');
}
} else {
// 文本消息处理(兼容旧协议或错误消息)
const rawData = event.data;
// 确保rawData是字符串类型
let rawString: string;
if (typeof rawData === 'string') {
rawString = rawData;
} else {
// 如果不是字符串,尝试转换为字符串
rawString = String(rawData);
}
const dataLength = rawString ? rawString.length : 0;
addLog(`接收到文本消息,长度: ${dataLength} 字符(已弃用,应使用二进制协议)`, 'debug');
if (!rawString || rawString.trim().length === 0) {
addLog('接收到空的WebSocket消息,跳过处理', 'warn');
return;
}
// 尝试解析为JSON(仅用于兼容旧数据)
try {
const parsedData = JSON.parse(rawString);
addLog('文本消息JSON解析成功,数据类型: ' + parsedData.type, 'info');
if (this.options.onMessage) {
this.options.onMessage(parsedData);
}
} catch (parseError) {
const errorMsg = (parseError as Error).message;
addLog('文本消息JSON解析失败: ' + errorMsg + '(建议检查是否应使用二进制协议)', 'warn');
}
}
} catch (e) {
addLog('处理WebSocket消息时发生异常:' + (e as Error).message, 'error');
addLog('错误堆栈:' + (e as Error).stack, 'debug');
}
};
// 连接关闭事件
ws.onclose = (event) => {
if (this.connectionTimeout) {
clearTimeout(this.connectionTimeout);
this.connectionTimeout = null;
}
addLog(`WebSocket连接已关闭,代码: ${event.code}, 原因: ${event.reason}`, 'info');
// 检查关闭原因
if (event.code === 1006) {
addLog('WebSocket连接异常关闭(代码1006),可能原因:', 'warn');
addLog('1. 网络中断或连接超时', 'info');
addLog('2. 服务器主动断开连接', 'info');
addLog('3. 防火墙、代理或中间件中断连接', 'info');
} else if (event.code !== 1000) {
addLog(`WebSocket异常关闭(代码${event.code}),需要检查服务器日志`, 'warn');
}
// 只有在非正常关闭的情况下才尝试重连
if (event.code !== 1000) { // 1000表示正常关闭
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
addLog(`WebSocket连接已断开,正在尝试第${this.reconnectAttempts}次重连...`, 'error');
// 指数退避重连策略
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000);
setTimeout(() => this.connect(this.url), delay); // 最大延迟30秒
} else {
addLog('WebSocket连接已断开,已达到最大重连次数,停止重连', 'error');
// 重置重连次数,以便用户手动重新连接
this.reconnectAttempts = 0;
}
} else {
addLog('WebSocket连接正常关闭,无需重连', 'info');
}
if (this.options.onClose) {
this.options.onClose(event);
}
};
// 连接错误事件
ws.onerror = (error) => {
if (this.connectionTimeout) {
clearTimeout(this.connectionTimeout);
this.connectionTimeout = null;
}
// 检查当前WebSocket状态来判断错误类型
const readyState = ws.readyState;
let errorDescription = '';
if (readyState === WebSocket.CLOSED) {
errorDescription = '握手失败:连接已关闭。通常是服务器拒绝连接(可能是认证失败、Token过期或无效)';
} else if (readyState === WebSocket.CLOSING) {
errorDescription = '连接正在关闭中';
} else if (readyState === WebSocket.CONNECTING) {
errorDescription = '连接超时或连接被中断';
} else {
errorDescription = '未知错误';
}
addLog('WebSocket错误 [状态码: ' + readyState + ']:' + errorDescription, 'error');
addLog('错误详情:' + (error as any).message, 'error');
addLog('URL: ' + this.url, 'error');
// 检查网络连接状态
if (!navigator.onLine) {
addLog('网络诊断:网络连接不可用,请检查您的网络连接', 'error');
} else {
addLog('网络诊断:网络连接正常,问题可能在服务器端', 'warn');
}
// 如果连接失败,尝试重新连接
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000);
addLog(`WebSocket连接错误,${delay}ms后进行第${this.reconnectAttempts}次重连...`, 'warn');
setTimeout(() => this.connect(this.url), delay);
} else {
addLog('WebSocket连接错误,已达到最大重连次数(' + this.maxReconnectAttempts + '),停止自动重连', 'error');
addLog('诊断建议:', 'info');
addLog('1. 检查后端服务是否正常运行(默认: ws://localhost:8080/ws/dom-sync)', 'info');
addLog('2. 检查JWT Token是否过期(Token有效期: 2小时)', 'info');
addLog('3. 检查浏览器控制台是否有详细的错误日志', 'info');
addLog('4. 检查后端是否输出认证拒绝的原因日志', 'info');
addLog('5. 可尝试手动刷新页面重新连接', 'info');
// 重置重连次数,以便用户手动重新连接
this.reconnectAttempts = 0;
}
if (this.options.onError) {
this.options.onError(error);
}
};
} catch (e) {
addLog('创建WebSocket连接失败: ' + (e as Error).message, 'error');
addLog('详细信息: ' + (e as Error).stack, 'debug');
addLog('诊断帮助:', 'info');
addLog('1. 检查URL格式是否正确: ' + this.url, 'info');
addLog('2. WebSocket创建失败可能指示浏览器不支持WebSocket或安全需求不满足', 'warn');
addLog('3. 检查HTTPS页面是否使用了WSS协议', 'warn');
// 如果创建连接失败,也尝试重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000);
addLog(`WebSocket连接创建失败,${delay}ms后进行第${this.reconnectAttempts}次重连...`, 'warn');
setTimeout(() => this.connect(this.url), delay);
} else {
addLog('WebSocket连接创建失败,已达到最大重连次数,停止重连', 'error');
addLog('提示:请检查网络连接或后端服务是否正常运行', 'info');
}
}
}
send(message: string) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(message);
addLog('已发送指令:' + message, 'info');
} catch (e) {
addLog('发送指令失败:' + (e as Error).message, 'error');
}
} else {
addLog('WebSocket连接已断开,无法发送指令', 'error');
// 尝试重新连接
this.connect(this.url);
}
}
close() {
if (this.ws) {
try {
this.ws.close();
addLog('WebSocket连接已关闭', 'info');
} catch (e) {
addLog('关闭WebSocket连接时出错: ' + (e as Error).message, 'error');
}
this.ws = null;
}
// 清理定时器
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
isConnected(): boolean {
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment