UDP协议(拓展选学)

UDP协议
●UDP(用户数据报协议):无连接,提供尽力而为的包传输,适用于对实时性要求高但对数据可靠性要求相对较低的应用。

1.2 UDP协议概述
UDP是一种简单的传输层协议,具有以下特点:

●无连接:发送数据前不需要建立连接,直接发送数据包。
●不保证可靠性:数据包可能丢失、重复或失序,且不进行重传。
●低开销:由于没有连接管理和流量控制,适合需要高性能的应用。
●数据报传输:以单个数据报的形式发送,每个数据报独立处理。

应用场景:

●实时视频/音频传输
●DNS查询
●物联网设备通信

1.3 Qt网络模块中的UDP支持
Qt通过QUdpSocket类提供对UDP通信的支持,使得开发跨平台的UDP应用更加简便。

QUdpSocket类详解

QUdpSocket是Qt中专门用于UDP网络通信的类。它继承自QAbstractSocket,提供了一系列用于发送和接收UDP数据报的方法和信号。

1 QUdpSocket的基本功能

  • 发送数据:通过writeDatagram()方法发送数据报。
  • 接收数据:通过绑定套接字并监听数据到达,接收到的数据通过信号readyRead()通知。
  • 绑定端口:通过bind()方法绑定本地端口进行接收。

2 关键方法解析

  • void bind(const QHostAddress &address = QHostAddress::Any, quint16 port = 0, BindMode mode = DefaultForPlatform)
    • 绑定本地地址和端口,以便接收数据。
  • qint64 writeDatagram(const QByteArray &datagram, const QHostAddress &host, quint16 port)
    • 发送数据报到指定的主机和端口。
  • qint64 writeDatagram(const char *data, qint64 size, const QHostAddress &host, quint16 port)
    • 重载形式,发送指定大小的数据。
  • void close()
    • 关闭套接字。

3 QUdpSocket的信号

  • void readyRead()
    • 当有新的数据报到达时发出。
  • void datagramWritten(qint64 bytes)
    • 当数据报被成功发送时发出。
  • void errorOccurred(QAbstractSocket::SocketError socketError)
    • 当发生错误时发出。

4 QUdpSocket与TCP编程的对比

特性

TCP

UDP

连接性

面向连接,需建立连接

无连接,直接发送数据报

可靠性

高,保证数据按序传输,无数据丢失

低,数据包可能丢失、重复、无序

传输方式

字节流

数据报

开销

高,需管理连接和状态

低,无需连接管理

适用场景

文件传输、网页浏览、电子邮件,在线游戏等

实时视频、音频等

聊天室案例UDP版(课外拓展)

UDP应用的场景有限,大多是对数据安全性不高的场景,我们的聊天服务也可以改为UDP版本, 效果如下图

客户端实现

我们可以自定义一个UdpClient类

#ifndef UDPCLIENT_H
#define UDPCLIENT_H

#include <QObject>
#include <QUdpSocket>

class UdpClient : public QObject
{
    Q_OBJECT
public:
    static UdpClient& Inst(){
        static UdpClient udpclient;
        return udpclient;
    }

    void bindPort(QString serverip, quint16 serverport, quint16 selfport);
    void processPendingDatagrams();
    void setName(QString);
private:
    explicit UdpClient(QObject *parent = nullptr);
    UdpClient(const UdpClient& uc) = delete;
    UdpClient& operator=(const UdpClient&) = delete;
    ~UdpClient();
    //udp socket用来通信
    QUdpSocket * _udpSocket;
    //服务器地址
    QHostAddress _server_address;
    //服务器端口
    quint16 _server_port;
    //客户端用户名
    QString  _name;
signals:
    //通知MainWindow显示信息
    void sig_append_msg(QString);
public slots:
    //发送消息的槽函数
    void slot_send_msg(QString);
};

#endif // UDPCLIENT_H

具体实现

#include "udpclient.h"
#include <QUdpSocket>
#include <QTextStream>

UdpClient::UdpClient(QObject *parent) : QObject(parent),_server_address(QHostAddress("127.0.0.1")),
    _server_port(20001)
{
    _udpSocket = new QUdpSocket(this);

    //连接信息到来的信号
    connect(_udpSocket, &QUdpSocket::readyRead, this, &UdpClient::processPendingDatagrams);
}

UdpClient::~UdpClient()
{

}

void UdpClient::slot_send_msg(QString message)
{
    message = QString("[%1] : %2").arg(_name).arg(message);

    //发送消息
    _udpSocket->writeDatagram(message.toUtf8(),
                              _server_address,
                              _server_port);

    //通知界面显示
    emit sig_append_msg(message);
}

//接受数据
void UdpClient::bindPort(QString serverip, quint16 serverport, quint16 selfport)
{
    //服务地址
    _server_address = QHostAddress(serverip);
    //服务器端口
    _server_port = serverport;
    // 绑定地址和端口
    _udpSocket->bind(QHostAddress::Any,selfport,QAbstractSocket::ShareAddress);
}

void UdpClient::processPendingDatagrams()
{
    while (_udpSocket->hasPendingDatagrams()) {
        QByteArray datagram;
        //udp不需要tlv,可以直接读取包的大小
        datagram.resize(static_cast<int>(_udpSocket->pendingDatagramSize()));
        QHostAddress sender;
        quint16 senderPort;
        //读取对端发送的数据,并且获取对端的ip和端口
        _udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
        QString message = QString::fromUtf8(datagram);
        qDebug() << "收到来自" << sender.toString() << ":" << senderPort << "的数据:" << message;
        //发送消息给MainWindow
        emit sig_append_msg(message);
    }
}

void UdpClient::setName(QString name)
{
    _name = name;
}

MainWindow声明

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private:
    Ui::MainWindow *ui;

public slots:
    //客户端端口改变
    void slot_client_port_changed(int value);
    //服务器端口改变
    void slot_server_port_changed(int value);
    //客户端地址改变
    void slot_server_address_changed();
    //追加消息
    void slot_append_msg(QString msg);
private slots:
    //发送按钮点击槽函数
    void on_send_btn_clicked();

signals:
    //通知udpclient发送消息的信号
    void sig_send_msg(QString);
};

#endif // MAINWINDOW_H

MainWindow界面拷贝之前的TcpClient版本的MainWindow就行了,稍作修改,增加一个自己的端口号,一般不用修改

MainWindow具体实现

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QRandomGenerator>
#include "udpclient.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    this->setWindowTitle("客户端");
    ui->address_ed->setText("127.0.0.1");
    ui->name_ed->setText("尼采");
    ui->port_spin->setMaximum(25000);
    ui->port_spin->setValue(20001);
    ui->clientPort->setMaximum(20000);

    //设置textEdit文字大小
    QFont font = ui->chatEdit->font();
    font.setPointSize(16);
    ui->chatEdit->setFont(font);
    ui->chatEdit->setReadOnly(true);

    // 创建一个 QRandomGenerator 实例
    QRandomGenerator *generator = QRandomGenerator::global();
    int randomInRange = generator->bounded(0, 20000);
    ui->clientPort->setValue(randomInRange);

    //显示调用一下槽函数用来绑定端口
    slot_client_port_changed(randomInRange);

    UdpClient::Inst().setName(ui->name_ed->text());

    //选择int类型版本的信号
    connect(ui->clientPort, QOverload<int>::of(&QSpinBox::valueChanged),
            this, &MainWindow::slot_client_port_changed);

    //连接int类型版本的信号
    connect(ui->port_spin, QOverload<int>::of(&QSpinBox::valueChanged), this,
            &MainWindow::slot_server_port_changed);

    //连接服务修改
    connect(ui->address_ed,&QLineEdit::editingFinished,this,&MainWindow::slot_server_address_changed);

    //连接发送数据的信号
    connect(this, &MainWindow::sig_send_msg, &UdpClient::Inst(), &UdpClient::slot_send_msg);

    //名字编辑
    connect(ui->name_ed, &QLineEdit::editingFinished, this, [=](){
        UdpClient::Inst().setName(ui->name_ed->text());
    });

    //发送信号更新界面
    connect(&UdpClient::Inst(), &UdpClient::sig_append_msg,
            this, &MainWindow::slot_append_msg);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::slot_client_port_changed(int value)
{
    UdpClient::Inst().bindPort(ui->address_ed->text(),
                               static_cast<quint16>(ui->port_spin->value()),
                               static_cast<quint16>(value));
}

void MainWindow::slot_server_port_changed(int value)
{
    UdpClient::Inst().bindPort(ui->address_ed->text(),
                               static_cast<quint16>(value),
                               static_cast<quint16>(ui->clientPort->value()));
}

void MainWindow::slot_server_address_changed()
{
    UdpClient::Inst().bindPort(ui->address_ed->text(),
                               static_cast<quint16>(ui->port_spin->value()),
                               static_cast<quint16>(ui->clientPort->value()));
}

void MainWindow::on_send_btn_clicked()
{
    auto message = ui->msg_ed->text();
    emit sig_send_msg(message);
}

void MainWindow::slot_append_msg(QString msg){
    ui->chatEdit->append(msg);
}

服务器

udpserver类声明

#ifndef UDPSERVER_H
#define UDPSERVER_H

#include <QObject>
#include <QUdpSocket>
#include <QHash>
#include <QHostAddress>

class UdpServer : public QObject
{
    Q_OBJECT
public:
    static UdpServer& Inst();
    //启动服务
    void startServer(int port);
    //关闭服务
    void closeServer();
    //广播消息
    void broadCastMsg(QString key, QByteArray datagram);
private:
    explicit UdpServer(QObject *parent = nullptr);
    UdpServer(const UdpServer& ) = delete;
    UdpServer& operator=(const UdpServer&) = delete;
    QUdpSocket * _udpSocket;
    quint16 _listenPort;

    struct ClientInfo{
        QHostAddress _address;
        quint16 _port;
    };

    //ip端口字符串到具体地址的映射
    QHash<QString, ClientInfo> _ip_ports;
signals:
    //通知MainWindow显示消息
    void sig_append_msg(QString);
public slots:
    //处理udp收到的消息
    void processPendingDatagrams();
};

#endif // UDPSERVER_H

udpserver的具体实现

#include "udpserver.h"
#include <QMessageBox>

UdpServer &UdpServer::Inst()
{
    static UdpServer udpserver;
    return udpserver;
}

void UdpServer::startServer(int port)
{
    if (!_udpSocket->bind(QHostAddress::Any, static_cast<quint16>(port))) {
        QMessageBox::information(nullptr, "提示信息","绑定端口失败!");
        return;
    }

    QMessageBox::information(nullptr,"提示信息",
                             QString("实时数据UDP服务器已启动,监听端口[%1]").arg(port));
}

void UdpServer::closeServer()
{
    _udpSocket->close();
    QMessageBox::information(nullptr,"提示信息",
                             QString("实时数据UDP服务器已关闭"));
}

//广播消息
void UdpServer::broadCastMsg(QString key, QByteArray datagram)
{
    for(auto & ip_port : _ip_ports.keys()){
        //如果是消息源客户端则不发送
        if(ip_port == key){
            continue;
        }
        //获取用户信息
        auto client_info = _ip_ports[ip_port];
        //发送信息给其他用户
        _udpSocket->writeDatagram(datagram, client_info._address, client_info._port);
    }
}

UdpServer::UdpServer(QObject *parent) : QObject(parent)
{
    //创建udpsocket
    _udpSocket = new QUdpSocket(this);
    //连接读取信息的信号
    connect(_udpSocket, &QUdpSocket::readyRead, this, &UdpServer::processPendingDatagrams);
}

void UdpServer::processPendingDatagrams()
{
    //获取对端发送的消息
    while (_udpSocket->hasPendingDatagrams()) {
          QByteArray datagram;
          //重置大小
          datagram.resize(static_cast<int>(_udpSocket->pendingDatagramSize()));
          QHostAddress sender;
          quint16 senderPort;

          _udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
          QString message = QString::fromUtf8(datagram);
          qDebug() << "收到来自" << sender.toString() << ":" << senderPort << "的消息:" << message;
          //发送消息给主界面
          emit sig_append_msg(message);

          //记录来自客户端的地址和端口,拼接成串
          auto key = sender.toString()+ QString::number(senderPort);
          //客户端地址信息
          ClientInfo client_info;
          client_info._address = sender;
          client_info._port = senderPort;
          _ip_ports.insert(key, client_info);

          //广播消息
          broadCastMsg(key,datagram);
      }
}

MainWindow界面稍作修改,因为udpserve只需要绑定端口就行了

mainwindow的声明

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

private slots:
    //点击开始后按钮
    void on_startBtn_clicked();
    //点击关闭按钮
    void on_closeBtn_clicked();
    //追加消息到屏幕
    void slot_append_msg(QString);

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

MainWindow具体实现

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "udpserver.h"
#include "udpserver.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    this->setWindowTitle("服务器");
    ui->addressEd->setText("0.0.0.0");
    ui->portSpin->setMaximum(25000);
    ui->portSpin->setValue(20001);
    ui->chatEdit->setReadOnly(true);

    //设置textEdit文字大小
    QFont font = ui->chatEdit->font();
    font.setPointSize(16);
    ui->chatEdit->setFont(font);

    ui->startBtn->setEnabled(true);
    ui->closeBtn->setEnabled(false);

    connect(&UdpServer::Inst(),&UdpServer::sig_append_msg,
            this, &MainWindow::slot_append_msg);
}

void MainWindow::slot_append_msg(QString msg){
    ui->chatEdit->append(msg);
}

MainWindow::~MainWindow()
{
    delete ui;
}

//启动服务器
void MainWindow::on_startBtn_clicked()
{
    //启动服务
    UdpServer::Inst().startServer(static_cast<int>(ui->portSpin->value()));
    ui->startBtn->setEnabled(false);
    ui->closeBtn->setEnabled(true);
}

void MainWindow::on_closeBtn_clicked()
{
    //关闭服务
    UdpServer::Inst().closeServer();
    ui->startBtn->setEnabled(true);
    ui->closeBtn->setEnabled(false);
}

此时运行服务器,启动多个客户端,修改为不同的名字,将消息发送给服务器,可以看到服务器收到消息并回复给其他客户端。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐