解码Qt网络编程(UDP+TCP)

Qt网络模块基础

使用Qt进行UDP/TCP网络编程,需先在项目的.pro文件中引入network模块,否则无法使用网络相关类:

QT += network

该模块封装了底层操作系统的网络API,实现跨平台(Windows/Linux/macOS)的网络通信开发,无需关注不同系统的网络接口差异。

UDP编程

UDP协议核心特性

UDP(User Datagram Protocol,用户数据报协议)是无连接、不可靠、基于数据报的传输层协议:

  • 无连接:通信前无需建立连接(无三次握手),客户端可直接向服务器发送数据报;
  • 不可靠:不保证数据到达、到达顺序与发送一致,无重传机制,丢包由应用层自行处理;
  • 高效快速:协议头部仅8字节,传输开销小,延迟低;
  • 适用场景:实时音视频传输、游戏数据同步、广播/组播通信、物联网轻量数据传输等。

UDP核心编程接口:QUdpSocket

Qt通过QUdpSocket类实现UDP通信,该类兼具客户端和服务器功能,核心函数/信号如下:

类型 名称 核心作用
函数 bind() 绑定IP和端口,服务器用于监听,客户端可选绑定(不绑定则系统自动分配端口)
函数 writeDatagram() 发送UDP数据报到指定IP和端口
函数 readDatagram() 读取接收到的UDP数据报(含发送方IP/端口)
函数 hasPendingDatagrams() 判断是否有未处理的待读取数据报
函数 pendingDatagramSize() 获取下一个待读取数据报的字节数
信号 readyRead() 有数据可读取时触发(核心信号)
函数 errorString() 获取最近一次操作的错误描述(用于故障排查)

UDP开发流程

UDP服务器流程

  • 创建QUdpSocket对象;
  • 调用bind()绑定监听的IP和端口(如QHostAddress::LocalHost:8899);
  • 关联readyRead()信号到槽函数,处理接收的数据;
  • (可选)通过writeDatagram()向客户端回发数据。

UDP客户端流程

  • 创建QUdpSocket对象;
  • (可选)调用bind()绑定本地端口(不绑定则系统自动分配);
  • 调用writeDatagram()向服务器的IP和端口发送数据;
  • (可选)关联readyRead()信号,接收服务器回发的数据。

UDP完整示例

UDP服务器代码

#include <QCoreApplication>
#include <QtNetwork/QUdpSocket>
#include <QDebug>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 创建UDP套接字对象
    QUdpSocket udpServerSocket;

    /**
     * @brief bind 绑定IP和端口,使服务器监听该端口的UDP数据
     * @param address 监听的IP地址:QHostAddress::LocalHost(仅本地访问,127.0.0.1)、QHostAddress::Any(所有网卡,0.0.0.0)
     * @param port 监听的端口号(建议1024-65535,避免占用系统保留端口1-1023)
     * @param mode 绑定模式(默认QUdpSocket::DefaultForPlatform,无需手动指定)
     * @return bool 绑定成功返回true,失败返回false(如端口被占用)
     */
    bool bindResult = udpServerSocket.bind(QHostAddress::LocalHost, 8899);
    if (!bindResult) {
        qDebug() << "UDP服务器绑定失败:" << udpServerSocket.errorString();
        return -1;
    }
    qDebug() << "UDP服务器启动成功,监听 127.0.0.1:8899";

    // 关联数据接收信号:有数据到达时触发槽函数
    QObject::connect(&udpServerSocket, &QUdpSocket::readyRead, [&]() {
        // 循环读取所有待处理的数据报(避免漏读)
        while (udpServerSocket.hasPendingDatagrams()) {
            /**
             * @brief pendingDatagramSize 获取下一个数据报的字节数
             * @return qint64 数据报长度,失败返回-1
             */
            QByteArray datagram;
            datagram.resize(udpServerSocket.pendingDatagramSize()); // 调整数组大小匹配数据报

            QHostAddress senderAddr; // 输出参数:发送方IP地址
            quint16 senderPort;      // 输出参数:发送方端口号

            /**
             * @brief readDatagram 读取UDP数据报
             * @param data 存储数据的缓冲区指针
             * @param size 缓冲区大小(需≥数据报长度)
             * @param sender 输出参数:发送方IP地址
             * @param senderPort 输出参数:发送方端口号
             * @return qint64 成功读取的字节数,失败返回-1
             */
            qint64 readLen = udpServerSocket.readDatagram(
                datagram.data(), datagram.size(), &senderAddr, &senderPort
            );

            if (readLen > 0) {
                qDebug() << "收到来自" << senderAddr.toString() << ":" << senderPort
                         << "的消息:" << datagram;

                // 可选:向客户端回发确认数据
                QByteArray replyData = "服务器已接收:" + datagram;
                /**
                 * @brief writeDatagram 发送UDP数据报
                 * @param data 待发送的字节数组
                 * @param address 目标IP地址(此处为客户端IP)
                 * @param port 目标端口号(此处为客户端端口)
                 * @return qint64 成功发送的字节数,失败返回-1
                 */
                qint64 sendLen = udpServerSocket.writeDatagram(
                    replyData, senderAddr, senderPort
                );
                if (sendLen == -1) {
                    qDebug() << "回发数据失败:" << udpServerSocket.errorString();
                }
            }
        }
    });

    return a.exec(); // 启动事件循环,保持服务器运行
}

UDP客户端代码

#include <QCoreApplication>
#include <QtNetwork/QUdpSocket>
#include <QDebug>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 创建UDP套接字对象
    QUdpSocket udpClientSocket;

    // 待发送的数据和服务器地址
    QByteArray sendData = "Hello UDP Server!";
    QHostAddress serverAddr = QHostAddress::LocalHost; // 服务器IP
    quint16 serverPort = 8899;                         // 服务器端口

    /**
     * @brief writeDatagram 发送UDP数据报到指定服务器
     * @param data 待发送的字节数组
     * @param address 服务器IP地址
     * @param port 服务器端口号
     * @return qint64 成功发送的字节数,失败返回-1(如网络不可达)
     */
    qint64 sendLen = udpClientSocket.writeDatagram(
        sendData, serverAddr, serverPort
    );
    if (sendLen == -1) {
        qDebug() << "发送数据失败:" << udpClientSocket.errorString();
        return -1;
    }
    qDebug() << "成功发送" << sendLen << "字节到" << serverAddr.toString() << ":" << serverPort;

    // 接收服务器回发的数据
    QObject::connect(&udpClientSocket, &QUdpSocket::readyRead, [&]() {
        while (udpClientSocket.hasPendingDatagrams()) {
            QByteArray datagram;
            datagram.resize(udpClientSocket.pendingDatagramSize());
            QHostAddress senderAddr;
            quint16 senderPort;

            qint64 readLen = udpClientSocket.readDatagram(
                datagram.data(), datagram.size(), &senderAddr, &senderPort
            );
            if (readLen > 0) {
                qDebug() << "收到服务器回发:" << QString::fromUtf8(datagram);
            }
        }
    });

    return a.exec(); // 保持客户端运行,等待接收回发数据
}

image
image

UDP编程注意事项

  • 数据报大小:UDP单包最大约65507字节(IPv4),超过会被IP层分片,建议单包控制在1472字节(适配MTU,避免分片丢包);
  • 端口冲突:绑定端口失败时,优先检查端口是否被其他程序占用;
  • 广播/组播:UDP支持广播(QHostAddress::Broadcast)和组播,TCP不支持;
  • 无连接特性:服务器无需“接受连接”,只要绑定端口就能接收任意客户端的数据。

TCP编程

TCP协议核心特性

TCP(Transmission Control Protocol,传输控制协议)是面向连接、可靠、基于字节流的传输层协议:

  • 面向连接:通信前需三次握手建立连接,通信后四次挥手断开连接;
  • 可靠性:通过确认应答、重传机制、序号/确认号保证数据按序、完整到达;
  • 面向字节流:无数据报边界,接收方可能将多次发送的小数据合并(粘包),需应用层处理;
  • 拥塞控制:内置滑动窗口、慢启动等机制,适配网络拥塞状态;
  • 适用场景:HTTP/HTTPS、文件传输(FTP)、邮件(SMTP/POP3)、即时通讯等要求数据可靠的场景。

TCP核心编程接口

类名 核心作用 关键函数/信号
QTcpServer 服务器端核心类,用于监听客户端连接 listen():启动端口监听
newConnection():有新客户端连接时触发
nextPendingConnection():获取与新客户端通信的QTcpSocket
QTcpSocket 客户端/服务器通信类,用于数据收发 connectToHost():客户端连接服务器
write():发送数据
readAll():读取所有接收数据
readyRead():有数据可读取时触发
connected():连接成功时触发
disconnected():连接断开时触发
state():获取连接状态(如ConnectedState)
QHostAddress 表示IP地址 支持IPv4(如QHostAddress("192.168.1.1"))、IPv6,常用常量:QHostAddress::Any(0.0.0.0)、QHostAddress::LocalHost(127.0.0.1)
QNetworkInterface 获取本机网络接口信息 allAddresses():获取本机所有IP地址
allInterfaces():获取所有网络接口(含MAC、子网掩码)

TCP开发流程

TCP服务器流程

  • 创建QTcpServer对象;
  • 调用listen()绑定IP和端口,启动监听;
  • 关联newConnection()信号到槽函数,处理新客户端连接;
  • 在槽函数中调用nextPendingConnection()获取QTcpSocket对象(与客户端通信);
  • 关联QTcpSocketreadyRead()信号,处理客户端发送的数据;
  • (可选)关联disconnected()信号,处理客户端断开连接;
  • 通过QTcpSocket::write()向客户端发送数据。

TCP客户端流程

  • 创建QTcpSocket对象;
  • 调用connectToHost()连接服务器IP和端口;
  • (可选)关联connected()信号,处理连接成功事件;
  • 调用write()向服务器发送数据;
  • 关联readyRead()信号,处理服务器回发的数据;
  • (可选)关联disconnected()信号,处理连接断开;
  • 通信结束后调用disconnectFromHost()断开连接。

TCP完整示例

项目配置(.pro 文件)

QT += core gui network widgets

CONFIG += c++11

# 生成可执行文件名称
TARGET = TcpGuiDemo
TEMPLATE = app

# 源文件
SOURCES += main.cpp \
           servertcp.cpp \
           clienttcp.cpp

# 头文件
HEADERS += servertcp.h \
           clienttcp.h

# UI文件(客户端界面)
FORMS += clienttcp.ui

TCP 服务器代码(无 UI,后台运行)

servertcp.h

#ifndef SERVERTCP_H
#define SERVERTCP_H
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
#include <QAbstractSocket>
class ServerTcp : public QObject{
    Q_OBJECT
public:
    explicit ServerTcp(QObject *parent = nullptr) : QObject(parent) {
        initServer();
    }

private:
    QTcpServer m_server; // TCP服务器核心对象

    void initServer() {
        // 监听所有网卡的9999端口
        bool listenOk = m_server.listen(QHostAddress::Any, 9999);
        if (!listenOk) {
            qDebug() << "[服务器] 启动失败:" << m_server.errorString();
            return;
        }
        qDebug() << "[服务器] 启动成功,监听 0.0.0.0:9999";

        // 关联新客户端连接信号
        connect(&m_server, &QTcpServer::newConnection, this, &ServerTcp::handleNewClient);
    }

private slots:
    // 处理新客户端连接
    void handleNewClient() {
        QTcpSocket *clientSocket = m_server.nextPendingConnection();
        if (!clientSocket) {
            qDebug() << "[服务器] 获取客户端套接字失败";
            return;
        }

        // 打印客户端信息
        QString clientInfo = QString("%1:%2").arg(clientSocket->peerAddress().toString())
                                             .arg(clientSocket->peerPort());
        qDebug() << "[服务器] 新客户端连接:" << clientInfo;

        // 发送欢迎消息
        clientSocket->write(QString("欢迎连接TCP服务器![%1]").arg(clientInfo).toUtf8());

        // 关联客户端数据接收信号
        connect(clientSocket, &QTcpSocket::readyRead, this, &ServerTcp::handleClientData);

        // 关联客户端断开连接信号
        connect(clientSocket, &QTcpSocket::disconnected, this, [=]() {
            qDebug() << "[服务器] 客户端断开连接:" << clientInfo;
            clientSocket->deleteLater(); // 释放资源
        });

        // 关联错误信号(Qt 5.15+ 推荐用&QTcpSocket::errorOccurred)
        connect(clientSocket, static_cast<void (QTcpSocket::*)(QAbstractSocket::SocketError)>(&QTcpSocket::error), 
								this, [=](QAbstractSocket::SocketError err) {
            qDebug() << "[服务器] 客户端错误(" << err << "):" << clientSocket->errorString();
            clientSocket->disconnectFromHost();
            clientSocket->deleteLater();
        });
    }

    // 处理客户端发送的数据
    void handleClientData() {
        QTcpSocket *clientSocket = qobject_cast<QTcpSocket*>(sender());
        if (!clientSocket) return;

        // 读取客户端数据
        QByteArray recvData = clientSocket->readAll();
        QString recvStr = QString::fromUtf8(recvData).trimmed(); // 去除首尾空白/换行
        QString clientInfo = QString("%1:%2").arg(clientSocket->peerAddress().toString())
                                             .arg(clientSocket->peerPort());
        qDebug() << "[服务器] 收到" << clientInfo << "数据:" << recvStr;

        // 回发数据给客户端
        QByteArray replyData = QString("[服务器已接收] %1").arg(recvStr).toUtf8();
        clientSocket->write(replyData);
    }
};

#endif // SERVERTCP_H

servertcp.cpp

#include "servertcp.h"// 无需额外实现,所有逻辑在头文件的构造函数/槽函数中

主函数main.cpp

#include <QApplication>
#include "servertcp.h"
#include "clienttcp.h"
#include <QThread>
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // 启动TCP服务器(后台运行,建议放子线程,这里简化)
    ServerTcp tcpServer;

    return a.exec();
}

TCP 客户端 UI 设计(clienttcp.ui)

image

用 Qt Designer 创建界面,布局如下:

控件类型 对象名 文本 / 提示 作用
QLineEdit ipEdit 127.0.0.1 输入服务器 IP
QLineEdit portEdit 9999 输入服务器端口
QPushButton connectBtn 连接服务器 触发连接操作
QTextEdit msgInputEdit (空) 输入要发送的消息
QPushButton sendBtn 发送消息 触发发送操作
QTextEdit msgDisplayEdit (空) 显示接收 / 发送的消息
QWidget (主窗口) TCP 客户端 窗口标题

UI 结构建议:

┌─────────────────────────────────────┐
│ IP: [127.0.0.1]  端口: [9999] [连接] │
│ ┌─────────────────────────────────┐ │
│ │                                 │ │
│ │         msgDisplayEdit          │ │
│ │                                 │ │
│ └─────────────────────────────────┘ │
│ [msgInputEdit]                [发送] │
└─────────────────────────────────────┘

TCP 客户端代码

clienttcp.h

#ifndef CLIENTTCP_H
#define CLIENTTCP_H
#include <QWidget>
#include <QTcpSocket>
#include <QAbstractSocket>
#include <QDateTime>// 包含UI头文件(Qt Designer生成)
namespace Ui {
class ClientTcp;
}

class ClientTcp : public QWidget{
    Q_OBJECT

public:
    explicit ClientTcp(QWidget *parent = nullptr);
    ~ClientTcp(); // 析构函数释放UI和套接字

private slots:
    // 连接按钮点击槽函数
    void on_connectBtn_clicked();
    // 发送按钮点击槽函数
    void on_sendBtn_clicked();
    // 处理服务器连接成功
    void onConnected();
    // 处理服务器断开连接
    void onDisconnected();
    // 处理接收服务器数据
    void onReadyRead();
    // 处理套接字错误
    void onErrorOccurred(QAbstractSocket::SocketError err);

private:
    Ui::ClientTcp *ui;          // UI对象
    QTcpSocket m_socket;        // TCP客户端套接字
    // 辅助函数:添加消息到显示框
    void addMsgToDisplay(const QString &msg, bool isSend = false);
};

#endif // CLIENTTCP_H

clienttcp.cpp

#include "clienttcp.h"
#include "ui_clienttcp.h"

#include <QHostAddress>ClientTcp::ClientTcp(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::ClientTcp)
{
    ui->setupUi(this); // 初始化UI

    // 禁用发送按钮(未连接时不可用)
    ui->sendBtn->setEnabled(false);

    // 关联套接字信号
    connect(&m_socket, &QTcpSocket::connected, this, &ClientTcp::onConnected);
    connect(&m_socket, &QTcpSocket::disconnected, this, &ClientTcp::onDisconnected);
    connect(&m_socket, &QTcpSocket::readyRead, this, &ClientTcp::onReadyRead);
    connect(&m_socket, static_cast<void (QTcpSocket::*)(QAbstractSocket::SocketError)>(&QTcpSocket::error), 
				    this, &ClientTcp::onErrorOccurred);
}

ClientTcp::~ClientTcp()
{
    // 断开连接并释放UI
    if (m_socket.state() == QTcpSocket::ConnectedState) {
        m_socket.disconnectFromHost();
    }
    delete ui;
}

// 连接按钮点击
void ClientTcp::on_connectBtn_clicked()
{
    // 读取IP和端口
    QString ip = ui->ipEdit->text().trimmed();
    QString portStr = ui->portEdit->text().trimmed();
    bool portOk;
    quint16 port = portStr.toUShort(&portOk);

    // 校验输入
    if (ip.isEmpty() || !portOk || port < 1 || port > 65535) {
        addMsgToDisplay("[错误] IP或端口输入无效!");
        return;
    }

    // 如果已连接,先断开
    if (m_socket.state() == QTcpSocket::ConnectedState) {
        m_socket.disconnectFromHost();
        ui->connectBtn->setText("连接服务器");
        ui->sendBtn->setEnabled(false);
        addMsgToDisplay("[提示] 已断开与服务器的连接");
        return;
    }

    // 连接服务器(异步)
    m_socket.connectToHost(ip, port);
    addMsgToDisplay(QString("[提示] 正在连接 %1:%2...").arg(ip).arg(port));
}

// 发送按钮点击
void ClientTcp::on_sendBtn_clicked()
{
    // 读取输入框消息
    QString msg = ui->msgInputEdit->toPlainText().trimmed();
    if (msg.isEmpty()) {
        addMsgToDisplay("[错误] 发送消息不能为空!");
        return;
    }

    // 发送消息
    QByteArray sendData = msg.toUtf8();
    qint64 sendLen = m_socket.write(sendData);
    if (sendLen > 0) {
        addMsgToDisplay(msg, true); // 标记为发送的消息
        ui->msgInputEdit->clear();  // 清空输入框
    } else {
        addMsgToDisplay("[错误] 消息发送失败:" + m_socket.errorString());
    }
}

// 连接成功处理
void ClientTcp::onConnected()
{
    ui->connectBtn->setText("断开连接");
    ui->sendBtn->setEnabled(true); // 启用发送按钮
    addMsgToDisplay("[成功] 已连接到服务器:" + m_socket.peerAddress().toString() + ":" + QString::number(m_socket.peerPort()));
}

// 断开连接处理
void ClientTcp::onDisconnected()
{
    ui->connectBtn->setText("连接服务器");
    ui->sendBtn->setEnabled(false); // 禁用发送按钮
    addMsgToDisplay("[提示] 与服务器断开连接");
}

// 接收服务器数据
void ClientTcp::onReadyRead()
{
    QByteArray recvData = m_socket.readAll();
    QString recvMsg = QString::fromUtf8(recvData).trimmed();
    addMsgToDisplay(recvMsg); // 标记为接收的消息
}

// 套接字错误处理
void ClientTcp::onErrorOccurred(QAbstractSocket::SocketError err)
{
    QString errMsg;
    switch (err) {
        case QAbstractSocket::ConnectionRefusedError: errMsg = "连接被拒绝(服务器未启动/端口错误)"; break;
        case QAbstractSocket::HostNotFoundError: errMsg = "主机未找到(IP地址错误)"; break;
        case QAbstractSocket::NetworkError: errMsg = "网络错误(网络断开)"; break;
        case QAbstractSocket::SocketTimeoutError: errMsg = "连接超时"; break;
        default: errMsg = m_socket.errorString();
    }
    addMsgToDisplay("[错误] " + errMsg);
}

// 辅助函数:添加消息到显示框(带时间戳,区分发送/接收)
void ClientTcp::addMsgToDisplay(const QString &msg, bool isSend)
{
    QString time = QDateTime::currentDateTime().toString("HH:mm:ss");
    QString displayMsg;
    if (isSend) {
        displayMsg = QString("[%1] 我:%2").arg(time).arg(msg);
    } else {
        displayMsg = QString("[%1] 服务器:%2").arg(time).arg(msg);
    }
    // 追加消息到显示框(自动换行)
    ui->msgDisplayEdit->append(displayMsg);
}

主函数(main.cpp)

#include <QApplication>
#include "servertcp.h"
#include "clienttcp.h"
#include <QThread>
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    // 启动TCP客户端UI
    ClientTcp clientWindow;
    // clientWindow.setWindowTitle("TCP客户端");
    // clientWindow.resize(500, 400); // 设置窗口大小
    clientWindow.show();

    return a.exec();
}

image

TCP编程注意事项

  • 粘包问题:TCP是字节流,接收方可能将多次发送的小数据合并,需自定义协议解决(如在数据前加长度头、用换行符分隔);
  • 连接状态:发送数据前必须检查QTcpSocket::state()是否为ConnectedState,避免发送失败;
  • 资源释放:客户端断开后,需调用QTcpSocket::deleteLater()释放资源,避免内存泄漏;
  • 多客户端并发:单线程服务器仅能处理一个客户端的请求,需结合QThreadQtConcurrent实现多客户端并发;
  • 异常断开:需监听disconnected()errorOccurred()信号,处理网络异常断开的情况(如重连)。

UDP与TCP核心区别总结

特性 UDP TCP
连接性 无连接(无需握手) 面向连接(三次握手建立连接)
可靠性 不可靠(无确认、重传) 可靠(确认、重传、排序)
数据格式 数据报(有边界) 字节流(无边界)
传输速度 快(协议开销小) 慢(协议开销大,拥塞控制)
广播/组播 支持 不支持
编程复杂度 简单(无需处理连接) 复杂(处理连接、粘包等)
适用场景 实时性优先(音视频、游戏) 可靠性优先(文件、网页)
posted @ 2025-12-22 21:03  YouEmbedded  阅读(0)  评论(0)    收藏  举报