From bb509e54091621c478a2411d75eb6045fad19f60 Mon Sep 17 00:00:00 2001 From: Misaki Date: Tue, 30 Dec 2025 04:59:03 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E9=87=8D=E6=9E=84=E4=BA=86WebSocket?= =?UTF-8?q?=E7=B1=BB=E7=9A=84=E5=AE=9E=E7=8E=B0=EF=BC=8C=E5=B9=B6=E4=B8=94?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=85=B6=E4=B8=BA=E9=A1=B9=E7=9B=AE=E7=9A=84?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E9=80=9A=E4=BF=A1=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Live2D/Src/LAppLive2D/Src/LAppModel.cpp | 2 + CMakeLists.txt | 3 + src/Core/Src/GLCore.cpp | 43 +- src/DAO/Inc/NetWorkDO.h | 103 ++++ src/DAO/Src/NetWorkDO.cpp | 118 ++++ src/NetWorkHandle/Inc/websocketmanager.h | 175 ++++-- src/NetWorkHandle/Src/websocketmanager.cpp | 509 +++++++++++++++--- src/NetWorkHandle/readme.md | 236 ++++++++ src/Setting/Inc/NetworkPage.h | 27 +- src/Setting/Src/NetworkPage.cpp | 289 +++++----- 10 files changed, 1225 insertions(+), 280 deletions(-) create mode 100644 src/DAO/Inc/NetWorkDO.h create mode 100644 src/DAO/Src/NetWorkDO.cpp create mode 100644 src/NetWorkHandle/readme.md diff --git a/3rdparty/Live2D/Src/LAppLive2D/Src/LAppModel.cpp b/3rdparty/Live2D/Src/LAppLive2D/Src/LAppModel.cpp index 88d8a02..e72c16c 100644 --- a/3rdparty/Live2D/Src/LAppLive2D/Src/LAppModel.cpp +++ b/3rdparty/Live2D/Src/LAppLive2D/Src/LAppModel.cpp @@ -94,6 +94,8 @@ bool LAppModel::IsPointOnModel(const csmFloat32 x, const csmFloat32 y) return false; } // 如果有命中区域,使用HitTest(是否存在命中区域,这取决于模型是否定义这两部分信息) + // 不过不少模型不会写或者是不写入腿部信息,因此实际会出现点击腿部的时候判定为在模型外面,这并不是bug + // 也就是尽量依赖HitAreas,以此获得最好的性能,但是如果模型没有定义,则使用IsPointOnDrawable /* 通常是这样的信息,在.model3.json当中 "HitAreas": [ { diff --git a/CMakeLists.txt b/CMakeLists.txt index 883e444..3488d16 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,8 @@ file(GLOB_RECURSE YosugaSrc "src/AudioHandle/Inc/*.h" "src/Menu/Src/*.cpp" "src/Menu/Inc/*.h" + "src/DAO/Inc/*.h" + "src/DAO/Src/*.cpp" "src/NetWorkHandle/Src/*.cpp" "src/NetWorkHandle/Inc/*.h" "src/Setting/Src/*.cpp" @@ -193,6 +195,7 @@ target_include_directories(${PROJECT_NAME} src/Setting/Inc src/Render/TextRender/Inc src/Core/Inc + src/DAO/Inc ) diff --git a/src/Core/Src/GLCore.cpp b/src/Core/Src/GLCore.cpp index dd025c7..76c848f 100644 --- a/src/Core/Src/GLCore.cpp +++ b/src/Core/Src/GLCore.cpp @@ -25,7 +25,7 @@ QMap GLCore::frameRateMap = { {"240", 240.0} }; -GLCore::GLCore(int w, int h, QWidget *parent) +GLCore::GLCore(const int width, const int height, QWidget *parent) : QOpenGLWidget(parent), isLeftPressed(false), // 显式初始化 isRightPressed(false) // 显式初始化 @@ -52,9 +52,9 @@ GLCore::GLCore(int w, int h, QWidget *parent) contextMenu = new Menu(this); // 设置窗口大小 - setFixedSize(w, h); + setFixedSize(width, height); // 设置文本渲染器窗口大小 - TextRenderer::getInstance()->setWindowSize(w, h); + TextRenderer::getInstance()->setWindowSize(width, height); TextRenderer::getInstance()->setGlobalFont(QFont("Microsoft YaHei", 14, QFont::Bold)); TextRenderer::getInstance()->setHoldDuration(1.0f); // 停留1.2秒 TextRenderer::getInstance()->setGravity(600.0f); // 更快的下坠速度 @@ -164,8 +164,7 @@ void GLCore::closeEvent(QCloseEvent* event) } #ifdef Q_OS_WIN -void GLCore::setWindowTransparentForMouse(bool transparent) -{ +void GLCore::setWindowTransparentForMouse(const bool transparent) const { if (!hwnd) return; LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); @@ -182,7 +181,7 @@ void GLCore::setWindowTransparentForMouse(bool transparent) SetWindowLong(hwnd, GWL_EXSTYLE, exStyle); // 刷新窗口 - SetWindowPos(hwnd, 0, 0, 0, 0, 0, + SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); } #endif @@ -250,8 +249,36 @@ void GLCore::mousePressEvent(QMouseEvent* event) // TODO: 右键菜单等 if (event->button() == Qt::RightButton) { // 在鼠标右键点击的位置创建菜单,显示自定义右键菜单 - contextMenu->showMenu(event->globalPos()); - this->isRightPressed = true; + if (onModel) { + contextMenu->showMenu(event->globalPos()); + this->isRightPressed = true; + } + else { +#ifdef Q_OS_WIN + // 设置窗口为鼠标穿透 + setWindowTransparentForMouse(true); + + // 发送鼠标按下事件到底层窗口 + POINT pt = { event->globalPos().x(), event->globalPos().y() }; + HWND hWndBelow = WindowFromPoint(pt); + if (hWndBelow && hWndBelow != hwnd) { + // 转换坐标 + ScreenToClient(hWndBelow, &pt); + + // 发送鼠标按下消息 + PostMessage(hWndBelow, WM_LBUTTONDOWN, + MK_LBUTTON, MAKELPARAM(pt.x, pt.y)); + PostMessage(hWndBelow, WM_LBUTTONUP, + 0, MAKELPARAM(pt.x, pt.y)); + } + + // 恢复窗口不穿透状态(下一次鼠标移动时会重新检测) + QTimer::singleShot(100, this, [this]() { + setWindowTransparentForMouse(false); + }); +#endif + this->isRightPressed = false; + } } } diff --git a/src/DAO/Inc/NetWorkDO.h b/src/DAO/Inc/NetWorkDO.h new file mode 100644 index 0000000..5db4704 --- /dev/null +++ b/src/DAO/Inc/NetWorkDO.h @@ -0,0 +1,103 @@ +// +// Created by misaki on 2025/12/29. +// +#pragma once + +/** + * 本类为网络数据流会使用到的数据访问对象封装 + * 目的是便于统一数据访问方式 + * 同时屏蔽了端到端数据交换格式,使得上层调用不再需要关心数据格式,而只需要填入数据即可 + */ + +/** + * 简单描述一下Yosuga客户端所需要使用到的数据 + * 主要为音频数据,控制信息,文本信息。 + * 其中文本信息与音频数据为捆绑收发,并且其中还包括了一些特别的信息,例如音频时长等 + * 控制信息与各种业务逻辑相关,例如模拟点击,模拟输入等 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * 数据传输对象 (DTO) 定义 + */ +// 音频文本捆绑数据结构 +struct AudioDataPacket { + QString text; // 文本内容 + QByteArray audioData; // 音频原始数据 (二进制) + int sampleRate; // 采样率 + int channels; // 通道数 + qint64 duration; // 时长 (ms) + + AudioDataPacket() : sampleRate(16000), channels(1), duration(0) {} +}; + +// 控制指令数据结构 (预留) +enum class ControlType { + Click, + Input, + Scroll +}; + +struct ControlDataPacket { + ControlType action; + int x; + int y; + QString extraData; +}; + +/** + * NetworkDO + */ +class NetworkDO final : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(NetworkDO) // 禁用拷贝 + +public: + // 单例访问点 + static NetworkDO* getInstance(); + // 显式销毁 + static void destroy(); + + // 定义发送回调函数类型 + using SenderFunc = std::function; + +public: + // 注入发送接口 + void registerSender(SenderFunc sender); + + // 业务发送函数 + void sendAudioPacket(const AudioDataPacket& packet); + void sendControlPacket(const ControlDataPacket& packet); + +signals: + // 业务接收信号 + void audioPacketReceived(const AudioDataPacket& packet); + void controlPacketReceived(const ControlDataPacket& packet); + void errorOccurred(const QString& errorMsg); + +public slots: + // 接收底层 JSON 数据 + void onDataReceived(const QString& type, const QJsonObject& data); +public: + ~NetworkDO() override; +private: + // 构造/析构函数私有化 + explicit NetworkDO(QObject *parent = nullptr); + + // 内部处理逻辑 + void handleAudioMessage(const QJsonObject& data); + + static QScopedPointer m_instance; + static QMutex m_mutex; + + SenderFunc m_sender; // 注入的发送器 +}; \ No newline at end of file diff --git a/src/DAO/Src/NetWorkDO.cpp b/src/DAO/Src/NetWorkDO.cpp new file mode 100644 index 0000000..b168e73 --- /dev/null +++ b/src/DAO/Src/NetWorkDO.cpp @@ -0,0 +1,118 @@ +// +// Created by misaki on 2025/12/29. +// +#include "NetWorkDO.h" + +#include +#include + +// 初始化静态成员 +QScopedPointer NetworkDO::m_instance; +QMutex NetworkDO::m_mutex; + +// 单例实现 (QScopedPointer + Mutex) +NetworkDO* NetworkDO::getInstance() +{ + if (m_instance.isNull()) { + QMutexLocker locker(&m_mutex); + if (m_instance.isNull()) { + // 使用 reset 创建实例,因为构造函数是私有的 + m_instance.reset(new NetworkDO()); + } + } + return m_instance.data(); +} + +void NetworkDO::destroy() +{ + QMutexLocker locker(&m_mutex); + if (!m_instance.isNull()) { + m_instance.reset(); // 这会触发析构函数 + } +} + +NetworkDO::NetworkDO(QObject *parent) : QObject(parent) +{ + qDebug() << "NetworkDO initialized"; +} + +NetworkDO::~NetworkDO() +{ + qDebug() << "NetworkDO destroyed"; +} + +// 业务逻辑实现 +void NetworkDO::registerSender(SenderFunc sender) +{ + QMutexLocker locker(&m_mutex); // 简单保护一下赋值 + m_sender = std::move(sender); +} + +void NetworkDO::sendAudioPacket(const AudioDataPacket& packet) +{ + // 检查发送器是否已注入 + if (!m_sender) { + emit errorOccurred("Sender not registered! Call registerSender() first."); + return; + } + + // 封装数据 (DTO -> JSON) + QJsonObject dataObj; + dataObj["text"] = packet.text; + // 音频转 Base64 字符串传输 + dataObj["audio"] = QString::fromLatin1(packet.audioData.toBase64()); + dataObj["sampleRate"] = packet.sampleRate; + dataObj["channels"] = packet.channels; + dataObj["duration"] = packet.duration; + + // 调用底层发送 (解耦) + // "textAudio" 是与后端约定的协议类型 + m_sender("textAudio", dataObj); +} + +void NetworkDO::sendControlPacket(const ControlDataPacket& packet) +{ + if (!m_sender) return; + + QJsonObject dataObj; + dataObj["action"] = static_cast(packet.action); + dataObj["x"] = packet.x; + dataObj["y"] = packet.y; + + m_sender("control", dataObj); +} + +void NetworkDO::onDataReceived(const QString& type, const QJsonObject& data) +{ + // 根据类型分发 + if (type == "textAudio") { + handleAudioMessage(data); + } + else if (type == "control") { + // handleControlMessage(data); + } + else { + qWarning() << "[NetworkDO] Received unknown type:" << type; + } +} + +void NetworkDO::handleAudioMessage(const QJsonObject& data) +{ + AudioDataPacket packet; + + // 解析基础字段 (JSON -> DTO) + packet.text = data.value("text").toString(); + packet.sampleRate = data.value("sampleRate").toInt(16000); + packet.channels = data.value("channels").toInt(1); + // 注意类型转换,确保 long long 精度 + packet.duration = static_cast(data.value("duration").toDouble()); + + // 解析音频 (Base64 -> Binary) + QString base64Audio = data.value("audio").toString(); + if (!base64Audio.isEmpty()) { + packet.audioData = QByteArray::fromBase64(base64Audio.toLatin1()); + } + + // 通知上层业务 + emit audioPacketReceived(packet); +} \ No newline at end of file diff --git a/src/NetWorkHandle/Inc/websocketmanager.h b/src/NetWorkHandle/Inc/websocketmanager.h index 09b5f56..19647ac 100644 --- a/src/NetWorkHandle/Inc/websocketmanager.h +++ b/src/NetWorkHandle/Inc/websocketmanager.h @@ -1,59 +1,150 @@ // // Created by Administrator on 2025/2/4. // +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include /** - * 已废弃 + * 2025.12.25重构 Misaki + * 多线程websocket实现 */ - -#ifndef AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H -#define AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H - - -#include -#include - -class WebSocketManager : public QObject +class WebSocketManager final : public QObject { Q_OBJECT public: - explicit WebSocketManager(QObject *parent = nullptr); - - ~WebSocketManager(); - - // 上传接口(传入本地文件路径) - Q_INVOKABLE void uploadFile(const QString &filePath); - - // 连接服务器 - void connectToServer(); - - // 断开服务器 - void disconnectFromServer(); - - // 设置服务器地址get&set方法 - void setUrl(const QString &url); - QString getUrl(); + // 构造函数 + explicit WebSocketManager(QObject *parent = nullptr); // 不携带URL参数 + ~WebSocketManager() override; + // 删除拷贝构造函数和赋值操作符 + WebSocketManager(const WebSocketManager&) = delete; + WebSocketManager& operator=(const WebSocketManager&) = delete; +public: + bool setRequestContent(const QString& requestToken); // 设置自定义websocket首次请求Token(鉴权用) 服务端应保持Token一致 + bool setSocketUrl(QUrl url); // 设置URL signals: - // 上传进度(0-100) - void uploadProgressChanged(int percent); - // 下载进度(0-100) - void downloadProgressChanged(int percent); - // 文件处理完成(返回保存路径) - void fileProcessed(const QString &filePath); - // 错误通知 - void errorOccurred(const QString &message); + // 发给主线程的信号 + void connected(); // 连接 + void disconnected(); // 断开 + void textReceived(const QString &message); // 接收文本 + void jsonReceived(const QString &type, const QJsonObject &data); // 接收JSON + void binaryReceived(const QByteArray &data); // 接收二进制数据 + void error(const QString &errorMsg); // 错误 + void log(const QString &msg); // 日志 + void reconnecting(int attempt); // 重连 + +public slots: + // 主线程调用的槽 + bool connectToServer(); // 连接到服务器 + void disconnectFromServer(); // 断开连接 + void sendText(const QString &message); // 发送文本 + void sendJson(const QString &type, const QJsonObject &data); // 发送JSON + void sendBinary(const QByteArray &data); // 发送二进制数据 + void setReconnectEnabled(bool enabled); // 设置重连 + void setRequestEnabled(bool enabled); // 设置自定义首次请求 + + [[nodiscard]] bool isConnected() const; // 是否已连接 private slots: - void onConnected(); - void onBinaryMessageReceived(const QByteArray &message); - void onError(QAbstractSocket::SocketError error); + void onConnected(); // 连接成功 + void onDisconnected(); // 断开连接 + void onTextMessageReceived(const QString &message); // 接收到文本消息 + void onError(QAbstractSocket::SocketError socketError); // 错误 + void onSslErrors(const QList &errors); // SSL错误 + void onPong(quint64 elapsedTime, const QByteArray &payload); // Pong + void sendPing() const; // 发送Ping + void tryReconnect(); // 尝试重连 private: - QWebSocket m_socket; - QByteArray m_receivedData; - qint64 m_totalFileSize = 0; - QString url = "ws://localhost:8765"; + QWebSocket *m_socket; /// WebSocket对象 + QUrl m_url; /// 服务器地址 + QTimer *m_pingTimer; /// Ping定时器 + QTimer *m_reconnectTimer; /// 重连定时器 + int m_reconnectAttempts; /// 重连尝试次数 + bool m_isReconnectEnabled; /// 是否启用重连 + QNetworkRequest m_request; /// 自定义websocket首次请求(鉴权用) + bool m_isRequest; /// 是否启用websocket首次请求 }; -#endif //AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H +/** + * WebSocket 客户端单例管理类 + * 负责管理 WebSocket 线程和全局访问 + */ +class WebSocketClient final : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(WebSocketClient) + +public: + static WebSocketClient* getInstance(); + static void destroy(); + + // 初始化/重新配置 WebSocket + bool setConfiguration(const QUrl& url, const QString& authToken = QString()); + + // WebSocket 操作 + void connectToServer(); + void disconnectFromServer(); + void reconnect(); + + void sendText(const QString& message); + void sendJson(const QString& type, const QJsonObject& data); + void sendBinary(const QByteArray& data); + + [[nodiscard]] bool isConnected() const; + [[nodiscard]] bool hasConfiguration() const { return m_url.isValid(); } + + void setAutoReconnect(bool enabled); + void setPingInterval(int milliseconds); + + [[nodiscard]] QUrl currentUrl() const { return m_url; } + [[nodiscard]] QString currentToken() const { return m_authToken; } + + // 获取内部管理器(仅供高级使用) + [[nodiscard]] WebSocketManager* manager() const { return m_webSocketManager; } + +signals: + // WebSocket 事件 + void connected(); + void disconnected(); + void textReceived(const QString &message); + void jsonReceived(const QString &type, const QJsonObject &data); + void binaryReceived(const QByteArray &data); + void error(const QString &errorMsg); + void log(const QString &msg); + void reconnecting(int attempt); + + // 配置变更 + void configurationChanged(const QUrl& oldUrl, const QUrl& newUrl); + + // 内部信号(用于跨线程通信) + void internalSetUrl(const QUrl& url); + void internalSetAuthToken(const QString& token); + void internalConnect(); + void internalDisconnect(); + void internalSendText(const QString& message); + void internalSendJson(const QString& type, const QJsonObject& data); + void internalSendBinary(const QByteArray& data); + void internalSetAutoReconnect(bool enabled); + void internalSetRequestEnabled(bool enabled); +public: + ~WebSocketClient() override; +private: + explicit WebSocketClient(QObject *parent = nullptr); + + static QMutex m_mutex; + static QScopedPointer m_instance; + + QThread* m_workerThread; + WebSocketManager* m_webSocketManager; + QUrl m_url; + QString m_authToken; + bool m_hasAuthToken; +}; \ No newline at end of file diff --git a/src/NetWorkHandle/Src/websocketmanager.cpp b/src/NetWorkHandle/Src/websocketmanager.cpp index 26a1196..0b4c616 100644 --- a/src/NetWorkHandle/Src/websocketmanager.cpp +++ b/src/NetWorkHandle/Src/websocketmanager.cpp @@ -2,124 +2,473 @@ // Created by Administrator on 2025/2/4. // - #include "websocketmanager.h" -#include -#include #include -#include +#include +#include +#include +/// WebSocketManager WebSocketManager::WebSocketManager(QObject *parent) - : QObject(parent) + : QObject(parent) + , m_socket(new QWebSocket) // 创建 WebSocket 对象 + , m_pingTimer(new QTimer(this)) + , m_reconnectTimer(new QTimer(this)) + , m_reconnectAttempts(0) // 重连尝试次数初始为0 + , m_isReconnectEnabled(false) // 默认不启用重连 + , m_isRequest(false) // 默认不启用自定义首次请求 { - // 连接信号槽 - connect(&m_socket, &QWebSocket::connected, + // 配置 WebSocket + m_socket->setParent(this); // 确保 socket 也在工作线程 + // 连接信号 + connect(m_socket, &QWebSocket::connected, this, &WebSocketManager::onConnected); - connect(&m_socket, &QWebSocket::binaryMessageReceived, - this, &WebSocketManager::onBinaryMessageReceived); - connect(&m_socket, QOverload::of(&QWebSocket::error), + connect(m_socket, &QWebSocket::disconnected, + this, &WebSocketManager::onDisconnected); + connect(m_socket, &QWebSocket::textMessageReceived, + this, &WebSocketManager::onTextMessageReceived); + connect(m_socket, QOverload::of(&QWebSocket::errorOccurred), this, &WebSocketManager::onError); + connect(m_socket, &QWebSocket::sslErrors, + this, &WebSocketManager::onSslErrors); + connect(m_socket, &QWebSocket::pong, + this, &WebSocketManager::onPong); + // 心跳定时器 + m_pingTimer->setInterval(30000); // 30秒 + connect(m_pingTimer, &QTimer::timeout, this, &WebSocketManager::sendPing); + // 重连定时器 + m_reconnectTimer->setSingleShot(true); + connect(m_reconnectTimer, &QTimer::timeout, this, &WebSocketManager::tryReconnect); } -WebSocketManager::~WebSocketManager() -{ - m_socket.close(); +WebSocketManager::~WebSocketManager() { + if (m_socket) { + m_socket->close(); + m_socket->deleteLater(); + } +} +bool WebSocketManager::setRequestContent(const QString& requestToken) { + if (this->m_url.isEmpty()) { + emit error("URL is empty!"); + return false; + } + m_request.setUrl(this->m_url); + m_request.setRawHeader("Authorization", requestToken.toUtf8()); // 设置请求头(包含鉴权Token) + return true; } -void WebSocketManager::uploadFile(const QString &filePath) +bool WebSocketManager::setSocketUrl(QUrl url) { + if (this->m_url == url) {return true;} // 如果 URL 没有变化,直接返回成功 + // 判断URL是否合法,即是否符合websocket的格式 + if (!url.isValid() || url.scheme() != "ws" && url.scheme() != "wss") { + emit error("Invalid URL!"); + return false; + } + this->m_url = std::move(url); // 设置 URL + return true; +} + +bool WebSocketManager::connectToServer() { + if (m_socket->state() != QAbstractSocket::UnconnectedState) { // 如果已经连接,则先断开连接重新连接 + m_socket->close(); + } + if (m_url.isEmpty()) { // 如果 URL 为空 + emit error("URL is empty!"); + return false; + } + emit log(QString("Connecting to %1...").arg(m_url.toString())); + + // SSL 配置 + if (m_url.scheme() == "wss") { + QSslConfiguration sslConfig = m_socket->sslConfiguration(); + sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); // 开发环境 + m_socket->setSslConfiguration(sslConfig); + } + if (m_isRequest) { // 如果需要自定义首次请求 + m_socket->open(this->m_request); // 使用重载函数 + } + else { + m_socket->open(m_url); + } + return true; +} + +void WebSocketManager::disconnectFromServer() { + m_isReconnectEnabled = false; // 手动断开不重连 + if (m_socket->state() != QAbstractSocket::UnconnectedState) { + m_socket->close(QWebSocketProtocol::CloseCodeNormal, "Client closed"); + } +} + +void WebSocketManager::sendText(const QString &message) { + if (m_socket->state() == QAbstractSocket::ConnectedState) { + m_socket->sendTextMessage(message); + } +} + +void WebSocketManager::sendJson(const QString &type, const QJsonObject &data) { + QJsonObject wrapper; + wrapper["type"] = type; + wrapper["data"] = data; + wrapper["timestamp"] = QDateTime::currentMSecsSinceEpoch(); + + sendText(QJsonDocument(wrapper).toJson(QJsonDocument::Compact)); +} + +void WebSocketManager::sendBinary(const QByteArray &data) { + if (m_socket->state() == QAbstractSocket::ConnectedState) { + m_socket->sendBinaryMessage(data); + } +} + +bool WebSocketManager::isConnected() const { + return m_socket->state() == QAbstractSocket::ConnectedState; +} + +void WebSocketManager::setReconnectEnabled(bool enabled) { + m_isReconnectEnabled = enabled; + if (!enabled) { + m_reconnectTimer->stop(); + } +} +void WebSocketManager::setRequestEnabled(const bool enabled) { + m_isRequest = enabled; +} + +// 私有槽 +void WebSocketManager::onConnected() { + m_reconnectAttempts = 0; + emit log("✅ WebSocket connected"); + emit connected(); + m_pingTimer->start(); +} + +void WebSocketManager::onDisconnected() { + emit log("⚠️ WebSocket disconnected"); + emit disconnected(); + m_pingTimer->stop(); + + // 自动重连 + if (m_isReconnectEnabled) { + m_reconnectTimer->start(3000); // 3秒后重试 + } +} + +void WebSocketManager::onTextMessageReceived(const QString &message) { + emit textReceived(message); + + // 自动解析 JSON + const QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8()); + if (doc.isObject()) { + const QJsonObject obj = doc.object(); + const QString type = obj.value("type").toString(); + const QJsonObject data = obj.value("data").toObject(); + if (!type.isEmpty()) { + emit jsonReceived(type, data); + } + } +} + +void WebSocketManager::onError(QAbstractSocket::SocketError socketError) { + QString errorMsg; + switch (socketError) { + case QAbstractSocket::ConnectionRefusedError: + errorMsg = "Connection refused"; + break; + case QAbstractSocket::RemoteHostClosedError: + errorMsg = "Remote host closed"; + break; + case QAbstractSocket::HostNotFoundError: + errorMsg = "Host not found"; + break; + case QAbstractSocket::SocketTimeoutError: + errorMsg = "Socket timeout"; + break; + case QAbstractSocket::NetworkError: + errorMsg = "Network error"; + break; + case QAbstractSocket::SslHandshakeFailedError: + errorMsg = "SSL handshake failed"; + break; + default: + errorMsg = m_socket->errorString(); + } + emit error(QString("Socket error: %1").arg(errorMsg)); +} + +void WebSocketManager::onSslErrors(const QList &errors) { + foreach (const QSslError &err, errors) { + emit log(QString("SSL error: %1").arg(err.errorString())); + } +#ifdef QT_DEBUG + m_socket->ignoreSslErrors(); // 开发环境忽略 +#endif +} + +void WebSocketManager::sendPing() const { + if (m_socket->state() == QAbstractSocket::ConnectedState) { + m_socket->ping(); + } +} + +void WebSocketManager::onPong(quint64 elapsedTime, const QByteArray &) { + emit log(QString("Pong received, latency: %1ms").arg(elapsedTime)); +} + +void WebSocketManager::tryReconnect() { + if (!m_isReconnectEnabled) return; + + m_reconnectAttempts++; + emit reconnecting(m_reconnectAttempts); + emit log(QString("🔄 Reconnecting... (attempt %1)").arg(m_reconnectAttempts)); + + m_socket->open(m_url); +} + +#include +QMutex WebSocketClient::m_mutex; +QScopedPointer WebSocketClient::m_instance; + +WebSocketClient* WebSocketClient::getInstance() { - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly)) { - emit errorOccurred(tr("无法打开文件: %1").arg(filePath)); + QMutexLocker locker(&m_mutex); + if (m_instance.isNull()) { + m_instance.reset(new WebSocketClient()); + } + return m_instance.data(); +} + +void WebSocketClient::destroy() +{ + QMutexLocker locker(&m_mutex); + if (!m_instance.isNull()) { + m_instance.reset(); + } +} + +WebSocketClient::WebSocketClient(QObject *parent) + : QObject(parent) + , m_workerThread(nullptr) + , m_webSocketManager(nullptr) + , m_hasAuthToken(false) +{ + // 创建工作线程 + m_workerThread = new QThread(this); + m_workerThread->setObjectName("WebSocketWorkerThread"); + + // 创建 WebSocketManager + m_webSocketManager = new WebSocketManager(); + m_webSocketManager->moveToThread(m_workerThread); + + // 连接线程结束信号 + connect(m_workerThread, &QThread::finished, + m_webSocketManager, &QObject::deleteLater); + + // 连接 WebSocketManager 的信号到本类的信号(转发到主线程) + connect(m_webSocketManager, &WebSocketManager::connected, + this, &WebSocketClient::connected); + connect(m_webSocketManager, &WebSocketManager::disconnected, + this, &WebSocketClient::disconnected); + connect(m_webSocketManager, &WebSocketManager::textReceived, + this, &WebSocketClient::textReceived); + connect(m_webSocketManager, &WebSocketManager::jsonReceived, + this, &WebSocketClient::jsonReceived); + connect(m_webSocketManager, &WebSocketManager::binaryReceived, + this, &WebSocketClient::binaryReceived); + connect(m_webSocketManager, &WebSocketManager::error, + this, &WebSocketClient::error); + connect(m_webSocketManager, &WebSocketManager::log, + this, &WebSocketClient::log); + connect(m_webSocketManager, &WebSocketManager::reconnecting, + this, &WebSocketClient::reconnecting); + + // 连接本类的内部信号到 WebSocketManager 的槽(跨线程调用) + // 注意:由于 internal* 信号是私有信号,我们需要使用 lambda 包装器 + connect(this, &WebSocketClient::internalSetUrl, + m_webSocketManager, [this](const QUrl& url) { + bool success = m_webSocketManager->setSocketUrl(url); + if (!success) { + emit error("Failed to set WebSocket URL"); + } + }, Qt::QueuedConnection); + + connect(this, &WebSocketClient::internalSetAuthToken, + m_webSocketManager, [this](const QString& token) { + if (!token.isEmpty()) { + bool success = m_webSocketManager->setRequestContent(token); + if (!success) { + emit error("Failed to set authentication token"); + } + m_webSocketManager->setRequestEnabled(true); + } else { + m_webSocketManager->setRequestEnabled(false); + } + }, Qt::QueuedConnection); + + connect(this, &WebSocketClient::internalConnect, + m_webSocketManager, &WebSocketManager::connectToServer, + Qt::QueuedConnection); + + connect(this, &WebSocketClient::internalDisconnect, + m_webSocketManager, &WebSocketManager::disconnectFromServer, + Qt::QueuedConnection); + + connect(this, &WebSocketClient::internalSendText, + m_webSocketManager, &WebSocketManager::sendText, + Qt::QueuedConnection); + + connect(this, &WebSocketClient::internalSendJson, + m_webSocketManager, &WebSocketManager::sendJson, + Qt::QueuedConnection); + + connect(this, &WebSocketClient::internalSendBinary, + m_webSocketManager, &WebSocketManager::sendBinary, + Qt::QueuedConnection); + + connect(this, &WebSocketClient::internalSetAutoReconnect, + m_webSocketManager, &WebSocketManager::setReconnectEnabled, + Qt::QueuedConnection); + + // 启动工作线程 + m_workerThread->start(); + + qDebug() << "WebSocketClient initialized, worker thread:" << m_workerThread; +} + +WebSocketClient::~WebSocketClient() +{ + qDebug() << "Shutting down WebSocketClient..."; + + // 断开连接 + disconnectFromServer(); + + // 停止工作线程 + if (m_workerThread && m_workerThread->isRunning()) { + m_workerThread->quit(); + if (!m_workerThread->wait(3000)) { + m_workerThread->terminate(); + m_workerThread->wait(); + } + delete m_workerThread; + m_workerThread = nullptr; + } +} + +bool WebSocketClient::setConfiguration(const QUrl& url, const QString& authToken) +{ + if (!url.isValid()) { + emit error("Invalid URL provided"); + return false; + } + + QUrl oldUrl = m_url; + m_url = url; + m_authToken = authToken; + m_hasAuthToken = !authToken.isEmpty(); + + // 发送到工作线程进行配置 + emit internalSetUrl(url); + if (!authToken.isEmpty()) { + emit internalSetAuthToken(authToken); + } + + // 通知配置变更 + if (oldUrl != url) { + emit configurationChanged(oldUrl, url); + } + + emit log(QString("WebSocket configuration updated: %1").arg(url.toString())); + return true; +} + +void WebSocketClient::connectToServer() +{ + if (!m_url.isValid()) { + emit error("WebSocket URL not configured. Call setConfiguration() first."); return; } - // 读取并压缩数据 - QByteArray rawData = file.readAll(); - QByteArray compressedData = qCompress(rawData, 9); - - // 重置接收缓存 - m_receivedData.clear(); - m_totalFileSize = compressedData.size(); - - // 分块发送(每64KB一个块) - const int chunkSize = 64 * 1024; - int totalChunks = (compressedData.size() + chunkSize - 1) / chunkSize; - - for (int i = 0; i < totalChunks; ++i) { - QByteArray chunk = compressedData.mid(i * chunkSize, chunkSize); - m_socket.sendBinaryMessage(chunk); - - // 计算上传进度 - int progress = (i + 1) * 100 / totalChunks; - emit uploadProgressChanged(progress); - } - - // 发送结束标记 - m_socket.sendBinaryMessage("END"); + emit log(QString("Connecting to server: %1").arg(m_url.toString())); + emit internalConnect(); } -void WebSocketManager::onBinaryMessageReceived(const QByteArray &message) +void WebSocketClient::disconnectFromServer() { - if (message == "END") { - // 解压接收数据 - QByteArray decompressedData = qUncompress(m_receivedData); + emit log("Disconnecting from server..."); + emit internalDisconnect(); +} - // 生成保存路径 - QString savePath = QDir::tempPath() + "/processed_" + - QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".wav"; +void WebSocketClient::reconnect() +{ + if (!hasConfiguration()) { + emit error("WebSocket not configured. Cannot reconnect."); + return; + } - // 保存文件 - QFile file(savePath); - if (file.open(QIODevice::WriteOnly)) { - file.write(decompressedData); - emit fileProcessed(savePath); - } else { - emit errorOccurred(tr("无法保存文件: %1").arg(savePath)); - } + // 先断开,再连接 + disconnectFromServer(); - m_receivedData.clear(); + // 短暂延迟后重新连接 + QTimer::singleShot(100, this, [this]() { + emit log("Attempting to reconnect..."); + connectToServer(); + }); +} + +void WebSocketClient::sendText(const QString& message) +{ + if (isConnected()) { + emit internalSendText(message); } else { - m_receivedData.append(message); - - // 计算下载进度 - if (m_totalFileSize > 0) { - int progress = m_receivedData.size() * 100 / m_totalFileSize; - emit downloadProgressChanged(progress); - } + emit error("Cannot send message: WebSocket is not connected"); } } -void WebSocketManager::connectToServer() +void WebSocketClient::sendJson(const QString& type, const QJsonObject& data) { - // 检查是否已经连接到服务器 - if (m_socket.state() == QAbstractSocket::ConnectedState) { - emit errorOccurred(tr("已经连接到服务器")); - return; + if (isConnected()) { + emit internalSendJson(type, data); + } else { + emit error("Cannot send JSON: WebSocket is not connected"); } - // 连接服务器 - m_socket.open(QUrl(this->url)); } - -void WebSocketManager::setUrl(const QString &url) +void WebSocketClient::sendBinary(const QByteArray& data) { - this->url = url; + if (isConnected()) { + emit internalSendBinary(data); + } else { + emit error("Cannot send binary data: WebSocket is not connected"); + } } -QString WebSocketManager::getUrl() +bool WebSocketClient::isConnected() const { - return this->url; + if (m_webSocketManager) { + bool connected = false; + // 使用阻塞调用获取连接状态 + QMetaObject::invokeMethod(const_cast(m_webSocketManager), + [&connected, manager = m_webSocketManager]() { + connected = manager->isConnected(); + }, + Qt::BlockingQueuedConnection); + return connected; + } + return false; } - -void WebSocketManager::onConnected() +void WebSocketClient::setAutoReconnect(bool enabled) { - qDebug() << "Connected to server"; + emit log(QString("Auto reconnect %1").arg(enabled ? "enabled" : "disabled")); + emit internalSetAutoReconnect(enabled); } -void WebSocketManager::onError(QAbstractSocket::SocketError error) +void WebSocketClient::setPingInterval(int milliseconds) { - emit errorOccurred(tr("网络错误: %1").arg(m_socket.errorString())); + // 注意:需要在 WebSocketManager 中添加相应的方法才能支持 + // 这里暂时记录日志,提醒需要实现 + emit log(QString("setPingInterval(%1) called, but not implemented in WebSocketManager") + .arg(milliseconds)); + Q_UNUSED(milliseconds) } \ No newline at end of file diff --git a/src/NetWorkHandle/readme.md b/src/NetWorkHandle/readme.md new file mode 100644 index 0000000..019b479 --- /dev/null +++ b/src/NetWorkHandle/readme.md @@ -0,0 +1,236 @@ +当前项目中实现了tcp, socket, websocket三种通信方式 +项目只用到了了**websocket**方式,其他两种是历史遗留(Yosuga[Qt5]所使用) +并且websocket经过了重构,交互数据也为自定义格式 + +这边顺便提供下WebSocket类的Mermaid图,帮助理解(将代码丢给AI生成出来的图,审阅了一下还是十分准确的) + +## 1. 类图 + +```mermaid +classDiagram + class WebSocketManager { + -QWebSocket* m_socket + -QUrl m_url + -QTimer* m_pingTimer + -QTimer* m_reconnectTimer + -int m_reconnectAttempts + -bool m_isReconnectEnabled + -QNetworkRequest m_request + -bool m_isRequest + +WebSocketManager(parent) + ~WebSocketManager() + +setRequestContent(requestToken) bool + +setSocketUrl(url) bool + +connectToServer() bool + +disconnectFromServer() + +sendText(message) + +sendJson(type, data) + +sendBinary(data) + +setReconnectEnabled(enabled) + +setRequestEnabled(enabled) + +isConnected() bool + -onConnected() + -onDisconnected() + -onTextMessageReceived(message) + -onError(socketError) + -onSslErrors(errors) + -onPong(elapsedTime, payload) + -sendPing() + -tryReconnect() + -- signals -- + +connected() + +disconnected() + +textReceived(message) + +jsonReceived(type, data) + +binaryReceived(data) + +error(errorMsg) + +log(msg) + +reconnecting(attempt) + } + + class WebSocketClient { + -static QMutex m_mutex + -static QScopedPointer~WebSocketClient~ m_instance + -QThread* m_workerThread + -WebSocketManager* m_webSocketManager + -QUrl m_url + -QString m_authToken + -bool m_hasAuthToken + +getInstance() WebSocketClient* + +destroy() + +setConfiguration(url, authToken) bool + +connectToServer() + +disconnectFromServer() + +reconnect() + +sendText(message) + +sendJson(type, data) + +sendBinary(data) + +isConnected() bool + +hasConfiguration() bool + +setAutoReconnect(enabled) + +setPingInterval(milliseconds) + +currentUrl() QUrl + +currentToken() QString + +manager() WebSocketManager* + -- signals -- + +connected() + +disconnected() + +textReceived(message) + +jsonReceived(type, data) + +binaryReceived(data) + +error(errorMsg) + +log(msg) + +reconnecting(attempt) + +configurationChanged(oldUrl, newUrl) + -internalSetUrl(url) + -internalSetAuthToken(token) + -internalConnect() + -internalDisconnect() + -internalSendText(message) + -internalSendJson(type, data) + -internalSendBinary(data) + -internalSetAutoReconnect(enabled) + -internalSetRequestEnabled(enabled) + } + + class QWebSocket { + +open(url) + +close() + +sendTextMessage(message) + +sendBinaryMessage(data) + +ping() + +state() QAbstractSocket::SocketState + } + + class QThread { + +start() + +quit() + +wait() + +terminate() + +isRunning() bool + } + + WebSocketClient "1" --> "1" WebSocketManager : 管理 + WebSocketClient "1" --> "1" QThread : 工作线程 + WebSocketManager "1" --> "1" QWebSocket : 封装 + WebSocketManager "1" --> "2" QTimer : 心跳/重连 +``` + +## 2. 连接时序图 + +```mermaid +sequenceDiagram + participant MainThread as 主线程 + participant Client as WebSocketClient + participant WorkerThread as 工作线程 + participant Manager as WebSocketManager + participant Socket as QWebSocket + participant Server as WebSocket服务器 + + MainThread->>Client: getInstance() + Client-->>MainThread: WebSocketClient实例 + + MainThread->>Client: setConfiguration(url, token) + Client->>WorkerThread: 创建工作线程 + Client->>Manager: 创建WebSocketManager + Manager->>Socket: 创建QWebSocket + Client->>Manager: 信号连接配置 + Client->>Manager: 设置URL和Token + + MainThread->>Client: connectToServer() + Client->>Manager: emit internalConnect() + Manager->>Socket: open(url/request) + Socket->>Server: WebSocket握手 + Server-->>Socket: 连接成功 + Socket->>Manager: connected() + Manager->>Manager: 启动心跳定时器 + Manager->>Client: emit connected() + Client->>MainThread: emit connected() +``` + +## 3. 状态图 + +```mermaid +stateDiagram-v2 + [*] --> 未配置 + + 未配置 --> 已配置 : setConfiguration() + 已配置 --> 连接中 : connectToServer() + + 连接中 --> 已连接 : 连接成功 + 连接中 --> 重连中 : 连接失败 + 连接中 --> [*] : disconnectFromServer() + + 已连接 --> 已连接 : 发送/接收数据 + 已连接 --> 已断开 : 连接断开 + 已连接 --> [*] : disconnectFromServer() + + 已断开 --> 重连中 : 自动重连开启 + 已断开 --> [*] : 自动重连关闭 + + 重连中 --> 已连接 : 重连成功 + 重连中 --> 重连中 : 重连失败(继续重试) + 重连中 --> [*] : disconnectFromServer() + + state 重连中 { + [*] --> 等待重试 + 等待重试 --> 尝试连接 : 定时器触发 + 尝试连接 --> 等待重试 : 连接失败 + 尝试连接 --> [*] : 连接成功 + } +``` + +## 4. 消息发送时序图 + +```mermaid +sequenceDiagram + participant App as 应用程序 + participant Client as WebSocketClient + participant Manager as WebSocketManager + participant Socket as QWebSocket + participant Server as WebSocket服务器 + + App->>Client: sendJson("message", data) + alt 已连接 + Client->>Manager: emit internalSendJson("message", data) + Manager->>Socket: sendTextMessage(json) + Socket->>Server: 发送JSON数据 + Server-->>Socket: 响应(可选) + Socket->>Manager: textMessageReceived() + Manager->>Manager: 解析JSON + Manager->>Client: emit jsonReceived() + Client->>App: emit jsonReceived() + else 未连接 + Client->>App: emit error("未连接") + end +``` + +## 5. 线程关系图 + +```mermaid +flowchart TD + subgraph 主线程 [UI/主线程] + direction LR + App[应用程序] + Client[WebSocketClient
单例] + end + + subgraph 工作线程 [WebSocket工作线程] + direction LR + Manager[WebSocketManager] + Socket[QWebSocket] + end + + subgraph 外部系统 + Server[WebSocket服务器] + end + + App -- 调用 --> Client + Client -- 跨线程信号 --> Manager + Manager -- Qt信号槽 --> Socket + Socket -- TCP/WebSocket --> Server + + Client -- 返回事件信号 --> App + Socket -- 接收数据 --> Manager + Manager -- 跨线程信号 --> Client +``` \ No newline at end of file diff --git a/src/Setting/Inc/NetworkPage.h b/src/Setting/Inc/NetworkPage.h index 122bdb6..3d0f27c 100644 --- a/src/Setting/Inc/NetworkPage.h +++ b/src/Setting/Inc/NetworkPage.h @@ -1,9 +1,7 @@ // // Created by Administrator on 2025/3/2. // - -#ifndef AIRI_DESKTOPGRIL_NETWORKPAGE_H -#define AIRI_DESKTOPGRIL_NETWORKPAGE_H +#pragma once #include "BasePage.h" #include "ElaPushButton.h" @@ -11,23 +9,23 @@ class ElaPushButton; class ElaLineEdit; -class NetWorkPage : public BasePage +class NetWorkPage final : public BasePage { Q_OBJECT public: Q_INVOKABLE explicit NetWorkPage(QWidget* parent = nullptr); - ~NetWorkPage(); + ~NetWorkPage() override; + +private: + void initUI(); + void initWebSocketClient(); private: - // IP控件 - ElaPushButton* ipPushButton = nullptr; - ElaLineEdit* ipLineEdit = nullptr; - - // 端口控件 - ElaPushButton* portPushButton = nullptr; - ElaLineEdit* portLineEdit = nullptr; + // websocket 控件 + ElaPushButton* websocketPushButton = nullptr; + ElaLineEdit* websocketLineEdit = nullptr; // 连接测试 ElaPushButton* connectTestPushButton = nullptr; @@ -36,7 +34,6 @@ private: // 断开 ElaPushButton* disconnectPushButton = nullptr; + // 发送测试按钮 + ElaPushButton* sendTestPushButton = nullptr; }; - - -#endif //AIRI_DESKTOPGRIL_NETWORKPAGE_H diff --git a/src/Setting/Src/NetworkPage.cpp b/src/Setting/Src/NetworkPage.cpp index 9f63c41..297a410 100644 --- a/src/Setting/Src/NetworkPage.cpp +++ b/src/Setting/Src/NetworkPage.cpp @@ -3,155 +3,174 @@ // #include "NetworkPage.h" - #include - -#include "ElaComboBox.h" -#include "ElaPlainTextEdit.h" #include "ElaScrollPageArea.h" -#include "ElaSpinBox.h" #include "ElaText.h" - -#include "socketmanager.h" -#include #include "ElaMessageBar.h" +#include "websocketmanager.h" +#include "NetWorkDO.h" + NetWorkPage::NetWorkPage(QWidget* parent) : BasePage(parent) { // 预览窗口标题 setWindowTitle("NetworkPage"); - // ip - ipPushButton = new ElaPushButton("设定",this); - ipLineEdit = new ElaLineEdit(this); - ElaScrollPageArea* ipToggleSwitchArea = new ElaScrollPageArea(this); - QHBoxLayout* ipToggleSwitchLayout = new QHBoxLayout(ipToggleSwitchArea); - ElaText* ipToggleSwitchText = new ElaText("服务端IP:", this); - ipToggleSwitchText->setTextPixelSize(15); - ipToggleSwitchLayout->addWidget(ipToggleSwitchText); - ipToggleSwitchLayout->addWidget(ipLineEdit); - ipToggleSwitchLayout->addStretch(); - connect(ipPushButton, &ElaPushButton::clicked, this, [=, this]() { - // 我爱lambda函数 - QString ip_temp = ipLineEdit->text(); - auto f_temp = [=](const QString &ip) -> bool { - QHostAddress addr; - if (addr.setAddress(ip) && addr.protocol() == QAbstractSocket::IPv4Protocol) { - return true; - } - return false; - }; - if(!f_temp(ip_temp)){ - ElaMessageBar::error(ElaMessageBarType::TopLeft, "连接设置", "服务端IP格式错误", 800.0, this); - } - else{ - SocketManager::getInstance()->setIp(ip_temp); - ElaMessageBar::success(ElaMessageBarType::TopRight, "连接设置", "服务端IP设置成功", 800.0, this); - } - }); - ipToggleSwitchLayout->addWidget(ipPushButton); - ipToggleSwitchLayout->addSpacing(10); - - // 端口 - portPushButton = new ElaPushButton("设定",this); - portLineEdit = new ElaLineEdit(this); - ElaScrollPageArea* portToggleSwitchArea = new ElaScrollPageArea(this); - QHBoxLayout* portToggleSwitchLayout = new QHBoxLayout(portToggleSwitchArea); - ElaText* portToggleSwitchText = new ElaText("服务端端口:", this); - portToggleSwitchText->setTextPixelSize(15); - portToggleSwitchLayout->addWidget(portToggleSwitchText); - portToggleSwitchLayout->addWidget(portLineEdit); - portToggleSwitchLayout->addStretch(); - connect(portPushButton, &ElaPushButton::clicked, this, [=, this]() { - QString port_temp = portLineEdit->text(); - auto f_temp = [=](const QString &port) -> bool { - return port.toInt() > 0 && port.toInt() < 65535; - }; - if(!f_temp(port_temp)){ - ElaMessageBar::error(ElaMessageBarType::TopLeft, "连接设置", "服务端端口格式错误", 800.0, this); - } - else{ - SocketManager::getInstance()->setPort(port_temp.toInt()); - ElaMessageBar::success(ElaMessageBarType::TopRight, "连接设置", "服务端端口设置成功", 800.0, this); - } - }); - portToggleSwitchLayout->addWidget(portPushButton); - portToggleSwitchLayout->addSpacing(10); - - connectTestPushButton = new ElaPushButton("连通测试",this); - connectTestPushButton->setToolTip("测试与服务器连通性(如果成功连通会自动连上服务器)"); - connectPushButton = new ElaPushButton("连接",this); - disconnectPushButton = new ElaPushButton("断开",this); - ElaScrollPageArea* connectTestArea = new ElaScrollPageArea(this); - QHBoxLayout* connectTestLayout = new QHBoxLayout(connectTestArea); - connectTestLayout->addWidget(connectTestPushButton); - connectTestLayout->addStretch(); - connect(connectTestPushButton, &ElaPushButton::clicked, this, [=, this]() { - if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){ - // 如果已连接 - ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "已连通", 800.0, this); - } - else{ - SocketManager::getInstance()->connectToServer(); - // TODO:等待很短的时间,等tcp握手完成 - - if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){ - ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "已连通", 800.0, this); - } - else{ - ElaMessageBar::error(ElaMessageBarType::TopLeft, "连通测试", "未连通,请检查服务器是否开启或IP和端口信息是否正确", 1500.0, this); - } - } - - }); - connect(connectPushButton, &ElaPushButton::clicked, this, [=, this]() { - if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){ - // 如果已连接 - ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "已连接", 800.0, this); - } - if(SocketManager::getInstance()->state() == QAbstractSocket::UnconnectedState){ - // 如果未连接 - SocketManager::getInstance()->connectToServer(); - if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){ - ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "连接成功", 800.0, this); - } - else{ - ElaMessageBar::error(ElaMessageBarType::TopLeft, "连通测试", "连接失败,请检查服务器是否开启或IP和端口信息是否正确", 1500.0, this); - } - } - }); - connect(disconnectPushButton, &ElaPushButton::clicked, this, [=, this]() { - if(SocketManager::getInstance()->state() == QAbstractSocket::ConnectedState){ - // 如果已连接,则断开 - SocketManager::getInstance()->disconnectFromServer(); - ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", "断开成功", 800.0, this); - } - else{ - // 如果未连接,则提示 - ElaMessageBar::information(ElaMessageBarType::BottomRight, "连通测试", "似乎并没有连接到服务器", 800.0, this); - } - }); - connectTestLayout->addWidget(disconnectPushButton); - connectTestLayout->addWidget(connectPushButton); - connectTestLayout->addSpacing(10); - - - - QWidget* centralWidget = new QWidget(this); - centralWidget->setWindowTitle("连接设置"); - QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget); - centerLayout->addWidget(ipToggleSwitchArea); - centerLayout->addWidget(portToggleSwitchArea); - centerLayout->addWidget(connectTestArea); - - centerLayout->addStretch(); - centerLayout->setContentsMargins(0, 0, 0, 0); - addCentralWidget(centralWidget, true, true, 0); - + this->initUI(); // 初始化UI + this->initWebSocketClient(); // 初始化websocket客户端(主要是相关的信号与槽) } NetWorkPage::~NetWorkPage() { + +} + +void NetWorkPage::initUI() { + // websocket UI + websocketPushButton = new ElaPushButton("设定",this); + websocketPushButton->setToolTip("设定服务端WebSocket地址"); + websocketLineEdit = new ElaLineEdit(this); + websocketLineEdit->setPlaceholderText("请输入服务端WebSocket地址"); + websocketLineEdit->setFixedWidth(300); // 设置websocketLineEdit框的宽度 + + ElaScrollPageArea* websocketToggleSwitchArea = new ElaScrollPageArea(this); + QHBoxLayout* websocketToggleSwitchLayout = new QHBoxLayout(websocketToggleSwitchArea); + ElaText* websocketToggleSwitchText = new ElaText("服务端WebSocket地址:", this); + websocketToggleSwitchText->setTextPixelSize(15); + websocketToggleSwitchLayout->addWidget(websocketToggleSwitchText); + websocketToggleSwitchLayout->addWidget(websocketLineEdit); + websocketToggleSwitchLayout->addStretch(); + websocketToggleSwitchLayout->addWidget(websocketPushButton); + websocketToggleSwitchLayout->addSpacing(10); + // 连通测试按钮 + connectTestPushButton = new ElaPushButton("连通测试",this); + connectTestPushButton->setToolTip("测试与服务器连通性(如果成功连通会自动连上服务器)"); + connectPushButton = new ElaPushButton("连接",this); + disconnectPushButton = new ElaPushButton("断开",this); + sendTestPushButton = new ElaPushButton("发送测试",this); + ElaScrollPageArea* connectTestArea = new ElaScrollPageArea(this); // 创建一个滚动页面 + QHBoxLayout* connectTestLayout = new QHBoxLayout(connectTestArea); + connectTestLayout->addWidget(connectTestPushButton); // 将连通测试按钮添加到布局中 + connectTestLayout->addStretch(); // 添加一个空格 + connectTestLayout->addWidget(sendTestPushButton); // 将发送测试按钮添加到布局中 + connectTestLayout->addWidget(disconnectPushButton); // 将断开按钮添加到布局中 + connectTestLayout->addWidget(connectPushButton); // 将连接按钮添加到布局中 + connectTestLayout->addSpacing(10); + // 添加到布局 + QWidget* centralWidget = new QWidget(this); // 中心部件 + centralWidget->setWindowTitle("连接设置"); // 中心部件标题 + QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget); // 中心部件布局 + centerLayout->addWidget(websocketToggleSwitchArea); // 将websocketToggleSwitchArea添加到布局中 + centerLayout->addWidget(connectTestArea); // 将connectTestArea添加到布局中 + + centerLayout->addStretch(); + centerLayout->setContentsMargins(0, 0, 0, 0); // 设置布局的边距 + addCentralWidget(centralWidget, true, true, 0); // 添加中心部件 +} + +void NetWorkPage::initWebSocketClient() { + auto* client = WebSocketClient::getInstance(); // 获取单例实例(设置一个默认地址) + auto* netDO = NetworkDO::getInstance(); + // 注入:将底层发送能力赋予 NetworkDO + netDO->registerSender([client](const QString& type, const QJsonObject& data){ + client->sendJson(type, data); + }); + // 监听:底层收到数据 -> NetworkDO 解析 + connect(client, &WebSocketClient::jsonReceived, + netDO, &NetworkDO::onDataReceived); + + // 连接成功的处理 + connect(client, &WebSocketClient::connected, this, [this]() { + ElaMessageBar::success(ElaMessageBarType::TopRight, "WebSocket", "连接成功", 800.0, this); + }); + // 连接失败的处理 + connect(client, &WebSocketClient::error, this, [this](const QString& errorMsg) { + ElaMessageBar::error(ElaMessageBarType::TopLeft, "WebSocket错误", errorMsg, 1500.0, this); + }); + // 断开连接的处理 + connect(client, &WebSocketClient::disconnected, this, [this]() { + ElaMessageBar::information(ElaMessageBarType::BottomRight, "WebSocket", "连接已断开", 800.0, this); + }); + // 接收数据处理 + connect(client, &WebSocketClient::jsonReceived, this, [](const QString &type, const QJsonObject &data) { + qDebug() << "Received JSON data: " << type << " " << data; + }); + + connect(websocketPushButton, &ElaPushButton::clicked, this, [this, client]() { // 设置服务端websocket地址 + const QUrl url(websocketLineEdit->text().trimmed()); // 从LineEdit中获取服务端websocket地址 + // 初始化客户端 + if (client->setConfiguration(url)) { + ElaMessageBar::success(ElaMessageBarType::TopRight, "连接设置", + QString("服务器地址已设置为: %1").arg(url.toString()), 800.0, this); + return; + } + ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连接设置", + QString("服务器地址存在问题"), 800.0, this); + }); + + connect(connectTestPushButton, &ElaPushButton::clicked, this, [this, client]() { + if (client->isConnected()) { + ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", + "当前已连通", 800.0, this); + return; + } + client->connectToServer(); // 连接 + // 使用定时器延迟检查连接状态 + QTimer::singleShot(1000, this, [this, client]() { + if (client->isConnected()) { + ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试", + "连通测试成功", 800.0, this); + } else { + ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连通测试", + "无法连接到服务器,请检查地址和服务器状态", 1500.0, this); + } + }); + }); + connect(connectPushButton, &ElaPushButton::clicked, this, [this, client]() { + if (client->isConnected()) { + ElaMessageBar::information(ElaMessageBarType::TopRight, "连接状态", + "已连接,无需重复连接", 800.0, this); + return; + } + const QString urlStr = websocketLineEdit->text().trimmed(); + if (urlStr.isEmpty()) { + ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连接", + "请先设置服务器地址", 800.0, this); + return; + } + // 确保使用正确的地址 + client->setConfiguration(QUrl(urlStr)); + client->connectToServer(); + // 连接结果会在 connected/error 信号中处理 + ElaMessageBar::information(ElaMessageBarType::TopRight, "连接", + "正在连接服务器...", 800.0, this); + }); + connect(disconnectPushButton, &ElaPushButton::clicked, this, [this, client]() { + if (client->isConnected()) { + client->disconnectFromServer(); + ElaMessageBar::success(ElaMessageBarType::TopRight, "断开连接", + "已断开连接", 800.0, this); + } else { + ElaMessageBar::information(ElaMessageBarType::BottomRight, "断开连接", + "当前未连接", 800.0, this); + } + }); + connect(sendTestPushButton, &ElaPushButton::clicked, this, [this, netDO]() { + // 创建数据包 + AudioDataPacket packet; + packet.text = "Hello, World!"; + // 填入元数据 + packet.sampleRate = 16000; + packet.channels = 1; + packet.duration = 2500; // 2.5秒 + // 填入音频数据 (模拟从文件或录音设备读取的二进制数据) + QByteArray rawAudioData; + rawAudioData.append("...这里是真实的PCM或WAV二进制数据..."); + packet.audioData = rawAudioData; + netDO->sendAudioPacket(packet); + ElaMessageBar::success(ElaMessageBarType::TopRight, "发送测试", + "已成功发送数据包", 1000.0, this); + }); }