【C++/Qt】Qt 封装 TCP 客户端底层 Network 类:连接、收发、自动测试与错误处理

在前面的 TCP Client 界面中,按钮、输入框、日志框都属于界面层逻辑。但真正负责 TCP 通信的部分,不应该全部写在界面类里。更合理的做法是把底层网络通信单独封装成一个 Network 类。

这样界面层只需要关心:

复制代码
用户点击了什么按钮
界面控件如何启用/禁用
日志如何显示

Network 类专门负责:

复制代码
连接服务器
断开连接
发送数据
接收数据
处理网络错误
自动测试定时发送

这个模块的核心就是:QTcpSocket 负责 TCP 通信,用信号槽把网络结果通知给界面层。 项目中的 Network 类继承自 QObject,内部封装了 QTcpSocketQTimer、接收缓冲区、自动测试消息计数和连接相关信号。

一、为什么要单独封装 Network 类?

做 TCP 客户端时,最直接的写法是把 QTcpSocket 直接放到界面类里,比如 FormTCPClient 里面直接连接服务器、发送数据、读取数据。

这种写法能跑,但后期会有几个问题:

复制代码
1. 界面类越来越大,既管界面又管网络
2. TCP 连接、发送、接收逻辑难以复用
3. 网络错误处理分散在多个按钮函数里
4. 自动测试、缓冲区、性能优化等逻辑会让界面类变乱

所以更好的思路是:把 TCP 通信抽成一个底层类,界面只调用它提供的接口。

这个类可以设计成下面这样:

cpp 复制代码
class Network : public QObject
{
    Q_OBJECT

public:
    explicit Network(QObject *parent = nullptr);
    ~Network();

    bool ClientConnectionToServer(QString serverIpAddress, int serverPort);
    void ClientSendMsgToServer(const QString &strData);
    void ClientSendBytesToServer(const QByteArray &data);
    void DisconnectFromHost();

signals:
    void connectionEstablished();
    void connectionFailed(const QString errorString);
    void dataReceived(const QString &data);
};

这个设计有一个很重要的思想:

Network 不直接操作界面,它只负责发信号告诉外部"连接成功了""连接失败了""收到数据了"。

这样 FormTCPClient 就可以这样使用它:

cpp 复制代码
connect(&NetworkClient, &Network::connectionEstablished, this, [this]() {
    // 界面层处理连接成功后的按钮状态和日志显示
});

connect(&NetworkClient, &Network::connectionFailed, this, [this](const QString &error) {
    // 界面层处理连接失败提示
});

connect(&NetworkClient, &Network::dataReceived, this, [this](const QString &data) {
    // 界面层显示接收到的数据
});

这样分层后,职责就很清楚:

cpp 复制代码
Network:只负责 TCP 通信
FormTCPClient:只负责界面展示和用户操作

二、初始化时要准备什么:socket、timer 和信号槽

一个 TCP 网络类,最核心的对象是:

cpp 复制代码
QTcpSocket *socket;

它负责真正的 TCP 连接、发送、接收。

如果要做自动测试,还需要一个定时器:

cpp 复制代码
QTimer *timer;

构造函数中要完成几件事:

cpp 复制代码
1. 创建 QTcpSocket
2. 创建 QTimer
3. 预分配接收缓冲区
4. 设置 socket 参数
5. 连接 socket 的关键事件信号
6. 连接 timer 的 timeout 信号

项目中构造函数里创建了 QTcpSocketQTimer,为接收缓冲区预留空间,并连接了 disconnectedreadyReadconnectederrorOccurred 等信号。

核心代码可以这样写:

cpp 复制代码
Network::Network(QObject *parent)
    : QObject(parent)
    , testMessageCount(0)
{
    // 创建 TCP 套接字
    socket = new QTcpSocket(this);

    // 创建自动测试定时器
    timer = new QTimer(this);

    // 提前给接收缓冲区分配空间,减少后续频繁扩容
    receiveBuffer.reserve(8192);

    // 配置 socket 参数
    setSocketOptions();

    // 连接断开信号:服务器断开或主动断开后触发
    connect(socket,
            &QTcpSocket::disconnected,
            this,
            &Network::ClientDisconnectFunc,
            Qt::QueuedConnection);

    // 连接可读信号:服务器发来数据后触发
    connect(socket,
            &QTcpSocket::readyRead,
            this,
            &Network::ReadServeMsg,
            Qt::DirectConnection);

    // 连接成功信号:connectToHost 成功后触发
    connect(socket,
            &QTcpSocket::connected,
            this,
            &Network::onConnected,
            Qt::QueuedConnection);

    // 连接错误信号:连接失败、服务器关闭、超时等情况触发
    connect(socket,
            SIGNAL(errorOccurred(QAbstractSocket::SocketError)),
            this,
            SLOT(onSocketError(QAbstractSocket::SocketError)));

    // 自动测试定时发送
    connect(timer,
            &QTimer::timeout,
            this,
            &Network::StartTimeOutFunc,
            Qt::DirectConnection);

    // 使用精确定时器,提高自动测试触发稳定性
    timer->setTimerType(Qt::PreciseTimer);
}

这里重点不是代码写法,而是设计思路:

QTcpSocket 的很多行为都是异步的,连接成功、收到数据、连接失败都不是立即返回结果,而是通过信号通知。因此网络类的核心就是提前把这些信号接好。

三、连接服务器:不能重复连接,要先判断 socket 状态

TCP 是面向连接的协议,所以连接服务器时不能无脑调用:

cpp 复制代码
socket->connectToHost(ip, port);

因为当前 socket 可能已经处于这些状态:

cpp 复制代码
正在连接
已经连接
正在断开
未连接

如果已经连接到同一个服务器,就没必要重复连接。

如果已经连接到另一个服务器,就应该先断开旧连接,再连接新地址。

项目中的 ClientConnectionToServer() 会先获取当前 socket 状态,如果已经连接或正在连接,会判断目标 IP 和端口是否相同;相同则直接返回,不同则先断开旧连接,必要时调用 abort() 强制关闭,最后才调用 connectToHost() 发起异步连接。

核心逻辑可以这样写:

cpp 复制代码
bool Network::ClientConnectionToServer(QString serverIpAddress, int serverPort)
{
    QAbstractSocket::SocketState currentState = socket->state();

    // 如果正在连接或已经连接,需要先判断当前连接状态
    if (currentState == QAbstractSocket::ConnectingState ||
        currentState == QAbstractSocket::ConnectedState) {

        // 如果已经连接到同一个服务器,直接复用当前连接
        if (socket->peerAddress().toString() == serverIpAddress &&
            socket->peerPort() == serverPort) {
            qDebug() << "Already connected to"
                     << serverIpAddress << ":" << serverPort;
            return true;
        }

        // 如果连接的是另一个服务器,先断开当前连接
        socket->disconnectFromHost();

        // 如果无法正常断开,强制关闭
        if (socket->state() != QAbstractSocket::UnconnectedState) {
            socket->abort();
        }
    }

    // 确保 socket 已经处于未连接状态
    if (socket->state() != QAbstractSocket::UnconnectedState) {
        qWarning() << "Socket is not in unconnected state";
        return false;
    }

    // 发起异步连接
    socket->connectToHost(serverIpAddress, serverPort);

    return true;
}

这里要注意一点:

cpp 复制代码
socket->connectToHost(serverIpAddress, serverPort);

这不是同步连接,不是调用完就代表连接成功。它只是"发起连接请求"。

真正连接成功后,会触发:

cpp 复制代码
QTcpSocket::connected

然后执行:

cpp 复制代码
Network::onConnected()

onConnected() 里,模块会重置自动测试计数、启动性能计时器、重新配置 socket 参数,并向外发出 connectionEstablished() 信号。

cpp 复制代码
void Network::onConnected()
{
    // 重置自动测试计数
    testMessageCount.store(0);

    // 启动连接性能计时器
    performanceTimer.start();

    // 配置 socket 参数
    setSocketOptions();

    // 通知外部:连接已经建立
    emit connectionEstablished();
}

这样界面层不需要直接判断 socket 是否连接成功,只需要监听 connectionEstablished() 信号即可。

四、发送和接收:文本要转字节,接收要做保护

TCP 发送的本质不是发送 QString,而是发送一串字节。

所以发送文本时,需要先把字符串转成 UTF-8 字节数组:

cpp 复制代码
const QByteArray data = strData.toUtf8();

然后再写入 socket:

cpp 复制代码
socket->write(data);

项目中 ClientSendMsgToServer() 会先检查 socket 是否存在、是否处于 ConnectedState,然后将 QString 转成 UTF-8 后写入 socket;如果 write() 返回负数,则说明发送失败。

关键代码如下:

cpp 复制代码
void Network::ClientSendMsgToServer(const QString &strData)
{
    // 未连接时不能发送
    if (!socket || socket->state() != QAbstractSocket::ConnectedState) {
        qWarning() << "Socket not connected, cannot send data";
        return;
    }

    // QString 转 UTF-8 字节数组
    const QByteArray data = strData.toUtf8();

    // 写入 socket,发送给服务器
    const qint64 bytesWritten = socket->write(data);

    if (bytesWritten < 0) {
        qWarning() << "Write failed:" << socket->errorString();
        return;
    }

    // 预留扩展点:后续可以统计发送字节数
    if (bytesWritten > 0) {
        QMutexLocker locker(&bufferMutex);
    }
}

如果要发送原始字节,比如二进制数据、文件数据,则不应该再转 UTF-8,而是直接发送 QByteArray

cpp 复制代码
void Network::ClientSendBytesToServer(const QByteArray &data)
{
    if (!socket || socket->state() != QAbstractSocket::ConnectedState) {
        qWarning() << "Socket not connected, cannot send data";
        return;
    }

    const qint64 bytesWritten = socket->write(data);

    if (bytesWritten > 0) {
        QMutexLocker locker(&bufferMutex);
    }
}

这就是为什么同时保留两个发送函数:

cpp 复制代码
ClientSendMsgToServer(QString)      用于普通文本
ClientSendBytesToServer(QByteArray) 用于原始字节

接收数据时,思路正好反过来:

cpp 复制代码
socket 有数据可读
        ↓
readyRead 信号触发
        ↓
ReadServeMsg() 读取 socket 缓冲区
        ↓
将 QByteArray 转成 QString
        ↓
emit dataReceived(data)
        ↓
界面层显示到日志框

项目中的 ReadServeMsg() 会先检查 socket 是否有效且已连接,然后通过 bytesAvailable() 获取可读数据大小;如果数据超过 1MB,会直接丢弃,避免异常大数据导致内存压力;正常情况下会读取所有数据,按 UTF-8 转为字符串,然后发出 dataReceived() 信号。

关键代码如下:

cpp 复制代码
void Network::ReadServeMsg()
{
    // socket 不可用或未连接时,不读取
    if (!socket || socket->state() != QAbstractSocket::ConnectedState) {
        qWarning() << "Socket not ready, skip read";
        return;
    }

    QMutexLocker locker(&bufferMutex);

    const qint64 bytesAvailable = socket->bytesAvailable();

    if (bytesAvailable <= 0) {
        return;
    }

    // 单次最大读取限制,避免异常数据导致内存压力
    static const qint64 kMaxBytes = 1024 * 1024;

    if (bytesAvailable > kMaxBytes) {
        qWarning() << "Too much incoming data, drop packet of size"
                   << bytesAvailable;

        socket->read(bytesAvailable);
        return;
    }

    // 如果当前缓冲区容量不足,则扩大容量
    if (receiveBuffer.capacity() < bytesAvailable) {
        receiveBuffer.reserve(bytesAvailable * 2);
    }

    // 读取所有可用数据
    receiveBuffer = socket->readAll();

    // UTF-8 解码为字符串
    strTempData = QString::fromUtf8(receiveBuffer);

    // 提前释放锁,再发信号,避免外部槽函数执行时间过长占用锁
    locker.unlock();

    // 通知外部:收到数据
    emit dataReceived(strTempData);
}

这里有几个细节比较值得注意:

cpp 复制代码
1. 读取前检查连接状态,避免无效读取
2. 用 QMutexLocker 保护接收缓冲区
3. 限制单次最大读取 1MB,避免异常数据冲击
4. 根据实际数据大小动态扩容 receiveBuffer
5. 读取完成后通过 signal 通知界面,而不是直接操作界面

这就是一个比较完整的接收处理思路。

五、自动测试、错误处理和资源释放

网络调试工具通常需要自动测试功能,比如每隔一段时间向服务器发送一次测试数据,用来观察连接是否稳定。

这个功能可以用 QTimer 实现。

项目中的 StartTimeOutFunc() 会先检查 socket 和 timer 是否有效,再判断 socket 是否处于连接状态。如果没有连接,就停止定时器;如果已经连接,就递增自动测试计数,并根据用户设置的消息内容生成发送文本,最后调用 ClientSendMsgToServer() 发送。

核心思路如下:

cpp 复制代码
void Network::StartTimeOutFunc()
{
    if (!socket || !timer) {
        return;
    }

    // 未连接时停止自动测试
    if (socket->state() != QAbstractSocket::ConnectedState) {
        timer->stop();
        return;
    }

    // 原子递增计数,保证计数安全
    const int currentCount = testMessageCount.fetch_add(1);

    QString messageToSend;

    if (!autoTestMessage.isEmpty()) {
        messageToSend = autoTestMessage;

        // 如果消息中包含 %1,则替换成当前计数
        if (messageToSend.contains("%1")) {
            messageToSend = messageToSend.arg(currentCount);
        }
    } else {
        messageToSend = QStringLiteral("\n[Prompt:Client automatic testing(%1)]")
                            .arg(currentCount);
    }

    // 发送自动测试消息
    ClientSendMsgToServer(messageToSend);

    // 如果定时器没有启动,则启动 1500ms 定时发送
    if (!timer->isActive()) {
        timer->start(1500);
    }
}

配套的设置和停止函数也很简单:

cpp 复制代码
void Network::setAutoTestMessage(const QString &message)
{
    autoTestMessage = message;
}

void Network::StopTimerOutFunc()
{
    timer->stop();
}

错误处理也不应该直接写在界面层。底层 socket 出错时,应该由 Network 转换成更友好的错误信息,再通过信号发出去。

项目中 onSocketError() 根据 QAbstractSocket::SocketError 类型,将错误转换为中文提示,比如"连接被拒绝""远程主机关闭连接""主机未找到""连接超时",最后发出 connectionFailed(errorString) 信号。

cpp 复制代码
void Network::onSocketError(QAbstractSocket::SocketError error)
{
    QString errorString;

    switch (error) {
    case QAbstractSocket::ConnectionRefusedError:
        errorString = "连接被拒绝";
        break;
    case QAbstractSocket::RemoteHostClosedError:
        errorString = "远程主机关闭连接";
        break;
    case QAbstractSocket::HostNotFoundError:
        errorString = "主机未找到";
        break;
    case QAbstractSocket::SocketTimeoutError:
        errorString = "连接超时";
        break;
    default:
        errorString = socket ? socket->errorString() : "未知网络错误";
        break;
    }

    emit connectionFailed(errorString);
}

最后是资源释放。Network 析构时要停止定时器、断开信号,并处理 socket 连接。如果 socket 还没有断开,会先调用 disconnectFromHost(),如果仍未断开,再调用 abort() 强制关闭。项目析构函数中就做了这类保护处理。

cpp 复制代码
Network::~Network()
{
    if (timer) {
        timer->stop();
        timer->disconnect();
    }

    if (socket) {
        socket->disconnect();

        if (socket->state() != QAbstractSocket::UnconnectedState) {
            socket->disconnectFromHost();

            if (socket->state() != QAbstractSocket::UnconnectedState) {
                socket->abort();
            }
        }
    }
}

这里的设计思想是:

网络对象销毁时,不能假设 socket 一定已经断开。要主动停止定时器、断开信号、关闭连接,避免程序退出时出现资源残留或异常状态。

总结

Network 类的核心价值不是简单包装几个 QTcpSocket 函数,而是把 TCP 客户端底层通信做成一个独立模块。

它主要解决了这些问题:

cpp 复制代码
1. 用 QTcpSocket 管理 TCP 连接、发送、接收
2. 用信号槽把连接成功、连接失败、收到数据通知给界面层
3. 连接前检查当前 socket 状态,避免重复连接或状态混乱
4. 发送文本时统一转 UTF-8,发送字节时直接写 QByteArray
5. 接收数据时加入缓冲区、互斥锁和最大数据量保护
6. 用 QTimer 实现自动测试定时发送
7. 用 onSocketError() 统一转换网络错误信息
8. 析构时停止定时器并安全关闭 socket

整体结构可以概括成:

cpp 复制代码
FormTCPClient 负责界面
Network 负责通信
QTcpSocket 负责底层 TCP
QTimer 负责自动测试
信号槽负责模块之间通信

这样设计后,界面层不用关心底层 socket 的细节,只需要调用:

cpp 复制代码
NetworkClient.ClientConnectionToServer(ip, port);
NetworkClient.ClientSendMsgToServer(message);
NetworkClient.DisconnectFromHost();

再监听:

cpp 复制代码
connectionEstablished()
connectionFailed(errorString)
dataReceived(data)

就能完成 TCP 客户端的主要功能。

这也是 Qt 项目中比较常见的一种写法:界面逻辑和网络通信分离,底层模块只发信号,不直接操作界面。

0voice · GitHub

相关推荐
KKKlucifer1 小时前
日志审计与行为分析在安全服务中的应用实践
网络·人工智能·安全
CodeOfCC1 小时前
Linux 嵌入式arm64安装openclaw
linux·运维·服务器
Aray12342 小时前
浅析内网跨网段连通差异:ICMP不可达与静默丢包底层原理拆解
网络·ping
Unbelievabletobe2 小时前
港股api的WebSocket推送如何订阅多只股票
网络·websocket·网络协议
羑悻的小杀马特2 小时前
零成本搞定!异地访问 OpenClaw 最简方案:SSH 端口映射组网!
运维·服务器·人工智能·docker·自动化·ssh·openclaw
TechWayfarer2 小时前
IP归属地运营商能解决什么问题?风控/增长/数据平台落地实践(附API代码)
开发语言·网络·python·网络协议·tcp/ip
TechWayfarer2 小时前
IP归属地运营商生产落地进阶:缓存+降级+灰度对账全解析
网络·python·网络协议·tcp/ip·缓存
magrich3 小时前
安装NoMachine并解决无外接显示器桌面黑屏
linux·运维·服务器
funnycoffee1233 小时前
华为USG防火墙修改tcp aging time , default is 1200S
网络·网络协议·tcp/ip·usg aging time