当前位置: 首页 > news >正文

Chap25-SeparateUI-NetThread

Separate UI-Net Thread

线程分离

我们的线程独立之前:

image-20251210171859843

独立之后:

image-20251210171921155

之所以考虑到将主线程和网络线程分开,是因为接下来要做文件传输,如果和主线程在一起,那么UI可能会卡顿,比如传输音频,大文件之类的。

因此我们将TcpManager也就是网络线程独立到新的线程,不要阻塞我们的主线程:

class TcpThread:public std::enable_shared_from_this<TcpThread> {
public:TcpThread();~TcpThread();
private:QThread* _tcp_thread;
};TcpThread::TcpThread()
{_tcp_thread = new QThread();TcpMgr::GetInstance()->moveToThread(_tcp_thread);QObject::connect(_tcp_thread, &QThread::finished, _tcp_thread, &QObject::deleteLater);_tcp_thread->start();
}TcpThread::~TcpThread()
{_tcp_thread->quit();
}// main.cpp//启动tcp线程
TcpThread tcpthread;
MainWindow w;
w.show();
return a.exec();

分离的问题

在原来的单线程中,直接信号和槽进行连接没有问题,信号传递参数,槽函数执行对应的逻辑。但是在qt的信号和槽在多线程中跨线程传递参数的时候,对于常用内置类型,比如int,QString等,没有问题正常传递。否则,对于复杂的类型比如std::vector,std::shared_ptr等等,传递过程会有极大概率异常。例如std::vector他的内存管理机制和Qt的队列管理机制不兼容,盲目的传参会导致未定义。所以我们需要使用qt的元对象系统进行声明和注册。

比如TLV的T,是我们的自定义枚举类RequestType,但是Qt并不认知这个参数,我们在定义之后,加一个宏声明:

Q_DECLARE_METATYPE(RequestType)

还有对于struct结构的UserInfo:

Q_DECLARE_METATYPE(UserInfo)
Q_DECLARE_METATYPE(std::shared_ptr<UserInfo>)
Q_DECLARE_METATYPE(std::vector<std::shared_ptr<UserInfo>>)

除此之外,在使用槽函数之前,必须进行注册:

void TcpManager::RegisterMetaType()
{// 注册自定义类型qRegisterMetaType<RequestType>("RequestType");qRegisterMetaType<UserInfo>("UserInfo");qRegisterMetaType<FriendInfo>("FriendInfo");qRegisterMetaType<MessageItem>("MessageItem");qRegisterMetaType<ConversationItem>("ConversationItem");// 注册std::shared_ptr类型qRegisterMetaType<std::shared_ptr<UserInfo>>("std::shared_ptr<UserInfo>");qRegisterMetaType<std::shared_ptr<FriendInfo>>("std::shared_ptr<FriendInfo>");qRegisterMetaType<std::shared_ptr<MessageItem>>("std::shared_ptr<MessageItem>");qRegisterMetaType<std::shared_ptr<ConversationItem>>("std::shared_ptr<ConversationItem>");// 注册容器类型qRegisterMetaType<std::vector<std::shared_ptr<UserInfo>>>("std::vector<std::shared_ptr<UserInfo>>");qRegisterMetaType<std::vector<std::shared_ptr<MessageItem>>>("std::vector<std::shared_ptr<MessageItem>>");qRegisterMetaType<QList<std::shared_ptr<FriendInfo>>>("QList<std::shared_ptr<FriendInfo>>");
}

在 Qt 的信号/槽机制中,信号参数的传递方式取决于连接(connect)的类型,而连接类型又由发信号对象和接收槽对象所在的线程决定:

  1. 同线程(Direct Connection)
  • 如果信号和槽都在同一个线程里,默认使用 Direct Connection
  • Direct Connection 本质上就是一个普通的 C++ 函数调用,参数直接按值或按引用传递,编译时就已经知道了类型,不需要任何额外的元类型信息。
  • 因此,即使你没有把 SearchInfo 注册为 QMetaType,编译器也能直接生成函数调用代码,信号里就可以直接传递 SearchInfo
  1. 跨线程(Queued Connection)
  • 如果信号发送者和接收者不在同一个线程,Qt 会自动把连接转成 Queued Connection
  • Queued Connection 的实现是:当信号发出时,Qt 会把信号参数打包成一个事件(QEvent),然后把事件放到目标线程的事件队列里;目标线程的事件循环(QCoreApplication::processEvents())再把这个事件取出来,调用槽函数。
  • 这里的“打包”与“解包”就需要运行时才能确定参数类型,以及如何拷贝或序列化这个类型——这正是 Qt 元对象系统(QMetaType)要干的事情。
  • 如果没有把 SearchInfo 声明成一个元类型,Qt 就不知道如何在内部把它从一个线程“打包”到事件里,又如何在另一线程里还原。

因此,跨线程传递自定义类型,必须在类型定义后加上:

  • Q_DECLARE_METATYPE(SearchInfo)

并在运行时注册(通常在 main() 里调用一次):

  • qRegisterMetaType("SearchInfo");

小结

  • 同线程:Direct Connection,编译时直接调用,不需要Q_DECLARE_METATYPE
  • 跨线程:Queued Connection,需要运行时打包/解包参数,必须Q_DECLARE_METATYPE(以及 qRegisterMetaType)来注册你的自定义类型。

发送队列

在do_send_data函数中,我们直接进行了_socket.write(data),这样在我们数据量并不大的时候没有问题,很少会出现一次写不完的情况。但如果我们发送很大的大文件,比如几百MB,甚至几个G,虽然会有切片,但是每一次发送仍有可能因为发送缓冲区已经满了而发丝失败,这时候如果不处理的话,就会数据异常丢失。

解决思路是,可以模仿我们的服务器写法,添加一个发送队列,然后将要发送的数据投递到发送队列。

//发送队列
QQueue<QByteArray> _send_queue;
//正在发送的包
QByteArray  _current_block;
//当前已发送的字节数
qint64        _bytes_sent;
//是否正在发送
bool _pending;

修改发送逻辑

void TcpMgr::slot_send_data(ReqId reqId, QByteArray dataBytes)
{uint16_t id = reqId;// 计算长度(使用网络字节序转换)quint16 len = static_cast<quint16>(dataBytes.length());// 创建一个QByteArray用于存储要发送的所有数据QByteArray block;QDataStream out(&block, QIODevice::WriteOnly);// 设置数据流使用网络字节序out.setByteOrder(QDataStream::BigEndian);// 写入ID和长度out << id << len;// 添加字符串数据block.append(dataBytes);//判断是否正在发送if (_pending) {//放入队列直接返回,因为目前有数据正在发送_send_queue.enqueue(block);return;}// 没有正在发送,把这包设为“当前块”,重置计数,并写出去_current_block = block;        // ← 保存当前正在发送的 block_bytes_sent = 0;            // ← 归零_pending = true;         // ← 标记正在发送qint64 written = _socket.write(_current_block);qDebug() << "tcp mgr send byte data is" << _current_block<< ", write() returned" << written;
}

在此之后我们还要新加一个槽函数,关于每次写入之后,返回一个信号,指明写入的字节数,我们连接这个信号:

QObject::connect(&_socket, &QTcpSocket::bytesWritten, this, [this](qint64 bytes) {//更新发送数据_bytes_sent += bytes;//未发送完整if (_bytes_sent < _current_block.size()) {//继续发送auto data_to_send = _current_block.mid(_bytes_sent);_socket.write(data_to_send);return;}//发送完全,则查看队列是否为空if (_send_queue.isEmpty()) {//队列为空,说明已经将所有数据发送完成,将pending设置为false,这样后续要发送数据时可以继续发送_current_block.clear();_pending = false;_bytes_sent = 0;return;}//队列不为空,则取出队首元素_current_block = _send_queue.dequeue();_bytes_sent = 0;_pending = true;qint64 w2 = _socket.write(_current_block);qDebug() << "[TcpMgr] Dequeued and write() returned" << w2;
});
http://www.jsqmd.com/news/135690/

相关文章:

  • Java计算机毕设之基于java的web仓库管理系统设计与实现基于java零售与仓储管理系统的设计与实现(完整前后端代码+说明文档+LW,调试定制等)
  • 【计算机毕业设计案例】基于Java的饭店点餐系统设计与实现基于JavaWeb的点餐系统的设计与实现(程序+文档+讲解+定制)
  • Java毕设选题推荐:基于JavaWeb的点餐系统的设计与实现基于JavaWeb的餐厅点餐系统设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】
  • 学长亲荐10个AI论文软件,助你搞定本科生毕业论文!
  • 佳能LBP2900 linux驱动 captdriver - 童晓伟
  • Chap18-AddFriend
  • 个人理财收支记账系统 家庭理财系统APP_vj9n8--小程序论文
  • Chap23-Heartbeat
  • Java毕设项目:基于JavaWeb的点餐系统的设计与实现(源码+文档,讲解、调试运行,定制等)
  • 路由策略和策略路由区别是什么
  • 深入剖析WordPress插件漏洞:未授权攻击的成功之道
  • Java毕设项目:基于SpringBoot+Vue的高校志愿活动管理系统的设计与实现(源码+文档,讲解、调试运行,定制等)
  • Chap24-Reconnect-LockPrecisionOptimization-AvatarEditBox
  • 如果计算引擎是MapReduce,那么Hive能跑Spark SQL作业吗?
  • GIF压缩策略优化:从激进到智能的演进之路
  • 告别复杂笔记软件!Memos+cpolar,让你的笔记随时随地可用
  • Asio18-Coroutine
  • 一个 .NET 开源免费、功能强大的 UI 自动化库
  • Chap20-Communication
  • 03. 图像的基本处理
  • Asio16-MultiThreadServicesPool
  • Chap17-SearchUsers
  • 2025继续教育必备8个降AI率工具测评榜单
  • Asio17-MultiThreadPool
  • 【大模型技术研究】SGLang入门指南:高效大模型推理与编程的利器(附实战代码)
  • 一个使用 WPF 开发的 Diagram 画板工具(包含流程图FlowChart,思维导图MindEditor)
  • 领导根本不关心你干了多少活,只在意这3点
  • 70
  • Asio12-HandlePacketStickingProblemSimply
  • 第三章 SQL Server函数