一、问题的由来
在 Qt 里做一个通讯静态库,同时支持串口、TCP、UDP,一个常见的"正确做法"是:为每个通讯通道单独开一个工作线程,并让通讯对象在这个线程里完成创建、使用、销毁的全生命周期。
很多资料只会笼统地说"怕卡界面",于是初学者往往把它简化成"在某个线程里调用一下 send() 就行了"。但只要稍微深入 Qt 的线程模型就会发现,这个架构不是过度设计,而是被一组环环相扣的硬性约束逼出来的。本文先把这些知识点讲透,再给出一个可直接编译运行的完整工程。
二、核心知识点
1. 线程亲和性(Thread Affinity)
每一个 QObject 及其派生类(QSerialPort、QTcpSocket、QUdpSocket 都是 QObject 子类)都有一个"归属线程"。归属关系在对象被 new 的那一刻 确定:归属于执行 new 的线程。
线程亲和性决定了三件事:
-
事件派发:对象的事件(定时器、网络 I/O 就绪通知等)只由所属线程的事件循环派发。
-
信号槽连接类型 :
AutoConnection(默认)在信号发出时比较"发射线程"与"接收对象归属线程",相同则直接调用,不同则自动转为排队连接。 -
操作合法性 :
QObject不是线程安全的,跨线程直接调用其方法属于未定义行为。
可以用 QObject::thread() 查询归属线程,用 moveToThread() 更改------但通信类不能靠 moveToThread 补救,原因见下一条。
2. QSocketNotifier 与事件循环的"硬绑定"
这是最容易踩的坑。QTcpSocket、QUdpSocket 底层依赖操作系统提供的异步 I/O 通知机制,Qt 为此在内部创建了 QSocketNotifier 对象来监听套接字上的可读/可写事件。
关键在于:QSocketNotifier 在创建时与"当时的线程事件循环"绑定 ,moveToThread() 搬走的只是通信对象本身,QSocketNotifier 的注册关系并不会跟着搬。
后果是:如果 QTcpSocket 在主线程 new,随后被 moveToThread 到工作线程,那么操作系统层面的 I/O 就绪通知仍然会被派发回主线程,而搬走后的对象却在等新线程的通知------readyRead() 永远等不到,通信静默失效。
所以正确的做法只有一个:在哪个线程使用,就在哪个线程从头 new ,让 QSocketNotifier 的"出生地"与"工作地"一致。
3. 内部状态非线程安全
QTcpSocket、QUdpSocket、QSerialPort 都继承自 QIODevice。Qt 官方明确说明这类 I/O 设备不能跨线程调用 ,因为其内部缓冲区与内部状态都没有互斥锁保护。
Qt 核心开发者 Thiago Macieira 曾解释:QAbstractSocket 会通过 QSocketNotifier 自动尝试读写,如果从多个线程使用它,"你将遇到大量数据竞争条件,导致数据丢失或程序崩溃"。
这意味着,即便你试图用外部互斥锁"保护"对同一个 Socket 的调用,也无法覆盖 Qt 内部在事件循环驱动下对缓冲区的自动访问。唯一安全的并发模型是"独占":一个 Socket 对象在任意时刻只被其归属线程访问。
4. 跨线程信号槽的"排队"本质
跨线程信号槽之所以安全,是因为 AutoConnection 检测到发射线程 ≠ 接收对象归属线程时,会自动转为队列连接(QueuedConnection):
-
槽函数不会在发射线程执行;
-
Qt 将这次调用封装成一个事件,投递到接收者所属线程的事件队列,由接收线程的事件循环稍后取出执行。
由此带来两个推论:
-
异步性:跨线程信号槽调用是异步的,发射方不会等待槽函数完成。
-
参数须可拷贝 :队列连接要求参数能被复制存储,自定义类型须
qRegisterMetaType()注册。
一个危险的错误是跨线程使用 Qt::DirectConnection,这会让槽函数在发射线程中直接执行,绕过队列机制,等价于一个线程直接操作另一个线程的对象,直接触发第 3 条的数据竞争。
5. deleteLater 的线程语义
QObject 的销毁同样受线程归属约束。工作线程中创建的 Socket 及其 QSocketNotifier,若被主线程直接 delete,属于未定义行为。
正确做法是 deleteLater(),它的语义是:在当前控制流返回到对象所属线程的事件循环时,才执行删除。如果对象所在线程的事件循环没有运行,该对象会在线程结束时被销毁。
因此"对象在该线程生,就在该线程死"是管理生命周期最省心的方式。
6. 阻塞 API 的"污染"与隔离设计
即便只使用异步 API,独立线程提供的隔离性也不可替代。
waitForReadyRead()、waitForBytesWritten()、waitForConnected() 等阻塞函数会挂起调用线程。若在 GUI 主线程调用,界面冻结;在某些平台/绑定下,在次级线程调用也可能间接影响主线程的事件循环。此外 Qt 要求这类阻塞函数必须在创建 Socket 的同一线程中调用,否则会出现随机行为和调试断言。
独立工作线程把这类型行为严格限制在一个隔离的"沙箱"里:一个通道阻塞或数据处理繁重,只会影响它自己。
7. 纯虚 QObject 基类的编译单元
ICommDevice 作为纯虚 QObject 基类,可以不写 .cpp,前提是:
-
其
.h必须出现在add_library/add_executable的源文件列表中,让 CMakeAUTOMOC为其生成 moc; -
该头文件至少被某个
.cppinclude 过,避免 moc 输出找不到符号。
否则链接时会出现 undefined reference to vtable for ICommDevice。
三、架构设计
有了上面的约束,架构就自然浮现:
text
┌──────────────┐ 信号排队回主线程 ┌─────────────────────┐
│ MainApp │◄───────────────────│ CommWorkThread │
│ (GUI主线程) │ │ (QThread + run) │
│ │ emit sendRequest │ │
│ │───────────────────►│ ┌────────────────┐ │
└──────────────┘ (Queued) │ │ ICommDevice │ │
│ │ (在工作线程 │ │
│ │ new 出来) │ │
│ └───┬────┬───┬───┘ │
│ Serial Tcp Udp │
└───────────────────────┘
关键设计:
| 关注点 | 方案 |
|---|---|
| 通讯对象创建位置 | 在 CommWorkThread::run() 中根据配置 new,确保 QSocketNotifier 与工作线程事件循环绑定 |
| 事件循环 | run() 中调用 exec(),是 readyRead 能被派发的前提 |
| 接收方向 | 通讯对象信号经 DirectConnection 转发为 CommWorkThread 的信号(发射动作留在工作线程),主程序以 Auto 连接,自动排队回主线程 |
| 发送方向 | 主程序只 emit sendRequest();sendRequest 与通讯对象以 QueuedConnection 相连,send() 在工作线程执行 |
| 状态反馈 | 统一为 CommState 枚举,通过 stateChanged(int)、connected、disconnected、errorOccurred 回主线程 |
| 发送结果反馈 | 每次发送分配请求 ID,write() 结果经 sendFinished(reqId, bytes) 异步返回 |
| 生命周期 | new、open、send、close、delete 全在 run() 内闭环;stop() 先 quit() 再 wait(),确保析构前 run() 已返回 |
| 配置驱动 | 构造函数接收 CommConfig,run() 中 switch 判断类型,主程序无需感知具体通讯方式 |
四、完整工程
4.1 目录结构
QtCommDemo/
├── CMakeLists.txt
├── CommLib/
│ ├── CMakeLists.txt
│ ├── include/
│ │ ├── CommConfig.h
│ │ ├── ICommDevice.h
│ │ ├── SerialCommDevice.h
│ │ ├── TcpCommDevice.h
│ │ ├── UdpCommDevice.h
│ │ └── CommWorkThread.h
│ └── src/
│ ├── SerialCommDevice.cpp
│ ├── TcpCommDevice.cpp
│ ├── UdpCommDevice.cpp
│ └── CommWorkThread.cpp
└── App/
├── CMakeLists.txt
├── MainApp.h
├── MainApp.cpp
└── main.cpp
4.2 根 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(QtCommDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network SerialPort)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network SerialPort)
add_subdirectory(CommLib)
add_subdirectory(App)
4.3 CommLib/CMakeLists.txt
add_library(CommLib STATIC
src/SerialCommDevice.cpp
src/TcpCommDevice.cpp
src/UdpCommDevice.cpp
src/CommWorkThread.cpp
include/CommConfig.h
include/ICommDevice.h
include/SerialCommDevice.h
include/TcpCommDevice.h
include/UdpCommDevice.h
include/CommWorkThread.h
)
target_include_directories(CommLib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_link_libraries(CommLib PUBLIC
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::SerialPort
)
4.4 include/CommConfig.h
#pragma once
#include <QString>
#include <QtGlobal>
// 通讯方式
enum class CommType {
Serial = 0,
Tcp = 1,
Udp = 2,
};
// 对外统一的连接状态,屏蔽底层不同通讯方式的状态差异
enum class CommState {
Unconnected = 0,
Connecting = 1,
Connected = 2,
Closing = 3,
Bound = 4,
};
// 串口配置
struct SerialCommConfig {
QString portName;
int baudRate = 9600;
int dataBits = 8; // QSerialPort::Data8
int parity = 0; // QSerialPort::NoParity
int stopBits = 1; // QSerialPort::OneStop
int flowControl = 0; // QSerialPort::NoFlowControl
};
// TCP 客户端配置
struct TcpCommConfig {
QString host = QStringLiteral("127.0.0.1");
quint16 port = 9000;
};
// UDP 配置
struct UdpCommConfig {
QString localAddress = QStringLiteral("0.0.0.0");
quint16 localPort = 9001;
QString remoteAddress = QStringLiteral("127.0.0.1");
quint16 remotePort = 9001;
};
// 综合配置
struct CommConfig {
CommType type = CommType::Serial;
SerialCommConfig serial;
TcpCommConfig tcp;
UdpCommConfig udp;
};
4.5 include/ICommDevice.h
#pragma once
#include <QObject>
#include <QByteArray>
// 通讯设备抽象基类,定义所有通讯方式共用的接口和信号
class ICommDevice : public QObject
{
Q_OBJECT
public:
explicit ICommDevice(QObject *parent = nullptr) : QObject(parent) {}
~ICommDevice() override = default;
// 打开设备
virtual bool open() = 0;
// 关闭设备
virtual void close() = 0;
// 发送数据,返回写入的字节数,失败返回负值
virtual qint64 send(const QByteArray &data) = 0;
// 查询设备是否处于可通讯状态
virtual bool isOpen() const = 0;
signals:
// 收到数据
void dataReceived(const QByteArray &data);
// 连接建立
void connected();
// 连接断开
void disconnected();
// 发生错误
void errorOccurred(const QString &error);
// 连接状态变化,值为 CommState 的整数值
void stateChanged(int state);
};
4.6 include/SerialCommDevice.h
#pragma once
#include "ICommDevice.h"
#include "CommConfig.h"
#include <QSerialPort>
// 串口通讯设备
class SerialCommDevice : public ICommDevice
{
Q_OBJECT
public:
explicit SerialCommDevice(const CommConfig &cfg, QObject *parent = nullptr);
bool open() override;
void close() override;
qint64 send(const QByteArray &data) override;
bool isOpen() const override;
private slots:
// 处理串口收到的数据
void onReadyRead();
private:
QSerialPort *m_serial = nullptr;
CommConfig m_cfg;
};
4.7 src/SerialCommDevice.cpp
#include "SerialCommDevice.h"
SerialCommDevice::SerialCommDevice(const CommConfig &cfg, QObject *parent)
: ICommDevice(parent)
, m_serial(new QSerialPort(this))
, m_cfg(cfg)
{
connect(m_serial, &QSerialPort::readyRead,
this, &SerialCommDevice::onReadyRead);
connect(m_serial, &QSerialPort::errorOccurred, this,
[this](QSerialPort::SerialPortError err) {
if (err != QSerialPort::NoError)
emit errorOccurred(m_serial->errorString());
});
}
bool SerialCommDevice::open()
{
m_serial->setPortName(m_cfg.serial.portName);
m_serial->setBaudRate(m_cfg.serial.baudRate);
m_serial->setDataBits(static_cast<QSerialPort::DataBits>(m_cfg.serial.dataBits));
m_serial->setParity(static_cast<QSerialPort::Parity>(m_cfg.serial.parity));
m_serial->setStopBits(static_cast<QSerialPort::StopBits>(m_cfg.serial.stopBits));
m_serial->setFlowControl(static_cast<QSerialPort::FlowControl>(m_cfg.serial.flowControl));
if (m_serial->open(QIODevice::ReadWrite)) {
emit stateChanged(static_cast<int>(CommState::Connected));
emit connected();
return true;
}
emit stateChanged(static_cast<int>(CommState::Unconnected));
emit errorOccurred(m_serial->errorString());
return false;
}
void SerialCommDevice::close()
{
if (m_serial->isOpen()) {
m_serial->close();
emit stateChanged(static_cast<int>(CommState::Unconnected));
emit disconnected();
}
}
qint64 SerialCommDevice::send(const QByteArray &data)
{
if (!m_serial->isOpen())
return -1;
return m_serial->write(data);
}
bool SerialCommDevice::isOpen() const
{
return m_serial->isOpen();
}
void SerialCommDevice::onReadyRead()
{
const QByteArray data = m_serial->readAll();
if (!data.isEmpty())
emit dataReceived(data);
}
4.8 include/TcpCommDevice.h
#pragma once
#include "ICommDevice.h"
#include "CommConfig.h"
#include <QTcpSocket>
// TCP 客户端通讯设备
class TcpCommDevice : public ICommDevice
{
Q_OBJECT
public:
explicit TcpCommDevice(const CommConfig &cfg, QObject *parent = nullptr);
bool open() override;
void close() override;
qint64 send(const QByteArray &data) override;
bool isOpen() const override;
private slots:
// 处理 TCP 收到的数据
void onReadyRead();
// 连接建立时上报状态
void onConnected();
// 连接断开时上报状态
void onDisconnected();
// 上报套接字错误
void onSocketError(QAbstractSocket::SocketError err);
// 将底层状态翻译为 CommState
void onSocketStateChanged(QAbstractSocket::SocketState state);
private:
QTcpSocket *m_socket = nullptr;
CommConfig m_cfg;
};
4.9 src/TcpCommDevice.cpp
#include "TcpCommDevice.h"
// 将 Qt 套接字状态翻译为统一的 CommState
static CommState toCommState(QAbstractSocket::SocketState s)
{
switch (s) {
case QAbstractSocket::UnconnectedState: return CommState::Unconnected;
case QAbstractSocket::HostLookupState:
case QAbstractSocket::ConnectingState: return CommState::Connecting;
case QAbstractSocket::ConnectedState: return CommState::Connected;
case QAbstractSocket::BoundState: return CommState::Bound;
case QAbstractSocket::ClosingState: return CommState::Closing;
default: return CommState::Unconnected;
}
}
TcpCommDevice::TcpCommDevice(const CommConfig &cfg, QObject *parent)
: ICommDevice(parent)
, m_socket(new QTcpSocket(this))
, m_cfg(cfg)
{
connect(m_socket, &QTcpSocket::readyRead,
this, &TcpCommDevice::onReadyRead);
connect(m_socket, &QTcpSocket::connected,
this, &TcpCommDevice::onConnected);
connect(m_socket, &QTcpSocket::disconnected,
this, &TcpCommDevice::onDisconnected);
connect(m_socket, &QAbstractSocket::errorOccurred,
this, &TcpCommDevice::onSocketError);
connect(m_socket, &QAbstractSocket::stateChanged,
this, &TcpCommDevice::onSocketStateChanged);
}
bool TcpCommDevice::open()
{
// 异步连接,结果通过 connected / errorOccurred 通知
m_socket->connectToHost(m_cfg.tcp.host, m_cfg.tcp.port);
return true;
}
void TcpCommDevice::close()
{
m_socket->disconnectFromHost();
}
qint64 TcpCommDevice::send(const QByteArray &data)
{
if (m_socket->state() != QAbstractSocket::ConnectedState)
return -1;
return m_socket->write(data);
}
bool TcpCommDevice::isOpen() const
{
return m_socket->state() == QAbstractSocket::ConnectedState;
}
void TcpCommDevice::onReadyRead()
{
const QByteArray data = m_socket->readAll();
if (!data.isEmpty())
emit dataReceived(data);
}
void TcpCommDevice::onConnected()
{
emit connected();
}
void TcpCommDevice::onDisconnected()
{
emit disconnected();
}
void TcpCommDevice::onSocketError(QAbstractSocket::SocketError)
{
emit errorOccurred(m_socket->errorString());
}
void TcpCommDevice::onSocketStateChanged(QAbstractSocket::SocketState state)
{
emit stateChanged(static_cast<int>(toCommState(state)));
}
4.10 include/UdpCommDevice.h
#pragma once
#include "ICommDevice.h"
#include "CommConfig.h"
#include <QUdpSocket>
// UDP 通讯设备
class UdpCommDevice : public ICommDevice
{
Q_OBJECT
public:
explicit UdpCommDevice(const CommConfig &cfg, QObject *parent = nullptr);
bool open() override;
void close() override;
qint64 send(const QByteArray &data) override;
bool isOpen() const override;
private slots:
// 处理 UDP 收到的数据报
void onReadyRead();
// 将底层状态翻译为 CommState
void onSocketStateChanged(QAbstractSocket::SocketState state);
private:
QUdpSocket *m_socket = nullptr;
CommConfig m_cfg;
};
4.11 src/UdpCommDevice.cpp
#include "UdpCommDevice.h"
#include <QHostAddress>
// 将 Qt 套接字状态翻译为统一的 CommState
static CommState toCommState(QAbstractSocket::SocketState s)
{
switch (s) {
case QAbstractSocket::UnconnectedState: return CommState::Unconnected;
case QAbstractSocket::HostLookupState:
case QAbstractSocket::ConnectingState: return CommState::Connecting;
case QAbstractSocket::ConnectedState: return CommState::Connected;
case QAbstractSocket::BoundState: return CommState::Bound;
case QAbstractSocket::ClosingState: return CommState::Closing;
default: return CommState::Unconnected;
}
}
UdpCommDevice::UdpCommDevice(const CommConfig &cfg, QObject *parent)
: ICommDevice(parent)
, m_socket(new QUdpSocket(this))
, m_cfg(cfg)
{
connect(m_socket, &QUdpSocket::readyRead,
this, &UdpCommDevice::onReadyRead);
connect(m_socket, &QAbstractSocket::stateChanged,
this, &UdpCommDevice::onSocketStateChanged);
}
bool UdpCommDevice::open()
{
if (!m_socket->bind(QHostAddress(m_cfg.udp.localAddress),
m_cfg.udp.localPort)) {
emit errorOccurred(m_socket->errorString());
return false;
}
emit connected();
return true;
}
void UdpCommDevice::close()
{
m_socket->close();
emit disconnected();
}
qint64 UdpCommDevice::send(const QByteArray &data)
{
const QHostAddress remote(m_cfg.udp.remoteAddress);
return m_socket->writeDatagram(data, remote, m_cfg.udp.remotePort);
}
bool UdpCommDevice::isOpen() const
{
return m_socket->state() == QAbstractSocket::BoundState;
}
void UdpCommDevice::onReadyRead()
{
while (m_socket->hasPendingDatagrams()) {
QByteArray buf;
buf.resize(static_cast<int>(m_socket->pendingDatagramSize()));
m_socket->readDatagram(buf.data(), buf.size());
emit dataReceived(buf);
}
}
void UdpCommDevice::onSocketStateChanged(QAbstractSocket::SocketState state)
{
emit stateChanged(static_cast<int>(toCommState(state)));
}
4.12 include/CommWorkThread.h
#pragma once
#include <QThread>
#include <QByteArray>
#include "CommConfig.h"
class ICommDevice;
// 通讯工作线程:在其 run() 中创建通讯对象并运行独立事件循环
class CommWorkThread : public QThread
{
Q_OBJECT
public:
explicit CommWorkThread(const CommConfig &cfg, QObject *parent = nullptr);
~CommWorkThread() override;
// 提交一次发送请求,返回本次请求的 ID
quint64 sendData(const QByteArray &data);
// 请求退出工作线程事件循环并等待线程结束
void stop();
signals:
// 收到数据
void dataReceived(const QByteArray &data);
// 连接建立
void connected();
// 连接断开
void disconnected();
// 发生错误
void errorOccurred(const QString &error);
// 设备创建并尝试打开的结果
void deviceOpened(bool ok);
// 连接状态变化
void stateChanged(int state);
// 发送完成,bytes 为写入字节数,负值表示失败
void sendFinished(quint64 requestId, qint64 bytes);
// 内部信号:主线程投递到工作线程的发送请求
void sendRequest(quint64 requestId, const QByteArray &data);
protected:
void run() override;
private:
CommConfig m_cfg;
ICommDevice *m_device = nullptr;
quint64 m_reqSeq = 0;
};
4.13 src/CommWorkThread.cpp
#include "CommWorkThread.h"
#include "ICommDevice.h"
#include "SerialCommDevice.h"
#include "TcpCommDevice.h"
#include "UdpCommDevice.h"
CommWorkThread::CommWorkThread(const CommConfig &cfg, QObject *parent)
: QThread(parent)
, m_cfg(cfg)
{
}
CommWorkThread::~CommWorkThread()
{
stop();
}
quint64 CommWorkThread::sendData(const QByteArray &data)
{
const quint64 id = ++m_reqSeq;
emit sendRequest(id, data);
return id;
}
void CommWorkThread::stop()
{
if (isRunning()) {
quit();
wait();
}
}
void CommWorkThread::run()
{
// 在工作线程中创建通讯对象,确保内部 QSocketNotifier
// 与工作线程的事件循环绑定
switch (m_cfg.type) {
case CommType::Serial:
m_device = new SerialCommDevice(m_cfg);
break;
case CommType::Tcp:
m_device = new TcpCommDevice(m_cfg);
break;
case CommType::Udp:
m_device = new UdpCommDevice(m_cfg);
break;
}
if (!m_device) {
emit deviceOpened(false);
return;
}
// 直接连接:转发动作在工作线程中执行,
// 主程序连接这些信号时因为线程不同会自动排队回主线程
connect(m_device, &ICommDevice::dataReceived,
this, &CommWorkThread::dataReceived, Qt::DirectConnection);
connect(m_device, &ICommDevice::connected,
this, &CommWorkThread::connected, Qt::DirectConnection);
connect(m_device, &ICommDevice::disconnected,
this, &CommWorkThread::disconnected, Qt::DirectConnection);
connect(m_device, &ICommDevice::errorOccurred,
this, &CommWorkThread::errorOccurred, Qt::DirectConnection);
connect(m_device, &ICommDevice::stateChanged,
this, &CommWorkThread::stateChanged, Qt::DirectConnection);
// 队列连接:发送请求由工作线程串行执行,回发结果
ICommDevice *dev = m_device;
connect(this, &CommWorkThread::sendRequest, dev,
[dev, this](quint64 reqId, const QByteArray &data) {
const qint64 n = dev->send(data);
emit sendFinished(reqId, n);
},
Qt::QueuedConnection);
const bool ok = m_device->open();
emit deviceOpened(ok);
if (ok)
exec();
// exec() 返回后仍在工作线程上下文,安全关闭并销毁设备
if (m_device) {
m_device->close();
delete m_device;
m_device = nullptr;
}
}
4.14 App/CMakeLists.txt
add_executable(CommDemo
main.cpp
MainApp.h
MainApp.cpp
)
target_link_libraries(CommDemo PRIVATE
CommLib
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::SerialPort
)
4.15 App/MainApp.h
#pragma once
#include <QObject>
#include "CommConfig.h"
class CommWorkThread;
// 主程序侧封装,负责与工作线程交互并处理所有反馈
class MainApp : public QObject
{
Q_OBJECT
public:
explicit MainApp(const CommConfig &cfg, QObject *parent = nullptr);
~MainApp() override;
// 提交一次发送,返回请求 ID
quint64 send(const QByteArray &data);
private slots:
// 处理设备创建结果
void onDeviceOpened(bool ok);
// 处理连接建立
void onConnected();
// 处理连接断开
void onDisconnected();
// 处理连接状态变化
void onStateChanged(int state);
// 处理接收到的数据
void onDataReceived(const QByteArray &data);
// 处理发送完成
void onSendFinished(quint64 requestId, qint64 bytes);
// 处理错误
void onError(const QString &error);
private:
CommWorkThread *m_worker = nullptr;
};
4.16 App/MainApp.cpp
#include "MainApp.h"
#include "CommWorkThread.h"
#include <QDebug>
MainApp::MainApp(const CommConfig &cfg, QObject *parent)
: QObject(parent)
, m_worker(new CommWorkThread(cfg, this))
{
connect(m_worker, &CommWorkThread::deviceOpened,
this, &MainApp::onDeviceOpened);
connect(m_worker, &CommWorkThread::connected,
this, &MainApp::onConnected);
connect(m_worker, &CommWorkThread::disconnected,
this, &MainApp::onDisconnected);
connect(m_worker, &CommWorkThread::stateChanged,
this, &MainApp::onStateChanged);
connect(m_worker, &CommWorkThread::dataReceived,
this, &MainApp::onDataReceived);
connect(m_worker, &CommWorkThread::sendFinished,
this, &MainApp::onSendFinished);
connect(m_worker, &CommWorkThread::errorOccurred,
this, &MainApp::onError);
m_worker->start();
}
MainApp::~MainApp()
{
m_worker->stop();
}
quint64 MainApp::send(const QByteArray &data)
{
return m_worker->sendData(data);
}
void MainApp::onDeviceOpened(bool ok)
{
qInfo() << "[Main]" << (ok ? "device opened" : "device open failed");
}
void MainApp::onConnected()
{
qInfo() << "[Main] connected";
}
void MainApp::onDisconnected()
{
qInfo() << "[Main] disconnected";
}
void MainApp::onStateChanged(int state)
{
qInfo() << "[Main] state changed:" << state;
}
void MainApp::onDataReceived(const QByteArray &data)
{
qInfo() << "[Main] recv:" << data;
}
void MainApp::onSendFinished(quint64 requestId, qint64 bytes)
{
if (bytes < 0)
qWarning() << "[Main] send failed, reqId =" << requestId;
else
qInfo() << "[Main] send ok, reqId =" << requestId
<< "bytes =" << bytes;
}
void MainApp::onError(const QString &error)
{
qWarning() << "[Main] error:" << error;
}
4.17 App/main.cpp
#include <QCoreApplication>
#include <QTimer>
#include "MainApp.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
CommConfig cfg;
cfg.type = CommType::Udp;
cfg.udp.localAddress = QStringLiteral("0.0.0.0");
cfg.udp.localPort = 9001;
cfg.udp.remoteAddress = QStringLiteral("127.0.0.1");
cfg.udp.remotePort = 9001;
MainApp demo(cfg);
QTimer::singleShot(1500, &demo, [&demo] {
const quint64 id = demo.send(QByteArrayLiteral("hello from main"));
qInfo() << "[Main] queued send, reqId =" << id;
});
return app.exec();
}
切换通讯方式只需改 cfg.type 与对应子结构体,其余代码无需改动。
五、整体反馈链路一览
| 方向 | 信号 | 说明 |
|---|---|---|
| 设备创建 | deviceOpened(bool) |
open() 的返回值 |
| 连接状态 | connected / disconnected / stateChanged(int) |
三种通讯统一为 CommState |
| 接收 | dataReceived(QByteArray) |
已排队回主线程 |
| 发送 | sendFinished(reqId, bytes) |
write() 的结果,负值表示失败 |
| 错误 | errorOccurred(QString) |
串口与网络统一 |
所有信号都是从工作线程发出,经 AutoConnection 排队回到主线程执行。主线程零阻塞,工作线程内部对通讯对象的访问完全串行化,符合 Qt 的线程模型约束。
六、小结
为串口、TCP、UDP 各自开启独立工作线程,并让通信对象的创建、使用、销毁在同一线程内闭环,根源在于:
-
线程亲和性规定了对象的归属线程;
-
QSocketNotifier与事件循环的硬绑定 要求对象必须在工作线程中诞生,moveToThread无法补救; -
内部状态非线程安全要求独占访问;
-
队列连接划分了跨线程交互的安全边界;
-
deleteLater语义规定了对象必须在其归属线程中销毁; -
阻塞 API 的潜在全局影响要求必须进行故障隔离。
这几条约束共同指向同一个结论:通信对象必须在该线程生,在该线程死。理解了这一点,就理解了 Qt 多线程通讯架构的全部设计动机。