【C++ Qt】网络编程(QUdpSocket、QTcpSocket、Http)

· C++编程教程

在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核心函数接口

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(服务端)

QTcpSocket(客户端)

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请求主要用到三个类,每个类的作用分别如下。

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);
}

五、常见故障排查方案

  1. 连接失败或数据发不出去:检查项目配置文件有没有添加network模块、目标IP和端口设置是否正确、本地防火墙有没有放开对应通信端口
  2. UDP收不到数据:确认服务端已经成功绑定端口、目标地址设置无误,同时防火墙没有拦截UDP数据报文
  3. TCP数据粘包:制定简单的自定义传输规则,通过数据长度字段精准拆分和解析数据
  4. HTTP请求报错:检查网络是否通畅、请求链接格式、请求头参数设置是否符合接口要求

Qt网络编程整体学起来难度不高,核心就是熟练用好信号槽机制、做好异常情况处理,避免界面出现阻塞卡顿,文中的所有代码都能直接搬到实际项目中使用,开发者也可以根据自身业务需求做拓展和优化。