嵌入式攻城狮_RayJie

嵌入式攻城狮_RayJie


Qt 网络模块:


一、QUdpSocket 实现 UDP 通信

1.UDP 通信概述

  UDP(用户数据报协议)是轻量的、不可靠的、面向数据报、无连接的协议,它可以用于对可靠性要求不高的场合。与 TCP 通信不同,两个程序之间进行 UDP 通信无需预先建立持久的 socket 连接,UDP 每次发送数据报都需要指定目标地址和端口。

Qt基础 | UDP通信 | UDP单播、广播、组播的介绍与实现-LMLPHP

  用于实现 UDP 通信,它从 QAbstractSocket 类继承,因而与 QTcpScoket 共享大部分的接口函数。主要区别是 QUdpSocket 以数据报传输数据,而不是以连续的数据流。QUdpSocket 类从 QAbstractSocket 类继承而来,但是又定义了较多新的功能函数用于实现 UDP 特有的一些功能,如数据报读写和多播通信功能。QUdpSocket 没有定义新的信号。

QUdpSocket 的主要功能函数

Qt基础 | UDP通信 | UDP单播、广播、组播的介绍与实现-LMLPHP

  使用函数 QUdpSocket::writeDatagram(),数据报的长度一般少于 512 个字节,每个数据报包含发送者和接受者的 IP 地址和端口等信息。

  要,要用 QUdpSocket::bind() 函数先,用于接收传入的数据报。当有数据报传入时会发射 readyRead() 信号,在其槽函数中使用 readDatagram() 函数来。

  UDP 消息传送有三种方式,

Qt基础 | UDP通信 | UDP单播、广播、组播的介绍与实现-LMLPHP

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

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

  • 组播(multicast)模式:也称为多播。UDP 客户端加入到另一个组播 IP 地址指定的多播组,成员向组播地址发送的数据报,组内成员都可以接收到,类似于 QQ 群的功能。QUdpSocket::joinMulticastGroup() 函数实现加入多播组的功能, 加入多播组后,UDP 数据的收发与正常的 UDP 数据收发方法一样。

  使用广播和多播模式,UDP可以实现一些比较灵活的通信功能,而 TCP 通信只有单播模式,没有广播和多播模式。所以,UDP 通信虽然不能保证数据传输的准确性,但是具有灵活性,一般的即时通信软件都是基于 UDP 通信的。

  在单播、广播和多播模式下,UDP 程序都是对等的(即两个实例之间可以进行 UDP 通信,这两个实例可以运行在同一台计算机上,也可以运行在不同的计算机上),不像 TCP 通信那样分为客户端和服务器端。多播和广播的实现方式基本相同,只是数据报的目标 IP 地址设置不同,多播模式需要加入多播组,实现方式有较大差异。

2.UDP 单播和广播

UDP 程序应具有以下功能

  • 可以进行 UDP 数据报的发送和接收
  • 两个实例之间可以进行 UDP 通信,这两个实例可以运行在同一台计算机上,也可以运行在不同的计算机上 。
  • ,例如实例 A 绑定端口 1200,实例 B 绑定端口 3355 。实例 A 向实例 B 发送数据报时,需要指定实例 B 所在主机的 IP 地址、绑定端口作为目标地址和目标端口,这样实例 B 才能接收到数据报。
  • ,因为 IP 地址不同了,不会导致绑定时发生冲突。一般的 UDP 通信程序都是在不同的计算机上运行的,约定一个固定的端口作为通信端口。

2.1 主窗口类定义和构造函数

  主窗口是一个基于 QMainWindow 的应用程序,界面是由 UI 设计器设计,设计如下:

Qt基础 | UDP通信 | UDP单播、广播、组播的介绍与实现-LMLPHP

  MainWindow 类的定义如下:

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include    <QUdpSocket>
#include    <QLabel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QLabel  *LabSocketState;//socket状态显示标签
    QUdpSocket  *udpSocket;//
    QString getLocalIP();//获取本机IP地址

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

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

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

私有变量:

  • udpSocket:用于 UDP 通信的 socket
  • LabSocketState:用于显示的 socket 状态标签

槽函数:

  • onSocketReadyRead() 函数

    这个槽函数与 udpSocket 的 readyRead() 信号关联,用于读取缓冲区的数据报

  • onSocketStateChange() 函数

    这个槽函数与 udpSocket 的 stateChanged() 信号关联,用于显示 udpSocket 当前的状态

构造函数

  构造函数主要完成了 udpSocket 的创建、信号与槽函数的连接。代码如下:

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);//用于与连接的客户端通讯的QTcpSocket

    connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    onSocketStateChange(udpSocket->state());

    connect(udpSocket,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}

其对应的槽函数如下:

void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{
    switch(socketState)
    {
    case QAbstractSocket::UnconnectedState:
        LabSocketState->setText("scoket状态:UnconnectedState");
        break;
    case QAbstractSocket::HostLookupState:
        LabSocketState->setText("scoket状态:HostLookupState");
        break;
    case QAbstractSocket::ConnectingState:
        LabSocketState->setText("scoket状态:ConnectingState");
        break;

    case QAbstractSocket::ConnectedState:
        LabSocketState->setText("scoket状态:ConnectedState");
        break;

    case QAbstractSocket::BoundState:
        LabSocketState->setText("scoket状态:BoundState");
        break;

    case QAbstractSocket::ClosingState:
        LabSocketState->setText("scoket状态:ClosingState");
        break;

    case QAbstractSocket::ListeningState:
        LabSocketState->setText("scoket状态:ListeningState");
    }
}

void MainWindow::onSocketReadyRead()
{//读取收到的数据报
    while(udpSocket->hasPendingDatagrams())
    {
        QByteArray   datagram;
        datagram.resize(udpSocket->pendingDatagramSize());

        QHostAddress    peerAddr;
        quint16 peerPort;
        udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
        QString str=datagram.data();

        QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";

        ui->plainTextEdit->appendPlainText(peer+str);
    }
}

2.2 UDP通信实现

  要实现 UDP 数据的接收,必须先用 QUdpSocket::bind() 函数绑定一个端口,用于监听传入的数据报,解除绑定则使用 abort() 函数。

绑定端口

  单击 “绑定端口” 按钮绑定端口后,socket 的状态变为 BoundState。

void MainWindow::on_actStart_triggered()
{//绑定端口
    quint16     port=ui->spinBindPort->value(); //本机UDP端口
    if (udpSocket->bind(port))//绑定端口成功
    {
        ui->plainTextEdit->appendPlainText("**已成功绑定");
        ui->plainTextEdit->appendPlainText("**绑定端口:"
               +QString::number(udpSocket->localPort()));

        ui->actStart->setEnabled(false);
        ui->actStop->setEnabled(true);
    }
    else
        ui->plainTextEdit->appendPlainText("**绑定失败");
}

解除绑定

  单击 “解除绑定”按钮后完成解绑,socket 的状态为 UnconnectedState。

void MainWindow::on_actStop_triggered()
{//解除绑定
    udpSocket->abort(); //解除绑定
    ui->actStart->setEnabled(true);
    ui->actStop->setEnabled(false);
    ui->plainTextEdit->appendPlainText("**已解除绑定");
}

发送消息(数据报)

  发送点对点消息和广播消息都使用 QUdpSocket::writeDatagram() 函数,使用 writeDatagram() 函数向一个目标用户发送消息时,需要指定目标 IP 地址和目标端口。只不过在广播消息时,需要将目标地址更换为一个特殊地址,即广播地址 QHostAddress::Broadcast,一般是 255.255.255.255。QUdpSocket 发送的数据报是 QByteArray 类型的字节数组,数据报的长度一般不超过 512 字节。数据报的内容可以是文本字符串,也可以自定义格式的二进制数据,文本字符串无需以换行符结束。

代码如下:

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->plainTextEdit->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->plainTextEdit->appendPlainText("[broadcast] "+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();
}

接收消息(数据报)

  QUdpSocket 接收到数据报后发射 readyRead() 信号,在关联的槽函数 onSocketReadyRead() 中调用 readDatagram() 函数来读取缓冲区的数据报。代码如下:

void MainWindow::onSocketReadyRead()
{//读取收到的数据报
    while(udpSocket->hasPendingDatagrams())
    {
        QByteArray   datagram;
        datagram.resize(udpSocket->pendingDatagramSize());

        QHostAddress    peerAddr;
        quint16 peerPort;
        udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
        QString str=datagram.data();

        QString peer="[From "+peerAddr.toString()+":"+QString::number(peerPort)+"] ";

        ui->plainTextEdit->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 变量是可选的,用于获取数据报来源的地址和端口。上面的代码中 使用了完整的参数形式,从而可以获得数据报来源的地址 peerAddr 和端口 peerPort。 如果无需获取来源地址和端口,可以采用简略形式,即:

      udpSocket->readDatagram(datagram.data(),datagram.size());
      

由于只是传输字符串,所以简单地将其转换为字符串即可。如果传输的是自定义格式的字符串或二进制数据,需要对接收到的数据进行解析。

运行结果:绑定的端口号是该实例接收数据报的端口

Qt基础 | UDP通信 | UDP单播、广播、组播的介绍与实现-LMLPHP

3.UDP 组播

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

  组播报文的目的地址使用 D 类 IP 地址,D 类地址不能出现在 IP 报文的源 IP 地址字段。用同一个 IP 多播地址接收多播数据报的所有主机构成一个组,称为多播组。所有的信息接收者都加入到一个组内,并且一旦加入之后,流向组地址的数据报立即开始向接收者传输,组中的所有成员都能接收到数据报。组中的成员是动态的,主机可以在任何时间加入和离开组。所以,采用 UDP 组播必须使用一个组播地址 。组播地址是 D 类 IP 地址,有特定的地址段。多播组可以是永久的也可以是临时的。多播组地址中,有一部分由官方分配,称为永久多播组。永久多播组保持不变的是它的 IP 地址,组中的成员构成可以发生变化。永久多播组中成员的数量可以是任意的,甚至可以为零。那些没有保留下来的供永久多播组使用的 IP 组播地址, 可以被临时多播组利用。

  关于组播 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 单播完全相同。

UDP 组播程序应具有以下功能

  • 可以发送和接收组播数据报,且在自己主机上发出的数据报,自己也可以接收到

3.1 主窗口类定义和构造函数

  主窗口是一个基于 QMainWindow 的应用程序,界面是由 UI 设计器设计,设计如下:

Qt基础 | UDP通信 | UDP单播、广播、组播的介绍与实现-LMLPHP

  主窗口类定义如下:

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include    <QMainWindow>

#include    <QUdpSocket>
#include    <QLabel>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

private:
    QLabel  *LabSocketState;
    QUdpSocket  *udpSocket;//用于与连接的客户端通讯的QTcpSocket
    QHostAddress    groupAddress;//组播地址
    QString getLocalIP();//获取本机IP地址
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
//自定义槽函数
    void    onSocketStateChange(QAbstractSocket::SocketState socketState);
    void    onSocketReadyRead();//读取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

私有变量:

  • udpSocket:用于 UDP 通信的 socket
  • LabSocketState:用于显示的 socket 状态标签
  • groupAddress:表示记录组播地址

槽函数:

  • onSocketReadyRead() 函数

    这个槽函数与 udpSocket 的 readyRead() 信号关联,用于读取缓冲区的数据报

  • onSocketStateChange() 函数

    这个槽函数与 udpSocket 的 stateChanged() 信号关联,用于显示 udpSocket 当前的状态

构造函数

  构造函数主要完成了 udpSocket 的创建、信号与槽函数的连接。代码如下:

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);//用于与连接的客户端通讯的QTcpSocket
//Multicast路由层次,1表示只在同一局域网内
    //组播TTL: 生存时间,每跨1个路由会减1,多播无法跨过大多数路由所以为1
    //默认值是1,表示数据包只能在本地的子网中传送。
    udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
//    udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,ui->spinTTL->value());

    connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    onSocketStateChange(udpSocket->state());

    connect(udpSocket,SIGNAL(readyRead()),
            this,SLOT(onSocketReadyRead()));
}
  • 使用 QUdpSocket::setSocketOption() 函数,对 socket 进行参数设置

    udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption,1);
    

    MulticastTtlOption 是 UDP 组播的数据报的生存期,数据报每跨 1 个路由会减 1。缺省值为 1 ,表示多播数据报只能在同一路由下的局域网内传播。

3.2 组播功能的程序实现

  要进行 UDP 组播通信, UDP 客户端必须先加入 UDP 多播组,也可以随时退出多播组。加入组播之前,必须,绑定端口的语句是:

udpSocket->bind(QHostAddress::AnyIPv4, groupPort, QUdpSocket::ShareAddress)

使用 joinMulticastGroup() 函数加入多播组,使用 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->plainTextEdit->appendPlainText("**加入组播成功");
        ui->plainTextEdit->appendPlainText("**组播地址IP:"+IP);
        ui->plainTextEdit->appendPlainText("**绑定端口:"+QString::number(groupPort));
        ui->actStart->setEnabled(false);
        ui->actStop->setEnabled(true);
        ui->comboIP->setEnabled(false);
    }
    else
        ui->plainTextEdit->appendPlainText("**绑定端口失败");
}

void MainWindow::on_actStop_triggered()
{//退出组播
    udpSocket->leaveMulticastGroup(groupAddress);//退出组播
    udpSocket->abort(); //解除绑定
    ui->actStart->setEnabled(true);
    ui->actStop->setEnabled(false);
    ui->comboIP->setEnabled(true);
    ui->plainTextEdit->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);
//    udpSocket->writeDatagram(datagram.data(),datagram.size(),
//                     groupAddress,groupPort);
    ui->plainTextEdit->appendPlainText("[multicst] "+msg);
    ui->editMsg->clear();
    ui->editMsg->setFocus();
}

接收消息(组播消息)

  在 readyRead() 信号的槽函数里用 readDatagram() 函数来读取数据报。

void MainWindow::onSocketReadyRead()
{//读取数据报
    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->plainTextEdit->appendPlainText(peer+str);
    }
}

07-31 20:18