UDP简介

UDP用户数据报协议(User Datagram Protocol,UDP)是轻量的、不可靠的、面向数据报(datagram) 的、无连接的协议,它可以用于对可靠性要求不高的场合(如实时网络视频通话)。UDP 通信不区分客户端和服务器端,UDP 程序都是客户端程序。

 两个 UDP 客户端之间进行 UDP通信时,无须预先建立持久的 socket 连接,UDP 客 户端每次发送数据报时指定目标地址和端口即可。

相关的类

QUdpSocket 类用于实现 UDP 通信,它与 QTcpSocket 具有相同的父类 QAbstractSocket,因而 这两个类共享大部分的接口函数。主要区别是 QUdpSocket 以数据报传输数据,而不是以连续的数据流传输数据。QUdpSocket::writeDatagram()函数用于发送数据报,数据报一般少于 512 字节,每个数据报包含发送者和接收者的 IP 地址和端口等信息。

UDP接收数据时,要先用 QUdpSocket::bind()函数绑定一个端口,用于接收传入的数据报。当有数据报传入时 QUdpSocket 会发射 readyRead()信号,使用 QUdpSocket::readDatagram()函数可以读取接收到的数据报。

UDP消息传送有三种模式:单播、广播、组播模式,各个模式示意如下:

• 单播(unicast):一个 UDP 客户端发出的数据报只发送到一个指定地址和端口的 UDP 客户端,是一对一的数据传输。

• 广播(broadcast):一个 UDP 客户端发出的数据报,在同一网络范围内所有的 UDP 客户端都可以收到。QUdpSocket 支持 IPv4 广播,广播经常用于实现网络发现的协议。要广播数据,只需在数据报中指定接收端地址为 QHostAddress::Broadcast,一般的广播地址是 255.255.255.255。

• 组播(multicast):也称为多播。UDP 客户端加入一个由组播 IP 地址指定的多播组(用同 一个组播 IP 地址接收组播数据报的所有主机构成一个组,称为多播组或组播组),成员向组播地址发送的数据报组内成员都可以接收到,类似于 QQ 群的功能。

QUdpSocket::joinMulticastGroup()函数可实现加入多播组的功能,加入多播组后,UDP 数据的收发与正常的 UDP 数据的收发方法一样。

TCP 通信只有单播模式, 没有广播和组播模式。UDP 通信虽然不能保证数据传输的准确性,但是它具有灵活性,一般的即时通信软件都是基于 UDP 通信的。

QUdpSocket 类扩展了一些函数用于支持UDP特有的一些功能,如数据报读写和组播通信功能。在单播、广播和组播模式下,UDP 程序都是对等的。组播和广播的实现方式基本相同,只是数据报的目标 IP 地址设置不同。组播模式需要加入多播组,实现方式有较大差异。

UDP单播和广播

可以打开两个UDP实例,在同一台计算机上运行时,两个运行实例需要绑定不同的端口,如果两个实例在不同计算机上运行,则可以绑定相同的端口,因为 IP 地址不同了,不会导致绑定时发生冲突。一般的 UDP 通信程序都是在不同计算机上运行的,约定一个固定的端口作为通信端口。

主窗口头文件

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include    <QUdpSocket>
#include    <QLabel>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QLabel  *labSocketState;    //状态栏上的标签
    QUdpSocket  *udpSocket;
    QString getLocalIP();       //获取本机IP地址

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    //自定义槽函数
    void    do_socketStateChange(QAbstractSocket::SocketState socketState);
    void    do_socketReadyRead();  //读取socket传入的数据  接收消息报文
    void on_actStart_triggered();  //绑定本机端口
    void on_actStop_triggered();   //解除绑定
    void on_actClear_triggered();  //清空文本框
    void on_btnSend_clicked();     //单播发送消息报
    void on_btnBroadcast_clicked(); //广播发送消息报文

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

在构造函数中获取本机IP与TCP通信一致(往期博客),然后创建了udpSocket用于进行UDP通信,并将其stateChanged()信号与自定义槽函数do_socketStateChange() 关联,将其 readyRead()信号与自定义槽函数 do_socketReadyRead()关联。

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    labSocketState=new QLabel("Socket状态:");
    labSocketState->setMinimumWidth(200);
    ui->statusBar->addWidget(labSocketState);
    QString localIP=getLocalIP();   //本机IP
    this->setWindowTitle(this->windowTitle()+"----本机IP地址:"+localIP);
    ui->comboTargetIP->addItem(localIP);
    udpSocket=new QUdpSocket(this);   //创建socket
    connect(udpSocket,&QUdpSocket::stateChanged,this,&MainWindow::do_socketStateChange);
    do_socketStateChange(udpSocket->state());  //执行一次,显示当前状态
    connect(udpSocket,SIGNAL(readyRead()), this,SLOT(do_socketReadyRead()));
}

UDP通信实现

绑定端口

使用bind()绑定本机的一个端口,用于监听该端口,其他UDP实例可以往该端口发送内容,当有数据时候udpSocket会发送readyRead()信号

void MainWindow::on_actStart_triggered()
{//"绑定端口"按钮
    quint16 port=ui->spinBindPort->value();  //本机UDP端口
    if (udpSocket->bind(port))   //绑定端口成功
    {
        ui->textEdit->appendPlainText("**已成功绑定");
        ui->textEdit->appendPlainText("**绑定端口:"
                        +QString::number(udpSocket->localPort()));
        ui->actStart->setEnabled(false);
        ui->actStop->setEnabled(true);
        ui->btnSend->setEnabled(true);
        ui->btnBroadcast->setEnabled(true);
    }
    else
        ui->textEdit->appendPlainText("**绑定失败");
}

解除绑定:使用abort()函数

void MainWindow::on_actStop_triggered()
{//"解除绑定"按钮
    udpSocket->abort();       //解除绑定,复位socket
    ui->actStart->setEnabled(true);
    ui->actStop->setEnabled(false);
    ui->btnSend->setEnabled(false);
    ui->btnBroadcast->setEnabled(false);
    ui->textEdit->appendPlainText("**已解除绑定");
}

绑定端口后,socket 的状态变为 QAbstractSocket::BoundState(已绑定),解除绑定后状态变为 QAbstractSocket::UnconnectedState(未连接)

消息发送

单播和广播都使用QUdpSocket::writeDatagram()函数发送消息。

void MainWindow::on_btnSend_clicked()
{//"发送消息" 按钮
    QString     targetIP=ui->comboTargetIP->currentText();  //目标IP
    QHostAddress  targetAddr(targetIP);
    quint16     targetPort=ui->spinTargetPort->value();     //目标port
    QString  msg=ui->editMsg->text();       //发送的消息内容

    QByteArray  str=msg.toUtf8();
    udpSocket->writeDatagram(str,targetAddr,targetPort); //发出数据报
    ui->textEdit->appendPlainText("[out] "+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();
}

void MainWindow::on_btnBroadcast_clicked()
{ //"广播消息" 按钮
    quint16     targetPort=ui->spinTargetPort->value();   //目标端口
    QString  msg=ui->editMsg->text();
    QByteArray  str=msg.toUtf8();
    udpSocket->writeDatagram(str,QHostAddress::Broadcast,targetPort);

    ui->textEdit->appendPlainText("[broadcast] "+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();
}

函数 writeDatagram()原型定义如下:

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

其中,datagram 是要发出的数据报,host 是目标主机,port 是目标端口。

对于单播,指定一个IP地址即可,对于广播设置成常量QHostAddress::Broadcast(这个地址一般是 255.255.255.255)。

注意UDP数据报发送的是QByteArray 类型的字节数据数组,数据报一般不超过 512 字节。数据报的内容可以是字符串,也可以是自定义格式的二进制数据,字符串无须以换行符结束(TCP需要换行结束)。

消息接收

QUdpSocket 接收到数据报后发射 readyRead()信号,可以设置对应的槽函数使用readDatagram()函数接收消息。

void MainWindow::do_socketReadyRead()
{//读取收到的数据报
    while(udpSocket->hasPendingDatagrams()) //有待读取的数据报
    {
        QByteArray   datagram;   //获取数据报的字节数
        datagram.resize(udpSocket->pendingDatagramSize());

        QHostAddress    peerAddr;
        quint16 peerPort;   //IP地址和端口是可选参数,可用于读取指定主机端口的数据
        udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
        QString str=datagram.data();

        QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";
        ui->textEdit->appendPlainText(peer+str);
    }
}

槽函数中,通过hasPendingDatagrams()函数判断是否有待读取的数据报。函数 pendingDatagramSize() 返回待读取数据报的字节数。

用于读取数据报的函数 readDatagram()原型如下:

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

参数 data 和 maxSize 是必需的,表示最多读取 maxSize 字节的数据到缓冲区 data 里。address 和 port 变量是可选的,用于获取数据报来源的地址和端口。

如果接收的是纯文本字符串,可以将获取到的QByteArray数据转为QString类型。如果传输的是自定义格式的字符串或二进制数据,需要对接收到的数据进行解析。

UDP组播

UDP 组播是主机之间“一对一组”的通信模式,当多个客户端加入由一个组播地址定义的多播组之后,客户端向组播地址和端口发送的 UDP 数据报,组内成员都可以接收到,其功能类似于 QQ 群。组内的成员是动态变化的,主机可以在任何时刻加入和离开组。

使用UDP组播必须使用一个组播地址。组播地址是 D 类 IP 地址,有特定的地址段。

• 224.0.0.0~224.0.0.255 为预留的组播地址(永久组地址),地址 224.0.0.0 保留不分配,其 他地址供路由协议使用。

• 224.0.1.0~224.0.1.255 是公用组播地址,可以用于 Internet。

• 224.0.2.0~238.255.255.255 为用户可用的组播地址(临时组地址),全网范围内有效。

• 239.0.0.0~239.255.255.255 为本地管理组播地址,仅在特定的本地范围内有效。 因此,若是在家庭或办公室局域网内测试 UDP 组播功能,可以使用的组播地址范围是:

239.0.0.0~239.255.255.255。

QUdpSocket 支持 UDP 组播,joinMulticastGroup()函数使主机加入多播组,leaveMulticastGroup()函数使主机离开多播组。UDP 组播的特点是使用组播地址,其他的端口绑定、数据报收发等功能 的实现与 UDP 单播的完全相同。

组播实示例程序设计

在局域网的两台计算机上(192.168.1.101和192.168.1.102)分别运行程序,将两个主机上的程序都加入地址为 239.255.43.21 的多播组,绑定端口 35320进行通信。

主窗口头文件

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include    <QMainWindow>

#include    <QUdpSocket>
#include    <QLabel>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QLabel  *labSocketState;
    QUdpSocket  *udpSocket;         //socket连接
    QHostAddress    groupAddress;   //组播地址
    QString getLocalIP();           //获取本机IP地址
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    //自定义槽函数
    void    do_socketStateChange(QAbstractSocket::SocketState socketState);
    void    do_socketReadyRead();//读取socket传入的数据
    void on_actStart_triggered();
    void on_actStop_triggered();
    void on_actClear_triggered();
    void on_actHostInfo_triggered();
    void on_btnMulticast_clicked();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

相比前面的例子,多了一个QHostAddress 类型变量 groupAddress,用于记录组播地址。

在主窗口构造函数中,使用函数setSocketOption()对 udpSocket 进行参数设置,该函数原型定义如下:

void QAbstractSocket::setSocketOption(QAbstractSocket::SocketOption option, const QVariant &value)

参数 option 是要设置的选项名称,属于枚举类型 QAbstractSocket::SocketOption;参数 value 是 要设置的选项的值。

代码中将udpSocket的QAbstractSocket::MulticastTtlOption选项的值设置为1。MulticastTtlOption 是 UDP 组播的数据报的生存期,数据报每跨一个路由该值会减 1。MulticastTtlOption 的默认值为 1, 表示组播数据报只能在同一路由下的局域网内传播。

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    labSocketState=new QLabel("Socket状态:");//
    labSocketState->setMinimumWidth(200);
    ui->statusBar->addWidget(labSocketState);

    QString localIP=getLocalIP();   //本地主机名
    this->setWindowTitle(this->windowTitle()+"----本机IP地址:"+localIP);

    udpSocket=new QUdpSocket(this);
    //Multicast路由层次,1表示只在同一局域网内
    //组播TTL: 生存时间,每跨1个路由会减1,多播无法跨过大多数路由所以为1
    //默认值是1,表示数据包只能在本地的子网中传送。
    udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
    //    udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,ui->spinTTL->value());
    connect(udpSocket,&QUdpSocket::stateChanged,this,&MainWindow::do_socketStateChange);
    do_socketStateChange(udpSocket->state());   //立即刷新一次
    connect(udpSocket,SIGNAL(readyRead()),this,SLOT(do_socketReadyRead()));
}

加入组播与退出组播

加入组播通过bind()函数,原型定义如下:

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

参数 addr 是枚举类型 QHostAddress::SpecialAddress,表示一些特殊的主机地址,程序中设置为 QHostAddress::AnyIPv4,表示任何 IPv4 地址。参数 port 是要绑定的端口,程序中设置为多播组统一的一个端口 groupPort。参数 mode 表示绑定模式,程序中设置为QUdpSocket::ShareAddress, 表示允许其他服务使用这个地址和端口,组播模式时参数 mode 必须设置为这个值。(而在UDP单播和广播中,bind函数只传递了目标IP地址)

绑定好组播地址后,需要使用 QUdpSocket:: joinMulticastGroup()函数将本机地址加入多播组中,退出组播地址使用QUdpSocket::leaveMulticastGroup()函数。

void MainWindow::on_actStart_triggered()
{//"加入组播"按钮
    QString   IP=ui->comboIP->currentText();
    groupAddress=QHostAddress(IP);      //多播组地址
    quint16   groupPort=ui->spinPort->value();    //端口
    if (udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress))
    {
        udpSocket->joinMulticastGroup(groupAddress); //加入多播组
        ui->textEdit->appendPlainText("**加入组播成功");
        ui->textEdit->appendPlainText("**组播地址IP:"+IP);
        ui->textEdit->appendPlainText("**绑定端口:"+QString::number(groupPort));
        ui->actStart->setEnabled(false);
        ui->actStop->setEnabled(true);
        ui->comboIP->setEnabled(false);
        ui->spinPort->setEnabled(false);
        ui->btnMulticast->setEnabled(true);
    }
    else
        ui->textEdit->appendPlainText("**绑定端口失败");
}

void MainWindow::on_actStop_triggered()
{//"退出组播"按钮
    udpSocket->leaveMulticastGroup(groupAddress);   //退出组播
    udpSocket->abort();     //解除绑定
    ui->actStart->setEnabled(true);
    ui->actStop->setEnabled(false);
    ui->comboIP->setEnabled(true);
    ui->spinPort->setEnabled(true);
    ui->btnMulticast->setEnabled(false);
    ui->textEdit->appendPlainText("**已退出组播,解除端口绑定");
}

组播消息

发送组播数据报也使用函数 writeDatagram(),只是目标地址使用的是组播地 址。

void MainWindow::on_btnMulticast_clicked()
{//"组播消息"按钮, 发送组播消息
    quint16  groupPort=ui->spinPort->value();  //组播端口
    QString  msg=ui->editMsg->text();
    QByteArray  datagram=msg.toUtf8();

    udpSocket->writeDatagram(datagram, groupAddress, groupPort);
    ui->textEdit->appendPlainText("[multicast] "+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();
}

接收消息部分和UDP 单播时接收数据的代码一致

void MainWindow::do_socketReadyRead()
{//读取数据报
    while(udpSocket->hasPendingDatagrams())
    {
        QByteArray   datagram;
        datagram.resize(udpSocket->pendingDatagramSize());
        QHostAddress    peerAddr;
        quint16 peerPort;
        udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
        //        udpSocket->readDatagram(datagram.data(),datagram.size());
        QString str=datagram.data();

        QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";
        ui->textEdit->appendPlainText(peer+str);
    }
}

参考

QT6 C++ 开发指南

作者:杨德杰

物联沃分享整理
物联沃-IOTWORD物联网 » QT网络(三):UDP通信

发表回复