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 <QUdpSocket>
#include <QHostAddress>
#include <QNetworkDatagram>

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 <QWidget>
#include <QUdpSocket>

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 <QNetworkInterface>
#include <QHostAddress>
#include <QNetworkDatagram>
#include <QDebug>

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<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
    qDebug()<<"ip list:"<<ipAddressesList;
    //下拉框切换
    connect(ui->comboBox, QOverload<int>::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<void(QAbstractSocket::*)(QAbstractSocket::SocketError)>(&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");
    }
}

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐