在Qt客户端程序开发的过程中,网络通信是必不可少的核心技术模块,不管是做局域网内的设备数据交互、客户端与服务端的双向数据传输,还是调用外部网页接口获取远程数据,都离不开TCP、UDP、HTTP这三种主流的网络通信协议。
Qt框架自带的Network模块,对底层Socket通信、网页网络请求都做了全面封装,开发者不用费心处理Windows和Linux系统下原生Socket接口的复杂调用步骤,也不用手动解决线程卡住、数据粘包这类常见问题,整体学起来简单、上手也更快。
一、前期准备:项目配置
Qt框架里所有和网络通信相关的类,都归属于Qt Network模块,项目开发调用这些功能之前,必须先做好模块配置,否则会出现头文件找不到、程序链接报错这类编译运行的问题。
1.1 qmake项目配置
如果是用qmake构建的项目,需要打开项目后缀为.pro的配置文件,添加network模块的依赖项,具体的配置语句如下所示。
QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
1.2 核心依赖头文件
#include <QUdpSocket> // UDP通信
#include <QTcpSocket> // TCP客户端
#include <QTcpServer> // TCP服务端
#include <QNetworkAccessManager> // HTTP请求管理
#include <QNetworkRequest> // HTTP请求封装
#include <QNetworkReply> // HTTP响应封装
#include <QHostAddress>
#include <QByteArray>
#include <QDebug>
二、QUdpSocket:UDP无连接通信
UDP属于无连接、传输不稳定、面向数据报文的传输层通信协议,最大的优势是传输速度快、占用资源少,适合用在对数据实时性要求高、能接受少量数据丢失的场景,像是局域网消息推送、音视频传输、设备心跳包发送等场景都很适用。
Qt框架通过QUdpSocket这个类实现UDP数据的收发操作,这种通信方式不用提前搭建连接通道,绑定好端口后就能直接收发数据报文,同时还支持单播、广播、组播多种数据发送形式。
2.1 UDP核心函数接口
- bind():完成本地IP地址与端口号绑定,用于监听并接收外部数据
- writeDatagram():向外发送UDP格式数据报文
- readyRead():内置信号,当套接字接收缓冲区有新数据到达时自动触发
- receiveDatagram():读取完整UDP数据报文,同时可获取发送方IP与端口信息
- hasPendingDatagrams():判断当前是否存在未读取的UDP数据报文
2.2 UDP客户端与服务端实现
UDP通信并没有严格的客户端和服务端区分,本文会实现一个简单的UDP回显测试程序,由服务端绑定指定端口监听数据,客户端主动发送消息,服务端接收后再将数据原路返回,以此完成基础的通信验证。
UDP服务端代码实现
// widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QUdpSocket>
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
private slots:
void slotRecvData(); // 接收数据槽函数
private:
QUdpSocket* m_udpSocket;
const quint16 m_port = 8888; // 监听端口
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
m_udpSocket = new QUdpSocket(this);
// 绑定端口,监听所有网卡
bool bindOk = m_udpSocket->bind(QHostAddress::Any, m_port);
if(bindOk) {
qDebug() << "UDP服务端绑定端口成功,端口:" << m_port;
} else {
qDebug() << "绑定失败:" << m_udpSocket->errorString();
}
// 连接信号槽,有数据则接收
connect(m_udpSocket, &QUdpSocket::readyRead, this, &Widget::slotRecvData);
}
// 接收并回显数据
void Widget::slotRecvData()
{
// 循环读取所有数据报
while(m_udpSocket->hasPendingDatagrams()) {
QNetworkDatagram datagram = m_udpSocket->receiveDatagram();
QByteArray recvData = datagram.data();
QHostAddress senderIp = datagram.senderAddress();
quint16 senderPort = datagram.senderPort();
qDebug() << "收到来自:" << senderIp.toString() << ":" << senderPort << "消息:" << recvData;
// 回显给客户端
m_udpSocket->writeDatagram(datagram);
}
}
UDP客户端代码实现
// 客户端只需创建socket,无需绑定,直接发送数据
void Widget::slotSendData()
{
QUdpSocket* clientUdp = new QUdpSocket(this);
QString sendMsg = "Hello UDP Server";
QHostAddress serverIp("127.0.0.1");
quint16 serverPort = 8888;
// 发送数据报
qint64 len = clientUdp->writeDatagram(sendMsg.toUtf8(), serverIp, serverPort);
if(len > 0) {
qDebug() << "UDP消息发送成功";
}
}
注意事项:想要实现UDP广播通信,只需要把目标主机IP改成QHostAddress::Broadcast,就能给局域网内所有联网设备推送消息,客户端不用手动绑定端口,系统会自动分配临时端口完成数据传输。
三、QTcpSocket:TCP可靠连接通信
TCP是面向连接、传输稳定、数据有序、基于字节流的传输层通信协议,通过三次握手建立通信连接、四次挥手断开连接,自带数据重发、顺序整理、流量管控等保障功能,适合用在文件传输、身份验证、指令发送这类不能丢失数据的场景。
Qt框架下的TCP通信分为两大核心部分,分别是负责监听外部连接的QTcpServer服务端类,以及负责搭建连接、传输数据的QTcpSocket客户端类,两者配合就能完成整套TCP通信流程。
3.1 TCP核心函数接口
QTcpServer(服务端)
- listen():监听本地指定IP与端口,等待客户端发起连接请求
- newConnection():内置信号,检测到新客户端接入时自动触发
- nextPendingConnection():获取新客户端对应的通信套接字对象
QTcpSocket(客户端)
- connectToHost():主动向目标服务端发起连接请求
- write():向服务端发送通信数据
- readyRead():内置信号,接收到服务端返回数据时触发
- connected()/disconnected():分别是连接建立成功、连接断开两大内置信号
- errorOccurred():通信过程出现异常错误时触发的报错信号
3.2 TCP服务端代码实现
// widget.h
#include <QTcpServer>
#include <QTcpSocket>
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = nullptr);
private slots:
void slotNewClient(); // 新客户端连接
void slotRecvTcpData(); // 接收数据
void slotClientDisconnect(); // 客户端断开
private:
QTcpServer* m_tcpServer;
QTcpSocket* m_tcpSocket;
};
// widget.cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
m_tcpServer = new QTcpServer(this);
// 监听本地8889端口
if(m_tcpServer->listen(QHostAddress::Any, 8889)) {
qDebug() << "TCP服务端监听成功";
}
// 新客户端连接
connect(m_tcpServer, &QTcpServer::newConnection, this, &Widget::slotNewClient);
}
// 处理新客户端
void Widget::slotNewClient()
{
// 获取客户端socket
m_tcpSocket = m_tcpServer->nextPendingConnection();
qDebug() << "新客户端接入:" << m_tcpSocket->peerAddress().toString();
// 数据接收、断开连接信号绑定
connect(m_tcpSocket, &QTcpSocket::readyRead, this, &Widget::slotRecvTcpData);
connect(m_tcpSocket, &QTcpSocket::disconnected, this, &Widget::slotClientDisconnect);
}
// 接收TCP数据
void Widget::slotRecvTcpData()
{
QByteArray recvData = m_tcpSocket->readAll();
qDebug() << "TCP接收数据:" << recvData;
// 回显数据
m_tcpSocket->write(recvData);
}
// 客户端断开
void Widget::slotClientDisconnect()
{
qDebug() << "客户端断开连接";
m_tcpSocket->deleteLater();
}
3.3 TCP客户端代码实现
// 连接服务端
void Widget::slotConnectServer()
{
m_tcpSocket = new QTcpSocket(this);
// 连接本地服务端
m_tcpSocket->connectToHost(QHostAddress("127.0.0.1"), 8889);
// 连接成功信号
connect(m_tcpSocket, &QTcpSocket::connected, [](){
qDebug() << "TCP客户端连接服务端成功";
});
// 接收数据
connect(m_tcpSocket, &QTcpSocket::readyRead, [=](){
QByteArray recv = m_tcpSocket->readAll();
qDebug() << "客户端收到:" << recv;
});
}
// 发送数据
void Widget::slotSendTcpMsg()
{
if(m_tcpSocket->state() == QAbstractSocket::ConnectedState) {
m_tcpSocket->write("Hello TCP Server");
}
}
开发要点:
TCP属于字节流传输方式,多次连续发送小体积数据很容易出现数据粘在一起的问题,实际项目开发时,建议通过添加数据长度头部、自定义分隔符的方式制定简单传输规则,完成数据拆分和解析,GUI主线程千万不能调用waitForXXX这类阻塞接口,避免出现界面卡死的情况。
四、Qt HTTP请求:接口调用实战
日常开发里,调用远程网页接口、抓取网页内容、上传表单参数这类上层需求,用HTTP协议实现会更简单省事,Qt框架自带一套简易的HTTP请求接口,开发者不用手动处理底层TCP连接的细节,全程采用异步通信方式,不会让界面出现卡顿卡住的问题。
HTTP请求主要用到三个类,每个类的作用分别如下。
- QNetworkAccessManager:网络请求管理工具,建议只创建一个实例,统一负责各类网络请求的发送和调度
- QNetworkRequest:请求参数打包类,用来设置请求地址、请求头、超时时间等基础信息
- QNetworkReply:响应结果接收类,通过内置信号获取服务端返回的数据和状态信息
4.1 HTTP GET请求实现
// widget.h
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
private slots:
void slotHttpGetFinished(QNetworkReply* reply);
private:
QNetworkAccessManager* m_httpManager;
// widget.cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
m_httpManager = new QNetworkAccessManager(this);
// 请求完成信号绑定
connect(m_httpManager, &QNetworkAccessManager::finished, this, &Widget::slotHttpGetFinished);
}
// 发送GET请求
void Widget::slotSendHttpGet()
{
QUrl url("https://www.baidu.com");
QNetworkRequest request(url);
// 可添加请求头
request.setHeader(QNetworkRequest::UserAgentHeader, "Qt HttpClient");
// 发送GET请求
m_httpManager->get(request);
}
// 请求完成处理
void Widget::slotHttpGetFinished(QNetworkReply *reply)
{
if(reply->error() == QNetworkReply::NoError) {
// 读取响应数据
QByteArray result = reply->readAll();
qDebug() << "HTTP请求成功:" << result;
} else {
qDebug() << "请求失败:" << reply->errorString();
}
reply->deleteLater();
}
4.2 HTTP POST请求(表单/JSON格式)
// 发送POST JSON请求
void Widget::slotSendHttpPost()
{
QUrl url("http://xxx.xxx.xxx/api/test");
QNetworkRequest request(url);
// 设置请求头为JSON
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json;charset=utf-8");
// 构造JSON参数
QJsonObject obj;
obj.insert("username", "test");
obj.insert("password", "123456");
QJsonDocument doc(obj);
QByteArray postData = doc.toJson(QJsonDocument::Compact);
// 发送POST请求
m_httpManager->post(request, postData);
}
五、常见故障排查方案
- 连接失败或数据发不出去:检查项目配置文件有没有添加network模块、目标IP和端口设置是否正确、本地防火墙有没有放开对应通信端口
- UDP收不到数据:确认服务端已经成功绑定端口、目标地址设置无误,同时防火墙没有拦截UDP数据报文
- TCP数据粘包:制定简单的自定义传输规则,通过数据长度字段精准拆分和解析数据
- HTTP请求报错:检查网络是否通畅、请求链接格式、请求头参数设置是否符合接口要求
Qt网络编程整体学起来难度不高,核心就是熟练用好信号槽机制、做好异常情况处理,避免界面出现阻塞卡顿,文中的所有代码都能直接搬到实际项目中使用,开发者也可以根据自身业务需求做拓展和优化。