0.前言

本文主要讲解 Qt UDP 相关接口的基本应用,一些实践相关的后面会单独写。

UDP(用户数据报协议 User Datagram Protocol)是一种轻量级,不可靠,面向数据报的无连接协议。

UDP 是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP 传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP 把每个消息段放在队列中,应用程序每次从队列中读一个消息段。由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等, 因此一台服务机可同时向多个客户机传输相同的消息。UDP 信息包的标题很短,只有 8 个字节,相对于 TCP 的 20 个字节信息包的额外开销很小。吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、 源端和终端主机性能的限制。UDP 使用尽最大努力交付,即不保证可靠交付, 因此主机不需要维持复杂的链接状态表(这里面有许多参数)。UDP 是面向报文的。发送方的 UDP 对应用程序交下来的报文, 在添加首部后就向下交付给 IP 层。既不拆分,也不合并,而是保留这些报文的边界, 因此,应用程序需要选择合适的报文大小。

UDP 知识参考:https://zhuanlan.zhihu.com/p/24860273

这篇参考的评论区也有很多可以学习的,比如:ping 命令是使用 IP 和网络控制信息协议 (ICMP),而不是 UDP。

1.准备工作

首先,要使用 Qt 的网络模块需要在 pro 中加上 network(如果是 VS IDE 就在模块选择里勾选上 network):

QT += network

引入相关类的头文件:

#include

#include

#include

Qt UDP 的操作流程:

图片参考:https://blog.csdn.net/qq_32298647/article/details/74834254

2.认识QUdpSocket的接口

QUdpSocket 是 QAbstractSocket 的子类,用于发送和接收 UDP 数据报。

可以使用 bind() 显式的绑定地址和端口。参数中的地址可以使用 QHostAddress::Any 绑定任意地址,IPv4 等效于 "0.0.0.0" , IPv6 等效于 "::";而 BindMode 一般可以设置 ShareAddress(允许其他服务绑定到相同的地址和端口) 和 DontShareAddress(不允许其他服务重新绑定),Windows 上默认等效于 ShareAddress。

bool QAbstractSocket::bind(const QHostAddress &address, quint16 port = 0, QAbstractSocket::BindMode mode = DefaultForPlatform)

bool QAbstractSocket::bind(quint16 port = 0, QAbstractSocket::BindMode mode = DefaultForPlatform)

绑定后,只要 UDP 数据报到达指定的地址和端口,就会触发 readyRead() 信号,此时可在槽函数中读取数据:

void QIODevice::readyRead()

可通过 hasPendingDatagrams() 判断是否有可读数据,通过 pendingDatagramSize() 判断数据长度。

对于数据报的读写依然是使用 read/write 相关接口:

qint64 QUdpSocket::readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr)

QNetworkDatagram QUdpSocket::receiveDatagram(qint64 maxSize = -1)

qint64 QUdpSocket::writeDatagram(const char *data, qint64 size, const QHostAddress &address, quint16 port)

qint64 QUdpSocket::writeDatagram(const QNetworkDatagram &datagram)

qint64 QUdpSocket::writeDatagram(const QByteArray &datagram, const QHostAddress &host, quint16 port)

对于单播,可以直接指定目标地址和端口发送:

const QString address_text = "127.0.0.1";

const QHostAddress address = QHostAddress(address_text);

const unsigned short port = 12345;

udpSocket->writeDatagram(QNetworkDatagram(send_data,address,port));

对于广播,需要发送到广播地址 "255.255.255.255",即使用 QHostAddress::Broadcast:

udpSocket->writeDatagram(QNetworkDatagram(send_data,QHostAddress::Broadcast,port));

对于组播,需要发送到指定的组播地址,不过要对方加入了这个组播(使用 joinMulticastGroup 和 leaveMulticastGroup 加入/退出组播):

//组播ip必须是D类ip

//D类IP段 224.0.0.0 到 239.255.255.255

//且组播地址不能是224.0.0.1

udpSocket->bind(QHostAddress::AnyIPv4,port); //根据Qt示例,组播的话IPv4和v6分开的

udpSocket->joinMulticastGroup(address); //QHostAddress("224.0.0.2")

udpSocket->writeDatagram(QNetworkDatagram(send_data,address,port)); //QHostAddress("224.0.0.2")

操作完之后,调用相关接口关闭和释放:

void QAbstractSocket::disconnectFromHost()

void QAbstractSocket::close()

void QAbstractSocket::abort()

其中, abort 调用了 close, close 调用了 disconnectFromHost。 abort 立即关闭套接字,并丢弃写缓冲区中的所有待处理数据。close 关闭套接字的 IO,以及套接字的连接。

文档:https://doc.qt.io/qt-5/qudpsocket.html

3.Qt Udp的简单示例

完整代码链接(SimpleUdpClient子项目):

https://github.com/gongjianbo/HelloQtNetwork

运行效果(可以下个网络助手调试,如果只有一台电脑,没有虚拟机,也可以用手机下个网络助手连同一个网的 Wifi 联调):

主要实现代码:

#ifndef WIDGET_H

#define WIDGET_H

#include

#include

QT_BEGIN_NAMESPACE

namespace Ui { class Widget; }

QT_END_NAMESPACE

//udp demo

class Widget : public QWidget

{

Q_OBJECT

public:

Widget(QWidget *parent = nullptr);

~Widget();

private:

//初始化client操作

void initClient();

//更新当前状态

void updateState();

private:

Ui::Widget *ui;

//socket对象

QUdpSocket *udpSocket;

};

#endif // WIDGET_H

#include "widget.h"

#include "ui_widget.h"

#include

#include

#include

#include

Widget::Widget(QWidget *parent)

: QWidget(parent)

, ui(new Ui::Widget)

{

ui->setupUi(this);

setWindowTitle("Client");

initClient();

}

Widget::~Widget()

{

//关闭套接字,并丢弃写缓冲区中的所有待处理数据。

udpSocket->abort();

delete ui;

}

void Widget::initClient()

{

//创建udp socket对象

udpSocket = new QUdpSocket(this);

//获取本机ip

QList ipAddressesList = QNetworkInterface::allAddresses();

qDebug()<<"ip list:"<

//下拉框切换

connect(ui->comboBox, QOverload::of(&QComboBox::currentIndexChanged),

[=](int index){

switch (index) {

case 0:

ui->editLocalAddress->setText(ipAddressesList.first().toString());

ui->editPeerAddress->setText(ipAddressesList.first().toString());

break;

case 1:

ui->editLocalAddress->setText("Any");

ui->editPeerAddress->setText("Broadcast");

break;

case 2:

ui->editLocalAddress->setText("224.0.0.2");

ui->editPeerAddress->setText("224.0.0.2");

break;

default:

break;

}

});

ui->editLocalAddress->setText(ipAddressesList.first().toString());

ui->editPeerAddress->setText(ipAddressesList.first().toString());

//点击绑定端口,根据ui设置进行绑定

connect(ui->btnBind,&QPushButton::clicked,[this]{

//判断当前是否已绑定,bind了就取消

if(udpSocket->state()==QAbstractSocket::BoundState){

//关闭套接字,并丢弃写缓冲区中的所有待处理数据。

udpSocket->abort();

}else if(udpSocket->state()==QAbstractSocket::UnconnectedState){

//从界面上读取ip和端口

const QHostAddress address=QHostAddress(ui->editLocalAddress->text());

const unsigned short port=ui->editLocalPort->text().toUShort();

//绑定本机地址

//combobox:单播-广播-组播

switch (ui->comboBox->currentIndex())

{

case 0:

//可以指定本地绑定的ip

udpSocket->bind(address,port);

//udpSocket->bind(port);

break;

case 1:

//udpSocket->bind(address,port);

//udpSocket->bind(port);

udpSocket->bind(QHostAddress::AnyIPv4,port);

break;

case 2:

//组播ip必须是D类ip

//D类IP段 224.0.0.0 到 239.255.255.255

//且组播地址不能是224.0.0.1

udpSocket->bind(QHostAddress::AnyIPv4,port); //根据Qt示例,组播的话IPv4和v6分开的

udpSocket->joinMulticastGroup(address); //QHostAddress("224.0.0.2")

break;

default:

break;

}

}else{

ui->textRecv->append("It is not BoundState or UnconnectedState");

}

});

//绑定状态改变

connect(udpSocket,&QUdpSocket::stateChanged,[this](QAbstractSocket::SocketState socketState){

//已绑定就设置为不可编辑

const bool is_bind=(socketState==QAbstractSocket::BoundState);

ui->btnBind->setText(is_bind?"Disbind":"Bind");

ui->editLocalAddress->setEnabled(!is_bind);

ui->editLocalPort->setEnabled(!is_bind);

ui->editPeerAddress->setEnabled(!is_bind);

ui->editPeerPort->setEnabled(!is_bind);

ui->comboBox->setEnabled(!is_bind);

updateState();

});

//发送数据

connect(ui->btnSend,&QPushButton::clicked,[this]{

//判断是可操作,isValid表示准备好读写

if(!udpSocket->isValid())

return;

//将发送区文本发送给客户端

const QByteArray send_data=ui->textSend->toPlainText().toUtf8();

//数据为空就返回

if(send_data.isEmpty())

return;

//从界面上读取ip和端口

const QString address_text=ui->editPeerAddress->text();

const QHostAddress address=QHostAddress(address_text);

const unsigned short port=ui->editPeerPort->text().toUShort();

//combobox:单播-广播-组播

switch (ui->comboBox->currentIndex())

{

case 0:

udpSocket->writeDatagram(QNetworkDatagram(send_data,address,port));

break;

case 1:

udpSocket->writeDatagram(QNetworkDatagram(send_data,QHostAddress::Broadcast,port));

break;

case 2:

udpSocket->writeDatagram(QNetworkDatagram(send_data,address,port)); //QHostAddress("224.0.0.2")

break;

default:

break;

}

});

//收到数据,触发readyRead

connect(udpSocket,&QUdpSocket::readyRead,[this]{

//没有可读的数据就返回

if(!udpSocket->hasPendingDatagrams()||

udpSocket->pendingDatagramSize()<=0)

return;

//注意收发两端文本要使用对应的编解码

QNetworkDatagram recv_datagram=udpSocket->receiveDatagram();

const QString recv_text=QString::fromUtf8(recv_datagram.data());

ui->textRecv->append(QString("[%1:%2]")

.arg(recv_datagram.senderAddress().toString())

.arg(recv_datagram.senderPort()));

ui->textRecv->append(recv_text);

});

//error信号在5.15换了名字

#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)

//错误信息

connect(udpSocket, static_cast(&QAbstractSocket::error),

[this](QAbstractSocket::SocketError){

ui->textRecv->append("Socket Error:"+udpSocket->errorString());

});

#else

//错误信息

connect(udpSocket,&QAbstractSocket::errorOccurred,[this](QAbstractSocket::SocketError){

ui->textRecv->append("Socket Error:"+udpSocket->errorString());

});

#endif

}

void Widget::updateState()

{

//将当前socket绑定的地址和端口写在标题栏

if(udpSocket->state()==QAbstractSocket::BoundState){

setWindowTitle(QString("Client[%1:%2]")

.arg(udpSocket->localAddress().toString())

.arg(udpSocket->localPort()));

}else{

setWindowTitle("Client");

}

}

2025-10-25 03:00:22