1. 重构了WebSocket类的实现,并且设置其为项目的核心通信协议
This commit is contained in:
@@ -94,6 +94,8 @@ bool LAppModel::IsPointOnModel(const csmFloat32 x, const csmFloat32 y)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 如果有命中区域,使用HitTest(是否存在命中区域,这取决于模型是否定义这两部分信息)
|
// 如果有命中区域,使用HitTest(是否存在命中区域,这取决于模型是否定义这两部分信息)
|
||||||
|
// 不过不少模型不会写或者是不写入腿部信息,因此实际会出现点击腿部的时候判定为在模型外面,这并不是bug
|
||||||
|
// 也就是尽量依赖HitAreas,以此获得最好的性能,但是如果模型没有定义,则使用IsPointOnDrawable
|
||||||
/* 通常是这样的信息,在.model3.json当中
|
/* 通常是这样的信息,在.model3.json当中
|
||||||
"HitAreas": [
|
"HitAreas": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ file(GLOB_RECURSE YosugaSrc
|
|||||||
"src/AudioHandle/Inc/*.h"
|
"src/AudioHandle/Inc/*.h"
|
||||||
"src/Menu/Src/*.cpp"
|
"src/Menu/Src/*.cpp"
|
||||||
"src/Menu/Inc/*.h"
|
"src/Menu/Inc/*.h"
|
||||||
|
"src/DAO/Inc/*.h"
|
||||||
|
"src/DAO/Src/*.cpp"
|
||||||
"src/NetWorkHandle/Src/*.cpp"
|
"src/NetWorkHandle/Src/*.cpp"
|
||||||
"src/NetWorkHandle/Inc/*.h"
|
"src/NetWorkHandle/Inc/*.h"
|
||||||
"src/Setting/Src/*.cpp"
|
"src/Setting/Src/*.cpp"
|
||||||
@@ -193,6 +195,7 @@ target_include_directories(${PROJECT_NAME}
|
|||||||
src/Setting/Inc
|
src/Setting/Inc
|
||||||
src/Render/TextRender/Inc
|
src/Render/TextRender/Inc
|
||||||
src/Core/Inc
|
src/Core/Inc
|
||||||
|
src/DAO/Inc
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+33
-6
@@ -25,7 +25,7 @@ QMap<QString, double> GLCore::frameRateMap = {
|
|||||||
{"240", 240.0}
|
{"240", 240.0}
|
||||||
};
|
};
|
||||||
|
|
||||||
GLCore::GLCore(int w, int h, QWidget *parent)
|
GLCore::GLCore(const int width, const int height, QWidget *parent)
|
||||||
: QOpenGLWidget(parent),
|
: QOpenGLWidget(parent),
|
||||||
isLeftPressed(false), // 显式初始化
|
isLeftPressed(false), // 显式初始化
|
||||||
isRightPressed(false) // 显式初始化
|
isRightPressed(false) // 显式初始化
|
||||||
@@ -52,9 +52,9 @@ GLCore::GLCore(int w, int h, QWidget *parent)
|
|||||||
contextMenu = new Menu(this);
|
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()->setGlobalFont(QFont("Microsoft YaHei", 14, QFont::Bold));
|
||||||
TextRenderer::getInstance()->setHoldDuration(1.0f); // 停留1.2秒
|
TextRenderer::getInstance()->setHoldDuration(1.0f); // 停留1.2秒
|
||||||
TextRenderer::getInstance()->setGravity(600.0f); // 更快的下坠速度
|
TextRenderer::getInstance()->setGravity(600.0f); // 更快的下坠速度
|
||||||
@@ -164,8 +164,7 @@ void GLCore::closeEvent(QCloseEvent* event)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef Q_OS_WIN
|
#ifdef Q_OS_WIN
|
||||||
void GLCore::setWindowTransparentForMouse(bool transparent)
|
void GLCore::setWindowTransparentForMouse(const bool transparent) const {
|
||||||
{
|
|
||||||
if (!hwnd) return;
|
if (!hwnd) return;
|
||||||
|
|
||||||
LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
|
LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
|
||||||
@@ -182,7 +181,7 @@ void GLCore::setWindowTransparentForMouse(bool transparent)
|
|||||||
|
|
||||||
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle);
|
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);
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -250,9 +249,37 @@ void GLCore::mousePressEvent(QMouseEvent* event)
|
|||||||
// TODO: 右键菜单等
|
// TODO: 右键菜单等
|
||||||
if (event->button() == Qt::RightButton) {
|
if (event->button() == Qt::RightButton) {
|
||||||
// 在鼠标右键点击的位置创建菜单,显示自定义右键菜单
|
// 在鼠标右键点击的位置创建菜单,显示自定义右键菜单
|
||||||
|
if (onModel) {
|
||||||
contextMenu->showMenu(event->globalPos());
|
contextMenu->showMenu(event->globalPos());
|
||||||
this->isRightPressed = true;
|
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)
|
void GLCore::mouseReleaseEvent(QMouseEvent* event)
|
||||||
|
|||||||
@@ -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; // 注入的发送器
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,59 +1,150 @@
|
|||||||
//
|
//
|
||||||
// Created by Administrator on 2025/2/4.
|
// 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实现
|
||||||
*/
|
*/
|
||||||
|
class WebSocketManager final : public QObject
|
||||||
#ifndef AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H
|
|
||||||
#define AIRI_DESKTOPGRIL_WEBSOCKETMANAGER_H
|
|
||||||
|
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QtWebSockets/QWebSocket>
|
|
||||||
|
|
||||||
class WebSocketManager : public QObject
|
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit WebSocketManager(QObject *parent = nullptr);
|
// 构造函数
|
||||||
|
explicit WebSocketManager(QObject *parent = nullptr); // 不携带URL参数
|
||||||
~WebSocketManager();
|
~WebSocketManager() override;
|
||||||
|
// 删除拷贝构造函数和赋值操作符
|
||||||
// 上传接口(传入本地文件路径)
|
WebSocketManager(const WebSocketManager&) = delete;
|
||||||
Q_INVOKABLE void uploadFile(const QString &filePath);
|
WebSocketManager& operator=(const WebSocketManager&) = delete;
|
||||||
|
public:
|
||||||
// 连接服务器
|
bool setRequestContent(const QString& requestToken); // 设置自定义websocket首次请求Token(鉴权用) 服务端应保持Token一致
|
||||||
void connectToServer();
|
bool setSocketUrl(QUrl url); // 设置URL
|
||||||
|
|
||||||
// 断开服务器
|
|
||||||
void disconnectFromServer();
|
|
||||||
|
|
||||||
// 设置服务器地址get&set方法
|
|
||||||
void setUrl(const QString &url);
|
|
||||||
QString getUrl();
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
// 上传进度(0-100)
|
// 发给主线程的信号
|
||||||
void uploadProgressChanged(int percent);
|
void connected(); // 连接
|
||||||
// 下载进度(0-100)
|
void disconnected(); // 断开
|
||||||
void downloadProgressChanged(int percent);
|
void textReceived(const QString &message); // 接收文本
|
||||||
// 文件处理完成(返回保存路径)
|
void jsonReceived(const QString &type, const QJsonObject &data); // 接收JSON
|
||||||
void fileProcessed(const QString &filePath);
|
void binaryReceived(const QByteArray &data); // 接收二进制数据
|
||||||
// 错误通知
|
void error(const QString &errorMsg); // 错误
|
||||||
void errorOccurred(const QString &message);
|
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:
|
private slots:
|
||||||
void onConnected();
|
void onConnected(); // 连接成功
|
||||||
void onBinaryMessageReceived(const QByteArray &message);
|
void onDisconnected(); // 断开连接
|
||||||
void onError(QAbstractSocket::SocketError error);
|
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:
|
private:
|
||||||
QWebSocket m_socket;
|
QWebSocket *m_socket; /// WebSocket对象
|
||||||
QByteArray m_receivedData;
|
QUrl m_url; /// 服务器地址
|
||||||
qint64 m_totalFileSize = 0;
|
QTimer *m_pingTimer; /// Ping定时器
|
||||||
QString url = "ws://localhost:8765";
|
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;
|
||||||
|
};
|
||||||
@@ -2,124 +2,473 @@
|
|||||||
// Created by Administrator on 2025/2/4.
|
// Created by Administrator on 2025/2/4.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
#include "websocketmanager.h"
|
#include "websocketmanager.h"
|
||||||
#include <QFile>
|
|
||||||
#include <QDir>
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <zlib.h>
|
#include <QJsonObject>
|
||||||
|
#include <utility>
|
||||||
|
#include <QMutexLocker>
|
||||||
|
|
||||||
|
/// WebSocketManager
|
||||||
WebSocketManager::WebSocketManager(QObject *parent)
|
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) // 默认不启用自定义首次请求
|
||||||
{
|
{
|
||||||
// 连接信号槽
|
// 配置 WebSocket
|
||||||
connect(&m_socket, &QWebSocket::connected,
|
m_socket->setParent(this); // 确保 socket 也在工作线程
|
||||||
|
// 连接信号
|
||||||
|
connect(m_socket, &QWebSocket::connected,
|
||||||
this, &WebSocketManager::onConnected);
|
this, &WebSocketManager::onConnected);
|
||||||
connect(&m_socket, &QWebSocket::binaryMessageReceived,
|
connect(m_socket, &QWebSocket::disconnected,
|
||||||
this, &WebSocketManager::onBinaryMessageReceived);
|
this, &WebSocketManager::onDisconnected);
|
||||||
connect(&m_socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
|
connect(m_socket, &QWebSocket::textMessageReceived,
|
||||||
|
this, &WebSocketManager::onTextMessageReceived);
|
||||||
|
connect(m_socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::errorOccurred),
|
||||||
this, &WebSocketManager::onError);
|
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()
|
WebSocketManager::~WebSocketManager() {
|
||||||
{
|
if (m_socket) {
|
||||||
m_socket.close();
|
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);
|
QMutexLocker locker(&m_mutex);
|
||||||
if (!file.open(QIODevice::ReadOnly)) {
|
if (m_instance.isNull()) {
|
||||||
emit errorOccurred(tr("无法打开文件: %1").arg(filePath));
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取并压缩数据
|
emit log(QString("Connecting to server: %1").arg(m_url.toString()));
|
||||||
QByteArray rawData = file.readAll();
|
emit internalConnect();
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketManager::onBinaryMessageReceived(const QByteArray &message)
|
void WebSocketClient::disconnectFromServer()
|
||||||
{
|
{
|
||||||
if (message == "END") {
|
emit log("Disconnecting from server...");
|
||||||
// 解压接收数据
|
emit internalDisconnect();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketManager::connectToServer()
|
void WebSocketClient::reconnect()
|
||||||
{
|
{
|
||||||
// 检查是否已经连接到服务器
|
if (!hasConfiguration()) {
|
||||||
if (m_socket.state() == QAbstractSocket::ConnectedState) {
|
emit error("WebSocket not configured. Cannot reconnect.");
|
||||||
emit errorOccurred(tr("已经连接到服务器"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 连接服务器
|
|
||||||
m_socket.open(QUrl(this->url));
|
// 先断开,再连接
|
||||||
|
disconnectFromServer();
|
||||||
|
|
||||||
|
// 短暂延迟后重新连接
|
||||||
|
QTimer::singleShot(100, this, [this]() {
|
||||||
|
emit log("Attempting to reconnect...");
|
||||||
|
connectToServer();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WebSocketClient::sendText(const QString& message)
|
||||||
void WebSocketManager::setUrl(const QString &url)
|
|
||||||
{
|
{
|
||||||
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 WebSocketClient::sendBinary(const QByteArray& data)
|
||||||
void WebSocketManager::onConnected()
|
|
||||||
{
|
{
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
//
|
//
|
||||||
// Created by Administrator on 2025/3/2.
|
// Created by Administrator on 2025/3/2.
|
||||||
//
|
//
|
||||||
|
#pragma once
|
||||||
#ifndef AIRI_DESKTOPGRIL_NETWORKPAGE_H
|
|
||||||
#define AIRI_DESKTOPGRIL_NETWORKPAGE_H
|
|
||||||
|
|
||||||
#include "BasePage.h"
|
#include "BasePage.h"
|
||||||
#include "ElaPushButton.h"
|
#include "ElaPushButton.h"
|
||||||
@@ -11,23 +9,23 @@
|
|||||||
|
|
||||||
class ElaPushButton;
|
class ElaPushButton;
|
||||||
class ElaLineEdit;
|
class ElaLineEdit;
|
||||||
class NetWorkPage : public BasePage
|
class NetWorkPage final : public BasePage
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
Q_INVOKABLE explicit NetWorkPage(QWidget* parent = nullptr);
|
Q_INVOKABLE explicit NetWorkPage(QWidget* parent = nullptr);
|
||||||
~NetWorkPage();
|
~NetWorkPage() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void initUI();
|
||||||
|
void initWebSocketClient();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// IP控件
|
// websocket 控件
|
||||||
ElaPushButton* ipPushButton = nullptr;
|
ElaPushButton* websocketPushButton = nullptr;
|
||||||
ElaLineEdit* ipLineEdit = nullptr;
|
ElaLineEdit* websocketLineEdit = nullptr;
|
||||||
|
|
||||||
// 端口控件
|
|
||||||
ElaPushButton* portPushButton = nullptr;
|
|
||||||
ElaLineEdit* portLineEdit = nullptr;
|
|
||||||
|
|
||||||
// 连接测试
|
// 连接测试
|
||||||
ElaPushButton* connectTestPushButton = nullptr;
|
ElaPushButton* connectTestPushButton = nullptr;
|
||||||
@@ -36,7 +34,6 @@ private:
|
|||||||
// 断开
|
// 断开
|
||||||
ElaPushButton* disconnectPushButton = nullptr;
|
ElaPushButton* disconnectPushButton = nullptr;
|
||||||
|
|
||||||
|
// 发送测试按钮
|
||||||
|
ElaPushButton* sendTestPushButton = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#endif //AIRI_DESKTOPGRIL_NETWORKPAGE_H
|
|
||||||
|
|||||||
+154
-135
@@ -3,155 +3,174 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#include "NetworkPage.h"
|
#include "NetworkPage.h"
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
|
||||||
#include "ElaComboBox.h"
|
|
||||||
#include "ElaPlainTextEdit.h"
|
|
||||||
#include "ElaScrollPageArea.h"
|
#include "ElaScrollPageArea.h"
|
||||||
#include "ElaSpinBox.h"
|
|
||||||
#include "ElaText.h"
|
#include "ElaText.h"
|
||||||
|
|
||||||
#include "socketmanager.h"
|
|
||||||
#include <QHostAddress>
|
|
||||||
#include "ElaMessageBar.h"
|
#include "ElaMessageBar.h"
|
||||||
|
#include "websocketmanager.h"
|
||||||
|
#include "NetWorkDO.h"
|
||||||
|
|
||||||
NetWorkPage::NetWorkPage(QWidget* parent)
|
NetWorkPage::NetWorkPage(QWidget* parent)
|
||||||
: BasePage(parent)
|
: BasePage(parent)
|
||||||
{
|
{
|
||||||
// 预览窗口标题
|
// 预览窗口标题
|
||||||
setWindowTitle("NetworkPage");
|
setWindowTitle("NetworkPage");
|
||||||
|
|
||||||
// ip
|
this->initUI(); // 初始化UI
|
||||||
ipPushButton = new ElaPushButton("设定",this);
|
this->initWebSocketClient(); // 初始化websocket客户端(主要是相关的信号与槽)
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NetWorkPage::~NetWorkPage()
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user