在前面的 TCP Client 界面中,按钮、输入框、日志框都属于界面层逻辑。但真正负责 TCP 通信的部分,不应该全部写在界面类里。更合理的做法是把底层网络通信单独封装成一个 Network 类。
这样界面层只需要关心:
用户点击了什么按钮
界面控件如何启用/禁用
日志如何显示
而 Network 类专门负责:
连接服务器
断开连接
发送数据
接收数据
处理网络错误
自动测试定时发送
这个模块的核心就是:用 QTcpSocket 负责 TCP 通信,用信号槽把网络结果通知给界面层。 项目中的 Network 类继承自 QObject,内部封装了 QTcpSocket、QTimer、接收缓冲区、自动测试消息计数和连接相关信号。
一、为什么要单独封装 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 信号
项目中构造函数里创建了 QTcpSocket 和 QTimer,为接收缓冲区预留空间,并连接了 disconnected、readyRead、connected、errorOccurred 等信号。
核心代码可以这样写:
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 项目中比较常见的一种写法:界面逻辑和网络通信分离,底层模块只发信号,不直接操作界面。