文章目录
- 前言
- 1.服务端实现流程
- 1.1步骤 1:创建 QTcpServer 并监听端口
- 1.2步骤 2:处理新连接请求
- 1.3步骤 3:接收客户端数据
- 1.4步骤 4:处理客户端断开
- 2.客户端实现流程
- 2.1步骤 1:创建 QTcpSocket 并连接服务器
- 2.2步骤 2:发送数据
- 2.3步骤 3:接收服务器回复
- 2.4步骤 4:处理连接和错误
- 3.关键注意事项
- 4.TCP粘包问题及其处理
- 4.1TCP粘包是什么
- 4.2TCP粘包为什么会产生
- 4.3TCP粘包的解决方案
前言
在Qt中实现TCP通信主要依赖 QTcpServer(服务端)和 QTcpSocket(客户端和服务端通信)类。
TCP/IP通信(即SOCKET通信)是通过网线将服务器Server端和客户机Client端进行连接,在遵循ISO/OSI模型的四层层级构架的基础上通过TCP/IP协议建立的通讯。控制器可以设置为服务器端或客户端。
服务端(简化版)
class MyServer : public QObject {Q_OBJECT
public:MyServer(QObject *parent = nullptr) : QObject(parent) {server = new QTcpServer(this);connect(server, &QTcpServer::newConnection, this, &MyServer::onNewConnection);server->listen(QHostAddress::Any, 8888);}private slots:void onNewConnection() { /* ... */ }void onReadyRead() { /* ... */ }void onDisconnected() { /* ... */ }private:QTcpServer *server;QList<QTcpSocket*> m_clients;
};
客户端(简化版)
class MyClient : public QObject {Q_OBJECT
public:MyClient(QObject *parent = nullptr) : QObject(parent) {socket = new QTcpSocket(this);connect(socket, &QTcpSocket::connected, this, &MyClient::onConnected);connect(socket, &QTcpSocket::readyRead, this, &MyClient::onReadyRead);socket->connectToHost("127.0.0.1", 8888);}void send(const QString &message) {socket->write(message.toUtf8());}private slots:void onConnected() { /* ... */ }void onReadyRead() { /* ... */ }private:QTcpSocket *socket;
};
运行效果
服务端启动后监听端口,客户端连接并发送数据。
服务端接收数据并回复,客户端显示回复内容。
断开连接后资源自动释放。
1.服务端实现流程
1.1步骤 1:创建 QTcpServer 并监听端口
// 创建TCP服务端对象
QTcpServer *server = new QTcpServer(this);// 监听所有IP的指定端口(例如8888)
if (!server->listen(QHostAddress::Any, 8888)) {qDebug() << "Server could not start. Error:" << server->errorString();
} else {qDebug() << "Server started on port 8888";
}
1.2步骤 2:处理新连接请求
当客户端连接时,QTcpServer 会触发 newConnection 信号,需通过槽函数处理:
// 连接信号到槽函数
connect(server, &QTcpServer::newConnection, this, &MyServer::onNewConnection);// 槽函数实现
void MyServer::onNewConnection() {// 获取新连接的socket对象QTcpSocket *socket = server->nextPendingConnection();// 存储socket以便后续通信(例如添加到列表)m_clients.append(socket);// 处理客户端数据到达的信号connect(socket, &QTcpSocket::readyRead, this, &MyServer::onReadyRead);// 处理断开连接的信号connect(socket, &QTcpSocket::disconnected, this, &MyServer::onDisconnected);
}
1.3步骤 3:接收客户端数据
通过 readyRead 信号读取数据:
void MyServer::onReadyRead() {QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());if (!socket) return;QByteArray data = socket->readAll();qDebug() << "Received data:" << data;// 示例:回复客户端socket->write("Server received: " + data);
}
1.4步骤 4:处理客户端断开
void MyServer::onDisconnected() {QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());if (!socket) return;m_clients.removeOne(socket);socket->deleteLater();qDebug() << "Client disconnected";
}
2.客户端实现流程
2.1步骤 1:创建 QTcpSocket 并连接服务器
QTcpSocket *socket = new QTcpSocket(this);// 连接服务器(假设服务器IP为127.0.0.1,端口8888)
socket->connectToHost("127.0.0.1", 8888);// 监听连接成功信号
connect(socket, &QTcpSocket::connected, this, &MyClient::onConnected);// 监听数据到达信号
connect(socket, &QTcpSocket::readyRead, this, &MyClient::onReadyRead);// 监听错误信号
connect(socket, &QTcpSocket::errorOccurred, this, &MyClient::onError);
2.2步骤 2:发送数据
void MyClient::sendData(const QByteArray &data) {if (socket->state() == QAbstractSocket::ConnectedState) {socket->write(data);socket->flush(); // 确保立即发送}
}
2.3步骤 3:接收服务器回复
void MyClient::onReadyRead() {QByteArray data = socket->readAll();qDebug() << "Server response:" << data;
}
2.4步骤 4:处理连接和错误
void MyClient::onConnected() {qDebug() << "Connected to server!";
}void MyClient::onError(QAbstractSocket::SocketError error) {qDebug() << "Error:" << socket->errorString();
}
3.关键注意事项
-
异步通信:
Qt的TCP操作基于事件循环,所有操作(连接、读写)都是异步的,需通过信号槽处理结果。 -
数据分包与粘包:
TCP是流式协议,需自行处理数据边界(例如定义协议头尾或使用长度前缀)。 -
资源管理:
及时释放断开连接的 QTcpSocket 对象(调用 deleteLater)。 -
跨线程操作:
若在多线程中使用,需将 QTcpSocket 或 QTcpServer 移至子线程(使用 moveToThread)。
4.TCP粘包问题及其处理
4.1TCP粘包是什么
TCP的粘包和拆包问题往往出现在基于TCP协议的通讯中,比如RPC框架、Netty等。
TCP在接受数据的时候,有一个滑动窗口来控制接受数据的大小,这个滑动窗口你就可以理解为一个缓冲区的大小。缓冲区满了就会把数据发送。数据包的大小是不固定的,有时候比缓冲区大有时候小。
如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;
如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。
这是最好理解的粘包问题的产生原因。还有一些其他的原因比如
1 客户端的发送频率远高于服务器的接收频率,就会导致数据在服务器的tcp接收缓冲区滞留形成粘连,比如客户端1s内连续发送了两个hello world!,服务器过了2s才接收数据,那一次性读出两个hello world!。
2 tcp底层的安全和效率机制不允许字节数特别少的小包发送频率过高,tcp会在底层累计数据长度到一定大小才一起发送,比如连续发送1字节的数据要累计到多个字节才发送,可以了解下tcp底层的Nagle算法。
3 再就是我们提到的最简单的情况,发送端缓冲区有上次未发送完的数据或者接收端的缓冲区里有未取出的数据导致数据粘连。
4.2TCP粘包为什么会产生
1.TCP会发生粘包问题:TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息;TCP协议是流式协议;所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要认为手动地去给这些协议划分边界。
粘包时:发送方每次写入数据 < 接收方套接字(Socket)缓冲区大小。
拆包时:发送方每次写入数据 > 接收方套接字(Socket)缓冲区大小。
2.UDP不会发生粘包问题:UDP具有保护消息边界,在每个UDP包中就有了消息头(UDP长度、源端口、目的端口、校验和)。
粘包拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中
4.3TCP粘包的解决方案
- 客户端在发送数据包的时候,每个包都固定长度,比如1024个字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度;
- 客户端在每个包的末尾使用固定的分隔符,例如\r\n,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的\r\n,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包;
- 将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;
- 通过自定义协议进行粘包和拆包的处理。
优缺点分析
- 解决方案1:固定数据大小
虽然这种方式可以解决粘包问题,但这种固定数据大小的传输方式,当数据量比较小时会使用空字符来填充,所以会额外的增加网络传输的负担,因此不是理想的解决方案。 - 解决方案2:特殊字符结尾
以特殊符号作为粘包的解决方案的最大优点是实现简单,但存在一定的局限性,比如当一条消息中间如果出现了结束符就会造成半包的问题,所以如果是复杂的字符串要对内容进行编码和解码处理,这样才能保证结束符的正确性。 - 解决方案4:设置消息头
此解决方案可以解决粘包问题,并且对于空间的利用也相对高 - 解决方案4:自定义请求协议
此解决方案虽然可以解决粘包问题,但消息的设计和代码的实现复杂度比较高,所以也不是理想的解决方案