1. 重构了WebSocket类的实现,并且设置其为项目的核心通信协议

This commit is contained in:
Misaki
2025-12-30 04:59:03 +08:00
parent 702b083e47
commit bb509e5409
10 changed files with 1225 additions and 280 deletions
+2
View File
@@ -94,6 +94,8 @@ bool LAppModel::IsPointOnModel(const csmFloat32 x, const csmFloat32 y)
return false;
}
// 如果有命中区域,使用HitTest(是否存在命中区域,这取决于模型是否定义这两部分信息)
// 不过不少模型不会写或者是不写入腿部信息,因此实际会出现点击腿部的时候判定为在模型外面,这并不是bug
// 也就是尽量依赖HitAreas,以此获得最好的性能,但是如果模型没有定义,则使用IsPointOnDrawable
/* 通常是这样的信息,在.model3.json当中
"HitAreas": [
{
+3
View File
@@ -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
)
+33 -6
View File
@@ -25,7 +25,7 @@ QMap<QString, double> 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,9 +249,37 @@ void GLCore::mousePressEvent(QMouseEvent* event)
// TODO: 右键菜单等
if (event->button() == Qt::RightButton) {
// 在鼠标右键点击的位置创建菜单,显示自定义右键菜单
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;
}
}
}
void GLCore::mouseReleaseEvent(QMouseEvent* event)
+103
View File
@@ -0,0 +1,103 @@
//
// Created by misaki on 2025/12/29.
//
#pragma once
/**
* 本类为网络数据流会使用到的数据访问对象封装
* 目的是便于统一数据访问方式
* 同时屏蔽了端到端数据交换格式,使得上层调用不再需要关心数据格式,而只需要填入数据即可
*/
/**
* 简单描述一下Yosuga客户端所需要使用到的数据
* 主要为音频数据,控制信息,文本信息。
* 其中文本信息与音频数据为捆绑收发,并且其中还包括了一些特别的信息,例如音频时长等
* 控制信息与各种业务逻辑相关,例如模拟点击,模拟输入等
*/
#include <QObject>
#include <QString>
#include <QByteArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <QScopedPointer>
#include <QMutex>
#include <functional>
/**
* 数据传输对象 (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<void(const QString& type, const QJsonObject& data)>;
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<NetworkDO> m_instance;
static QMutex m_mutex;
SenderFunc m_sender; // 注入的发送器
};
+118
View File
@@ -0,0 +1,118 @@
//
// Created by misaki on 2025/12/29.
//
#include "NetWorkDO.h"
#include <QDebug>
#include <QMutexLocker>
// 初始化静态成员
QScopedPointer<NetworkDO> 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<int>(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<qint64>(data.value("duration").toDouble());
// 解析音频 (Base64 -> Binary)
QString base64Audio = data.value("audio").toString();
if (!base64Audio.isEmpty()) {
packet.audioData = QByteArray::fromBase64(base64Audio.toLatin1());
}
// 通知上层业务
emit audioPacketReceived(packet);
}
+133 -42
View File
@@ -1,59 +1,150 @@
//
// Created by Administrator on 2025/2/4.
//
#pragma once
#include <QWebSocket>
#include <QObject>
#include <QThread>
#include <QJsonDocument>
#include <QTimer>
#include <QQueue>
#include <QAtomicPointer>
#include <QMutexLocker>
/**
* 已废弃
* 2025.12.25重构 Misaki
* 多线程websocket实现
*/
#ifndef AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H
#define AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H
#include <QObject>
#include <QtWebSockets/QWebSocket>
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<QSslError> &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<WebSocketClient> m_instance;
QThread* m_workerThread;
WebSocketManager* m_webSocketManager;
QUrl m_url;
QString m_authToken;
bool m_hasAuthToken;
};
+432 -83
View File
@@ -2,124 +2,473 @@
// Created by Administrator on 2025/2/4.
//
#include "websocketmanager.h"
#include <QFile>
#include <QDir>
#include <QDebug>
#include <zlib.h>
#include <QJsonObject>
#include <utility>
#include <QMutexLocker>
/// WebSocketManager
WebSocketManager::WebSocketManager(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<QAbstractSocket::SocketError>::of(&QWebSocket::error),
connect(m_socket, &QWebSocket::disconnected,
this, &WebSocketManager::onDisconnected);
connect(m_socket, &QWebSocket::textMessageReceived,
this, &WebSocketManager::onTextMessageReceived);
connect(m_socket, QOverload<QAbstractSocket::SocketError>::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<QSslError> &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 <QScopedPointer>
QMutex WebSocketClient::m_mutex;
QScopedPointer<WebSocketClient> 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);
// 生成保存路径
QString savePath = QDir::tempPath() + "/processed_" +
QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".wav";
// 保存文件
QFile file(savePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(decompressedData);
emit fileProcessed(savePath);
} else {
emit errorOccurred(tr("无法保存文件: %1").arg(savePath));
}
m_receivedData.clear();
} else {
m_receivedData.append(message);
// 计算下载进度
if (m_totalFileSize > 0) {
int progress = m_receivedData.size() * 100 / m_totalFileSize;
emit downloadProgressChanged(progress);
}
}
emit log("Disconnecting from server...");
emit internalDisconnect();
}
void WebSocketManager::connectToServer()
void WebSocketClient::reconnect()
{
// 检查是否已经连接到服务器
if (m_socket.state() == QAbstractSocket::ConnectedState) {
emit errorOccurred(tr("已经连接到服务器"));
if (!hasConfiguration()) {
emit error("WebSocket not configured. Cannot reconnect.");
return;
}
// 连接服务器
m_socket.open(QUrl(this->url));
// 先断开,再连接
disconnectFromServer();
// 短暂延迟后重新连接
QTimer::singleShot(100, this, [this]() {
emit log("Attempting to reconnect...");
connectToServer();
});
}
void WebSocketManager::setUrl(const QString &url)
void WebSocketClient::sendText(const QString& message)
{
this->url = url;
if (isConnected()) {
emit internalSendText(message);
} else {
emit error("Cannot send message: WebSocket is not connected");
}
}
QString WebSocketManager::getUrl()
void WebSocketClient::sendJson(const QString& type, const QJsonObject& data)
{
return this->url;
if (isConnected()) {
emit internalSendJson(type, data);
} else {
emit error("Cannot send JSON: WebSocket is not connected");
}
}
void WebSocketManager::onConnected()
void WebSocketClient::sendBinary(const QByteArray& data)
{
qDebug() << "Connected to server";
if (isConnected()) {
emit internalSendBinary(data);
} else {
emit error("Cannot send binary data: WebSocket is not connected");
}
}
void WebSocketManager::onError(QAbstractSocket::SocketError error)
bool WebSocketClient::isConnected() const
{
emit errorOccurred(tr("网络错误: %1").arg(m_socket.errorString()));
if (m_webSocketManager) {
bool connected = false;
// 使用阻塞调用获取连接状态
QMetaObject::invokeMethod(const_cast<WebSocketManager*>(m_webSocketManager),
[&connected, manager = m_webSocketManager]() {
connected = manager->isConnected();
},
Qt::BlockingQueuedConnection);
return connected;
}
return false;
}
void WebSocketClient::setAutoReconnect(bool enabled)
{
emit log(QString("Auto reconnect %1").arg(enabled ? "enabled" : "disabled"));
emit internalSetAutoReconnect(enabled);
}
void WebSocketClient::setPingInterval(int milliseconds)
{
// 注意:需要在 WebSocketManager 中添加相应的方法才能支持
// 这里暂时记录日志,提醒需要实现
emit log(QString("setPingInterval(%1) called, but not implemented in WebSocketManager")
.arg(milliseconds));
Q_UNUSED(milliseconds)
}
+236
View File
@@ -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<br/>单例]
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
```
+12 -15
View File
@@ -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
+154 -135
View File
@@ -3,155 +3,174 @@
//
#include "NetworkPage.h"
#include <QHBoxLayout>
#include "ElaComboBox.h"
#include "ElaPlainTextEdit.h"
#include "ElaScrollPageArea.h"
#include "ElaSpinBox.h"
#include "ElaText.h"
#include "socketmanager.h"
#include <QHostAddress>
#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);
});
}