C++ Qt開發:QUdpSocket網路通訊元件

lyshark發表於2024-03-19

Qt 是一個跨平臺C++圖形介面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以透過拖拽的方式將不同元件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹如何運用QUdpSocket元件實現基於UDP的網路通訊功能。

QTcpSocket元件功能類似,QUdpSocket元件是 Qt 中用於實現使用者資料包協議(UDP,User Datagram Protocol)通訊的類。UDP 是一種無連線的、不可靠的資料傳輸協議,它不保證資料包的順序和可靠性,但具有低延遲和簡單的特點。

以下是 QUdpSocket 類的完整函式及其簡要解釋:

函式 描述
QUdpSocket(QObject *parent = nullptr) 建構函式,建立一個新的 QUdpSocket 物件。
~QUdpSocket() 解構函式,釋放 QUdpSocket 物件及其資源。
void bind(const QHostAddress &address, quint16 port, BindMode mode = DefaultForPlatform) 將套接字繫結到指定的本地地址和埠。
void close() 關閉套接字。
bool joinMulticastGroup(const QHostAddress &groupAddress, const QNetworkInterface &iface = QNetworkInterface()) 加入多播組。
bool leaveMulticastGroup(const QHostAddress &groupAddress, const QNetworkInterface &iface = QNetworkInterface()) 離開多播組。
qint64 pendingDatagramSize() const 返回下一個待讀取的資料包的大小。
qint64 readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr) 讀取資料包。
QByteArray readDatagram(qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr) 讀取資料包,返回 QByteArray 物件。
qint64 writeDatagram(const char *data, qint64 size, const QHostAddress &address, quint16 port) 傳送資料包。
qint64 writeDatagram(const QByteArray &datagram, const QHostAddress &address, quint16 port) 傳送資料包,接受 QByteArray 物件。
QAbstractSocket::SocketState state() const 返回套接字的當前狀態。
QAbstractSocket::SocketType socketType() const 返回套接字的型別。
bool isValid() const 如果套接字有效,則返回 true;否則返回 false
int error() const 返回套接字的當前錯誤程式碼。
QHostAddress localAddress() const 返回本地地址。
quint16 localPort() const 返回本地埠。
int readBufferSize() const 返回讀取緩衝區的大小。
void setReadBufferSize(int size) 設定讀取緩衝區的大小。
QNetworkInterface multicastInterface() const 返回多播組的網路介面。
void setMulticastInterface(const QNetworkInterface &iface) 設定多播組的網路介面。
bool hasPendingDatagrams() const 如果有待讀取的資料包,則返回 true;否則返回 false
bool isReadable() const 如果套接字可讀,則返回 true;否則返回 false
bool isWritable() const 如果套接字可寫,則返回 true;否則返回 false
bool setSocketDescriptor(int socketDescriptor, QUdpSocket::SocketState socketState = ConnectedState, QIODevice::OpenMode openMode = ReadWrite) 設定套接字描述符。
int socketDescriptor() const 返回套接字描述符。
bool waitForReadyRead(int msecs = 30000) 等待套接字可讀取資料。
bool waitForBytesWritten(int msecs = 30000) 等待套接字已寫入指定位元組數的資料。
void ignoreSslErrors(const QList<QSslError> &errors) 忽略 SSL 錯誤。
void abort() 強制關閉套接字。
QNetworkProxy proxy() const 返回套接字的代理設定。
void setProxy(const QNetworkProxy &networkProxy) 設定套接字的代理設定。
QString errorString() const 返回套接字的錯誤訊息字串。

這些函式提供了在 UDP 通訊中使用 QUdpSocket 的各種功能,包括繫結、傳送和接收資料包、設定和獲取套接字的狀態等。

1.1 初始化部分

在初始化部分我們首先透過new QUdpSocket來實現建立UDP物件,QUdpSocket 建構函式的函式原型如下:

QUdpSocket::QUdpSocket(QObject * parent = nullptr)

如上建構函式建立一個新的 QUdpSocket 物件。如果提供了 parent 引數,則會將新建立的 QUdpSocket 物件新增到 parent 物件的子物件列表中,並且在 parent 物件被銷燬時自動銷燬 QUdpSocket 物件。如果沒有提供 parent 引數,則 QUdpSocket 物件將不會有父物件,並且需要手動管理其生命週期。

初始化結束後,則下一步需要呼叫bind()bind() 函式是 QUdpSocket 類的一個成員函式,用於將套接字繫結到特定的本地地址和埠。它的函式原型如下:

void QUdpSocket::bind(const QHostAddress &address, quint16 port, BindMode mode = DefaultForPlatform)
  • address:要繫結的本地地址,通常是 QHostAddress::Any,表示繫結到所有可用的網路介面。
  • port:要繫結的本地埠號。
  • mode:繫結模式,指定套接字的行為。預設值是 DefaultForPlatform,表示使用平臺預設的繫結模式。

該函式允許 QUdpSocket 在本地網路介面上監聽傳入的資料包。一旦呼叫了 bind() 函式,QUdpSocket 就可以接收來自指定地址和埠的資料包。

在呼叫 bind() 函式之後,如果成功繫結了指定的地址和埠,套接字將處於 BoundState 狀態。如果出現錯誤,可以透過檢查 error() 函式獲取錯誤程式碼,並透過 errorString() 函式獲取錯誤訊息。

接著我們透過connect()函式依次繫結套接字到stateChanged狀態改變訊號,以及readyRead()讀取訊號上,這段初始化程式碼如下所示;

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

    udpSocket=new QUdpSocket(this);

    // 生成隨機整數 包含2000 - 不包含65534
    int randomInt = QRandomGenerator::global()->bounded(2000, 65534);

    if(udpSocket->bind(randomInt))
    {
        this->setWindowTitle(this->windowTitle() + " | 地址: " + getLocalAddress() + " 繫結埠:" + QString::number(udpSocket->localPort()));
    }

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

接著切換到讀取訊號所對應的槽函式上,onSocketReadyRead是我們自定義的一個槽,該槽函式功能如下所示;

// 讀取收到的資料包
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="[訊息來自 " + peerAddr.toString()+":"+QString::number(peerPort)+"] | ";

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

首先在程式碼中呼叫pendingDatagramSize函式,pendingDatagramSize()QUdpSocket 類的一個成員函式,用於獲取下一個待讀取的資料包的大小。它的函式原型如下:

qint64 QUdpSocket::pendingDatagramSize() const

該函式返回一個 qint64 型別的值,表示下一個待讀取的資料包的大小(以位元組為單位)。如果沒有待讀取的資料包,或者發生了錯誤,該函式將返回 -1。

通常,可以在呼叫 readDatagram() 函式之前呼叫 pendingDatagramSize() 函式來獲取下一個待讀取的資料包的大小。這樣可以為資料緩衝區分配正確大小的空間,以確保完整地讀取資料包。

當有了待讀取位元組後,接著就可以直接透過呼叫readDatagram函式來從套接字中讀取資料包,readDatagram()QUdpSocket 類的一個成員函式,它有幾個過載形式,其中最常用的是:

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

該函式用於讀取資料包並將其儲存到指定的緩衝區 data 中,最多讀取 maxSize 個位元組的資料。可選引數 addressport 用於返回資料包的源地址和埠號。如果不需要這些資訊,可以將它們設定為 nullptr

函式返回實際讀取的位元組數,如果發生錯誤,返回 -1。要檢視錯誤資訊,可以使用 error()errorString() 函式。

另外,還有一個更簡單的過載形式:

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

這個過載函式直接返回一個 QByteArray 物件,其中包含了讀取的資料包。

1.2 單播與廣播訊息

單播(Unicast)和廣播(Broadcast)是網路通訊中常見的兩種資料傳輸方式,它們在資料包的傳輸範圍和目標數量上有所不同。

單播(Unicast)

單播是一種一對一的通訊方式,其中資料包從一個傳送者傳輸到一個接收者。在單播通訊中,資料包只傳送到目標主機的網路介面,並且只有目標主機能夠接收和處理這個資料包。

  • 一對一通訊:每個資料包只有一個傳送者和一個接收者。
  • 目標明確:資料包只傳送到特定的目標主機,其他主機不會接收到這個資料包。
  • 點到點通訊:適用於直接通訊的場景,如客戶端與伺服器之間的通訊。

當按鈕傳送訊息被點選後,則是一種單播模式,通常該模式需要得到目標地址與埠號,並透過呼叫writeDatagram來實現資料的傳送,該函式透過傳入三個引數,分別是傳送字串,目標地址與目標埠來實現一對一推送。

void MainWindow::on_pushButton_clicked()
{
    QHostAddress targetAddr(ui->lineEdit_addr->text());
    QString portString = ui->lineEdit_port->text();
    quint16 targetPort = portString.toUShort();

    QString msg=ui->lineEdit_msg->text();
    QByteArray str=msg.toUtf8();

    // 傳送資料包
    udpSocket->writeDatagram(str,targetAddr,targetPort);
    ui->plainTextEdit->appendPlainText("[單播訊息] | " + msg);
}

廣播(Broadcast)

廣播是一種一對多的通訊方式,其中資料包從一個傳送者傳輸到同一網路中的所有主機。在廣播通訊中,資料包被髮送到網路中的所有主機,並且所有的主機都能夠接收和處理這個資料包。

  • 一對多通訊:每個資料包有一個傳送者,但可以有多個接收者。
  • 目標不明確:資料包被髮送到網路中的所有主機,不需要知道接收者的具體地址。
  • 廣播域:在區域網中進行廣播,只有在同一廣播域內的主機才能接收到廣播訊息。
  • 網路負載:在大型網路中使用廣播可能會產生大量的網路流量,影響網路效能。

當按鈕廣播訊息被點選後,則同樣是呼叫writeDatagram函式與,唯一的區別在於第二個引數並未指定地址,而是使用了QHostAddress::Broadcast來代替,意味著只要埠是一致的則對所有的客戶推送訊息,其他保持不變。

void MainWindow::on_pushButton_2_clicked()
{
    // 廣播地址
    QString portString = ui->lineEdit_port->text();
    quint16 targetPort = portString.toUShort();

    QString  msg=ui->lineEdit_msg->text();
    QByteArray str=msg.toUtf8();
    udpSocket->writeDatagram(str,QHostAddress::Broadcast,targetPort);

    ui->plainTextEdit->appendPlainText("[廣播訊息] | " + msg);
}

讀者可自行執行兩次客戶端,此時的埠將會隨機分配,當指定對端埠後就可以向其傳送資料,如下圖所示;具體實現細節,請參考文章附件。

相關文章