#include "websocketclient.h" #include #include #include #include #include #include #include #include #include #include #include #include "networkaccessmanager.h" #include WebSocketClient::WebSocketClient(QObject* parent) : QObject(parent) , m_connected(false) , m_autoReconnect(true) , m_reconnectAttempts(0) , m_maxReconnectAttempts(300) , m_isReconnecting(false) { // 连接WebSocket信号 connect(&m_webSocket, &QWebSocket::connected, this, &WebSocketClient::onConnected); connect(&m_webSocket, &QWebSocket::disconnected, this, &WebSocketClient::onDisconnected); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebSocketClient::onTextMessageReceived); connect(&m_webSocket, QOverload::of(&QWebSocket::error), this, &WebSocketClient::onError); // 设置心跳包定时器 connect(&m_pingTimer, &QTimer::timeout, this, &WebSocketClient::sendPing); m_pingTimer.setInterval(30000); // 30秒发送一次心跳 // 设置重连定时器 m_reconnectTimer.setSingleShot(true); connect(&m_reconnectTimer, &QTimer::timeout, this, &WebSocketClient::tryReconnect); } WebSocketClient::~WebSocketClient() { disconnect(); } void WebSocketClient::connectToRoom(const QString& roomId) { if (m_connected) { disconnect(); } m_roomId = roomId; m_reconnectAttempts = 0; // 重置重连尝试次数 QUrl url = buildWebSocketUrl(roomId); qDebug() << "connectToRoom" << url; // 设置请求头 QNetworkRequest request(url); const QString token = AppEvent::instance()->jwtToken(); if (!token.isEmpty()) { request.setRawHeader("Authorization", "Bearer " + token.toUtf8()); } qDebug() << "---" << token; request.setRawHeader("Machine-Code", AppEvent::instance()->machineCode().toUtf8()); request.setRawHeader("Accept-Language", AppEvent::instance()->locale().toUtf8()); // 连接WebSocket m_webSocket.open(request); } void WebSocketClient::disconnect() { m_autoReconnect = false; // 禁用自动重连 m_reconnectTimer.stop(); // 停止重连定时器 if (m_connected) { m_pingTimer.stop(); m_webSocket.close(); m_connected = false; emit connectionStateChanged(false); } } void WebSocketClient::sendMessage(const QString& content, const QString& type) { if (!m_connected) { emit errorOccurred("未连接到聊天室,无法发送消息"); return; } QJsonObject message; message["content"] = content; message["type"] = type; message["roomId"] = m_roomId; message["time"] = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate); QJsonDocument doc(message); m_webSocket.sendTextMessage(doc.toJson(QJsonDocument::Compact)); } void WebSocketClient::sendImageMessage(const QString& imagePath, const QString& text) { if (!m_connected) { emit errorOccurred("未连接到聊天室,无法发送图片消息"); return; } // 检查文件是否存在 QFileInfo fileInfo(imagePath); if (!fileInfo.exists() || !fileInfo.isFile()) { emit errorOccurred("图片文件不存在: " + imagePath); return; } // 读取图片文件并转换为Base64 QFile file(imagePath); if (!file.open(QIODevice::ReadOnly)) { emit errorOccurred("无法读取图片文件: " + imagePath); return; } QByteArray imageData = file.readAll(); file.close(); // 检查文件大小限制(例如5MB) const qint64 maxFileSize = 5 * 1024 * 1024; // 5MB if (imageData.size() > maxFileSize) { emit errorOccurred("图片文件过大,请选择小于5MB的图片"); return; } // 获取文件扩展名来确定MIME类型 QString mimeType = "image/jpeg"; // 默认 QString suffix = fileInfo.suffix().toLower(); if (suffix == "png") { mimeType = "image/png"; } else if (suffix == "gif") { mimeType = "image/gif"; } else if (suffix == "bmp") { mimeType = "image/bmp"; } else if (suffix == "webp") { mimeType = "image/webp"; } // 构建符合WSMessage结构的消息 QJsonObject message; message["type"] = "room"; // 使用room类型 message["roomId"] = m_roomId; message["time"] = QDateTime::currentDateTime().toUTC().toString(Qt::ISODate); // 如果有附加文本消息,放在content字段 if (!text.isEmpty()) { message["content"] = text; } // 图片数据放在data字段中 QJsonObject imageData_obj; imageData_obj["messageType"] = "image"; // 标识这是图片消息 imageData_obj["imageData"] = QString::fromLatin1(imageData.toBase64()); imageData_obj["mimeType"] = mimeType; imageData_obj["fileName"] = fileInfo.fileName(); imageData_obj["fileSize"] = imageData.size(); message["data"] = imageData_obj; QJsonDocument doc(message); m_webSocket.sendTextMessage(doc.toJson(QJsonDocument::Compact)); } bool WebSocketClient::isConnected() const { return m_connected; } void WebSocketClient::onConnected() { m_connected = true; m_isReconnecting = false; m_reconnectAttempts = 0; // 连接成功后重置重连计数 m_pingTimer.start(); emit connectionStateChanged(true); } void WebSocketClient::onDisconnected() { m_connected = false; m_pingTimer.stop(); emit connectionStateChanged(false); // 如果启用了自动重连,且当前不在重连过程中,则尝试重连 if (m_autoReconnect && !m_roomId.isEmpty() && !m_isReconnecting) { qDebug() << "WebSocket断开连接,准备重连..."; // 使用指数退避策略,每次重连间隔时间增加 int reconnectDelay = 1000 * (1 << qMin(m_reconnectAttempts, 5)); // 最大延迟32秒 m_reconnectTimer.start(reconnectDelay); } } void WebSocketClient::onTextMessageReceived(const QString& message) { QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { emit errorOccurred("解析消息失败: " + error.errorString()); return; } if (doc.isObject()) { QJsonObject obj = doc.object(); qDebug() << "----------------------------->" << obj; // 处理心跳响应 if (obj.contains("type") && obj["type"].toString() == "pong") { return; } // 处理聊天消息 ChatMessage chatMessage; const QString type = obj["type"].toString(); const QString content = obj["content"].toString(); // 处理特殊消息类型 if (type == "ping") { return; } else if (type == "room_live_status") { emit liveStatus(content); return; } else if (type == "stats_update") { emit statsUpdate(obj); return; } // 设置消息类型 if (type == "room") { chatMessage.type = MessageType::Left; } else if (type == "system") { chatMessage.type = MessageType::System; } else if (type == "private") { chatMessage.type = MessageType::Right; } // 检查是否包含扩展数据(图片等) if (obj.contains("data") && !obj["data"].isNull()) { QJsonObject dataObj = obj["data"].toObject(); QString messageType = dataObj["messageType"].toString(); if (messageType == "image") { // 处理图片消息 QString imageData = dataObj["imageData"].toString(); QString mimeType = dataObj["mimeType"].toString(); QString fileName = dataObj["fileName"].toString(); // 解码Base64图片数据 QByteArray decodedData = QByteArray::fromBase64(imageData.toLatin1()); // 创建临时文件保存图片 QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); QString tempFileName = QString("chat_image_%1_%2").arg(QDateTime::currentMSecsSinceEpoch()).arg(fileName); QString tempFilePath = QDir(tempDir).absoluteFilePath(tempFileName); QFile tempFile(tempFilePath); if (tempFile.open(QIODevice::WriteOnly)) { tempFile.write(decodedData); tempFile.close(); // 设置图片消息属性 chatMessage.imagePath = tempFilePath; chatMessage.contentType = content.isEmpty() ? ContentType::Image : ContentType::Mixed; // 获取图片尺寸 QImageReader reader(tempFilePath); if (reader.canRead()) { chatMessage.imageSize = reader.size(); } } else { qDebug() << "Failed to save image to temp file:" << tempFilePath; return; } } } else { // 纯文本消息 chatMessage.contentType = ContentType::Text; } // 设置消息内容 chatMessage.text = content; chatMessage.senderName = obj["fromUserName"].toString(); // 设置时间(如果服务器提供) if (obj.contains("time") && !obj["time"].toString().isEmpty()) { QDateTime msgTime = QDateTime::fromString(obj["time"].toString(), Qt::ISODate); if (msgTime.isValid()) { chatMessage.timestamp = msgTime; } else { chatMessage.timestamp = QDateTime::currentDateTime(); } } else { chatMessage.timestamp = QDateTime::currentDateTime(); } // 设置发送者ID(如果服务器提供) // if (obj.contains("senderId")) { // chatMessage.senderId = obj["senderId"].toString(); // } emit messageReceived(chatMessage); } } void WebSocketClient::onError(QAbstractSocket::SocketError error) { Q_UNUSED(error); emit errorOccurred("WebSocket错误: " + m_webSocket.errorString()); // 错误发生时不需要额外处理,因为错误通常会触发disconnected信号, // 在onDisconnected中已经处理了重连逻辑 } void WebSocketClient::sendPing() { if (m_connected) { QJsonObject pingMessage; pingMessage["type"] = "ping"; pingMessage["time"] = QDateTime::currentDateTime().toString(Qt::ISODate); QJsonDocument doc(pingMessage); m_webSocket.sendTextMessage(doc.toJson(QJsonDocument::Compact)); } } QUrl WebSocketClient::buildWebSocketUrl(const QString& roomId) { // 从HTTP URL构建WebSocket URL QString baseUrl = TC::RequestClient::globalInstance()->baseUrl(); // 将http(s)://替换为ws(s):// QString wsUrl = baseUrl; if (wsUrl.startsWith("https://")) { wsUrl.replace(0, 8, "wss://"); } else if (wsUrl.startsWith("http://")) { wsUrl.replace(0, 7, "ws://"); } if (wsUrl.isEmpty()) { wsUrl = "ws://127.0.0.1:8200"; } // 构建完整的WebSocket URL return QUrl(wsUrl + "/api/ws/chat/" + roomId); } void WebSocketClient::tryReconnect() { // 防止重复进入重连流程 if (m_isReconnecting || !m_autoReconnect || m_roomId.isEmpty() || m_connected) { return; } m_isReconnecting = true; m_reconnectAttempts++; if (m_reconnectAttempts <= m_maxReconnectAttempts) { qDebug() << "尝试重新连接WebSocket,第" << m_reconnectAttempts << "次尝试..."; emit reconnecting(m_reconnectAttempts, m_maxReconnectAttempts); // 确保WebSocket处于关闭状态 if (m_webSocket.state() != QAbstractSocket::UnconnectedState) { m_webSocket.abort(); QTimer::singleShot(500, this, &WebSocketClient::doReconnect); } else { doReconnect(); } } else { qDebug() << "WebSocket重连失败,已达到最大尝试次数"; emit reconnectFailed(); m_autoReconnect = false; // 禁用自动重连 m_isReconnecting = false; } } void WebSocketClient::doReconnect() { QUrl url = buildWebSocketUrl(m_roomId); QNetworkRequest request(url); const QString token = AppEvent::instance()->jwtToken(); if (!token.isEmpty()) { request.setRawHeader("Authorization", "Bearer " + token.toUtf8()); } request.setRawHeader("Machine-Code", AppEvent::instance()->machineCode().toUtf8()); request.setRawHeader("Accept-Language", AppEvent::instance()->locale().toUtf8()); m_webSocket.open(request); // 添加超时保护,防止连接过程卡死 QTimer::singleShot(10000, this, [this]() { if (!m_connected && m_isReconnecting) { qDebug() << "重连超时,中断当前连接尝试"; m_webSocket.abort(); m_isReconnecting = false; // 继续下一次重连尝试 if (m_autoReconnect && m_reconnectAttempts < m_maxReconnectAttempts) { int reconnectDelay = 1000 * (1 << qMin(m_reconnectAttempts, 5)); m_reconnectTimer.start(reconnectDelay); } else if (m_reconnectAttempts >= m_maxReconnectAttempts) { emit reconnectFailed(); m_autoReconnect = false; } } }); } void WebSocketClient::setAutoReconnect(bool enable) { m_autoReconnect = enable; } void WebSocketClient::setMaxReconnectAttempts(int maxAttempts) { m_maxReconnectAttempts = maxAttempts; }