天天看點

QT5.14.2 官方例子 - Qt Network 1: Network Chat Example(網絡聊天例子)一:系列總連結:二:項目位置:三:項目描述:四:官網講解:五:解析:六:邏輯了解:七:類或函數積累:八:其他積累:九:舉一反三:十:借鑒思路:十一:輔助

一:系列總連結:

QT5.14.2 官方例子 - 學習系列

https://blog.csdn.net/qq_22122811/article/details/108007519

二:項目位置:

Examples\Qt-5.14.2\network\network-chat

注:在Examples下的路徑

項目子產品:network\network-chat

2.1: 資源下載下傳:

管道1:

下載下傳qtcreator源碼,會附帶該例程;

管道2:

github下載下傳連結:

https://github.com/peterwei24/QT5.14.2_Examples/tree/master/allExamplesCode/network/network-chat

三:項目描述:

示範一個有狀态的對等聊天用戶端。

這個例子使用QUdpSocket和QNetworkInterface廣播來發現它的對等點。(換言之,區域網路的聊天室)

項目效果:

QT5.14.2 官方例子 - Qt Network 1: Network Chat Example(網絡聊天例子)一:系列總連結:二:項目位置:三:項目描述:四:官網講解:五:解析:六:邏輯了解:七:類或函數積累:八:其他積累:九:舉一反三:十:借鑒思路:十一:輔助

四:官網講解:

Network Chat Example | Qt Network 5.14.2

https://doc.qt.io/qt-5.14/qtnetwork-network-chat-example.html

五:解析:

5.1:原理分析:

利用了廣播的特性,将每一個裝置作為用戶端,分别向同一個區域網路的位址群發送資料包,同時也作為服務端,綁定指定端口和任一IP,監聽所有區域網路内的所有IP位址,并接收資料,這樣就完成了區域網路聊天室的功能;

5.2:界面布局:

QT5.14.2 官方例子 - Qt Network 1: Network Chat Example(網絡聊天例子)一:系列總連結:二:項目位置:三:項目描述:四:官網講解:五:解析:六:邏輯了解:七:類或函數積累:八:其他積累:九:舉一反三:十:借鑒思路:十一:輔助

5.3:結構設計:

如原理分析中,每個執行端都作為用戶端,同時也作為服務端,來完成區域網路聊天室的功能;

QT5.14.2 官方例子 - Qt Network 1: Network Chat Example(網絡聊天例子)一:系列總連結:二:項目位置:三:項目描述:四:官網講解:五:解析:六:邏輯了解:七:類或函數積累:八:其他積累:九:舉一反三:十:借鑒思路:十一:輔助

5.4:類解析:

ChatDialog: 界面顯示;

Client: 控制本機,并開始廣播;

PeerManager: 管理本機同時作為用戶端和服務端的工作,接收和發送;

Server: 繼承QTcpServer,如果目前有新的連接配接,則建立連接配接;

Connection: 繼承QTcpSocket,建立連接配接;

六:邏輯了解:

6.1:總體的邏輯:

通過Chatdialog建立一個對話框,在對話框中建立一個用戶端Client,這個用戶端會建立同級點之間的管理者PeerManager,來處理接收和發送的任務。

6.2:發消息給區域網路其他成員的過程:

lineEdit輸入文字,Enter鍵按下, ChatDialog調用returnPressed槽來響應:

/* 在lineEdit輸入文本,并敲Enter鍵後,執行該函數*/
void ChatDialog::returnPressed()
{
    QString text = lineEdit->text();
    if (text.isEmpty())
        return;

    if (text.startsWith(QChar('/'))) {
        QColor color = textEdit->textColor();
        textEdit->setTextColor(Qt::red);
        textEdit->append(tr("! Unknown command: %1")
                         .arg(text.left(text.indexOf(' '))));
        textEdit->setTextColor(color);
    } else {
        // 用戶端像區域網路内發送該文本
        client.sendMessage(text);
        // 追加該綽号和文本,并顯示在左邊對話框的上面
        appendMessage(myNickName, text);
    }

    lineEdit->clear();
}
           

Client執行sendMessage(): 

// 發送資訊
void Client::sendMessage(const QString &message)
{
    if (message.isEmpty())
        return;
    
    // 周遊所有的區域網路已連接配接的socket, 并逐一發送消息
    for (Connection *connection : qAsConst(peers))
        connection->sendMessage(message);
}
           

Connection(socket)執行sendMessage():

// 發送資訊
bool Connection::sendMessage(const QString &message)
{
    if (message.isEmpty())
        return false;

    // 往流中寫入資料, 對QCborStreamWriter的使用還待了解
    writer.startMap(1);
    writer.append(PlainText);
    writer.append(message);
    writer.endMap();
    return true;
}
           

6.3:接收區域網路其他成員發送的消息過程:

目前的使用者在peerManager類内建立一個socket,該socket綁定了指定的廣播端口以及任意位址,并允許位址複用和共享:

// 建立udpsocket,綁定任一位址,指定端口,設定位址可共用,可複用
    broadcastSocket.bind(QHostAddress::Any, broadcastPort, QUdpSocket::ShareAddress
                         | QUdpSocket::ReuseAddressHint);

    // socket讀到資料 => 處理廣播接收到的資料
    connect(&broadcastSocket, SIGNAL(readyRead()),
            this, SLOT(readBroadcastDatagram()));
           

如果PeerManager内的broadSocket讀取到資料,則執行readBroadcastDatagram():

// 讀取廣播包
void PeerManager::readBroadcastDatagram()
{
    // 以45000端口接收到的資料
    while (broadcastSocket.hasPendingDatagrams())
    {
        QHostAddress senderIp;
        quint16 senderPort;
        QByteArray datagram;
        datagram.resize(broadcastSocket.pendingDatagramSize());
        // 讀取資料包
        if (broadcastSocket.readDatagram(datagram.data(), datagram.size(),
                                         &senderIp, &senderPort) == -1)
            continue;

        int senderServerPort;
        {
            // decode the datagram
            QCborStreamReader reader(datagram);
            if (reader.lastError() != QCborError::NoError || !reader.isArray())
                continue;
            if (!reader.isLengthKnown() || reader.length() != 2)
                continue;

            reader.enterContainer();
            if (reader.lastError() != QCborError::NoError || !reader.isString())
                continue;
            while (reader.readString().status == QCborStreamReader::Ok) {
                // we don't actually need the username right now
            }

            if (reader.lastError() != QCborError::NoError || !reader.isUnsignedInteger())
                continue;
            senderServerPort = reader.toInteger();
        }

        // 如果發送的位址是目前本地的位址,并且發送的伺服器端口和伺服器端口一緻,
        //  則認為該發送者無效
        if (isLocalHostAddress(senderIp) && senderServerPort == serverPort)
            continue;

        // 判斷用戶端和該發送的IP位址是否有連接配接
        if (!client->hasConnection(senderIp))
        {
            // 如果沒連接配接,則将該連接配接添加到發送伺服器的端口和IP上
            Connection *connection = new Connection(this);
            emit newConnection(connection);
            connection->connectToHost(senderIp, senderServerPort);
        }
    }
}
           

這個socket在自己的類中connection,接收到資訊的并輸出顯示到左邊對話框QTextEdit内:

// readyRead():讀到輸入的資料 => processReadyRead(): 處理該資料
    QObject::connect(this, SIGNAL(readyRead()), this, SLOT(processReadyRead()));
           

處理資料:

// 處理讀取資料
void Connection::processReadyRead()
{
    // we've got more data, let's parse
    reader.reparse();
    while (reader.lastError() == QCborError::NoError)
    {
        if (state == WaitingForGreeting)
        {
            if (!reader.isArray())
                break;                  // protocol error

            reader.enterContainer();    // we'll be in this array forever
            state = ReadingGreeting;
        }
        else if (reader.containerDepth() == 1)
        {
            // Current state: no command read
            // Next state: read command ID
            if (!reader.hasNext())
            {
                reader.leaveContainer();
                disconnectFromHost();
                return;
            }

            if (!reader.isMap() || !reader.isLengthKnown() || reader.length() != 1)
                break;                  // protocol error
            reader.enterContainer();
        }
        else if (currentDataType == Undefined)
        {
            // Current state: read command ID
            // Next state: read command payload
            if (!reader.isInteger())
                break;                  // protocol error
            currentDataType = DataType(reader.toInteger());
            reader.next();
        }
        else
        {
            // Current state: read command payload
            if (reader.isString())
            {
                auto r = reader.readString();
                buffer += r.data;
                if (r.status != QCborStreamReader::EndOfString)
                    continue;
            }
            else if (reader.isNull())
            {
                reader.next();
            }
            else
            {
                break;                   // protocol error
            }

            // Next state: no command read
            reader.leaveContainer();
            if (transferTimerId != -1) {
                killTimer(transferTimerId);
                transferTimerId = -1;
            }

            if (state == ReadingGreeting)
            {
                if (currentDataType != Greeting)
                    break;              // protocol error
                processGreeting();
            }
            else
            {
                // 處理其他使用者發送的資料
                processData();
            }
        }
    }

    if (reader.lastError() != QCborError::EndOfFile)
        abort();       // parse error

    if (transferTimerId != -1 && reader.containerDepth() > 1)
        transferTimerId = startTimer(TransferTimeout);
}
           
// 處理資料
void Connection::processData()
{
    switch (currentDataType) {
    case PlainText:
        // 告知目前用戶端,有新資料
        emit newMessage(username, buffer);
        break;
    case Ping:
        writer.startMap(1);
        writer.append(Pong);
        writer.append(nullptr);     // no payload
        writer.endMap();
        break;
    case Pong:
        pongTime.restart();
        break;
    default:
        break;
    }

    currentDataType = Undefined;
    buffer.clear();
}
           
// 新使用者加入聊天室,執行該槽函數
void Client::readyForUse()
{
    Connection *connection = qobject_cast<Connection *>(sender());
    if (!connection || hasConnection(connection->peerAddress(),
                                     connection->peerPort()))
        return;

    // socket觸發信号,目前用戶端轉發該信号,并傳遞使用者資訊和資料
    connect(connection, SIGNAL(newMessage(QString,QString)),
            this, SIGNAL(newMessage(QString,QString)));

    peers.insert(connection->peerAddress(), connection);
    QString nick = connection->name();
    if (!nick.isEmpty())
        emit newParticipant(nick);
}
           

=》

ChatDialog::ChatDialog(QWidget *parent)
    : QDialog(parent)
{
    setupUi(this);

    // 設定焦點
    lineEdit->setFocusPolicy(Qt::StrongFocus);
    textEdit->setFocusPolicy(Qt::NoFocus);
    textEdit->setReadOnly(true);
    listWidget->setFocusPolicy(Qt::NoFocus);

    // 敲lineEditEnter鍵,執行returnPressed()
    connect(lineEdit, SIGNAL(returnPressed()), this, SLOT(returnPressed()));
    connect(lineEdit, SIGNAL(returnPressed()), this, SLOT(returnPressed()));

    // 用戶端有新資訊輸入newMessage(),執行appendMessage()
    connect(&client, SIGNAL(newMessage(QString,QString)),
            this, SLOT(appendMessage(QString,QString)));
    // 用戶端有新加入newParticipant(),執行newParticipant()
    connect(&client, SIGNAL(newParticipant(QString)),
            this, SLOT(newParticipant(QString)));
    //
    connect(&client, SIGNAL(participantLeft(QString)),
            this, SLOT(participantLeft(QString)));

    myNickName = client.nickName();
    newParticipant(myNickName);
    tableFormat.setBorder(0);
    QTimer::singleShot(10 * 1000, this, SLOT(showInformation()));
}
           

=》

/* 左邊的list清單新增條目時,即區域網路其他使用者加入,向其中追加資訊 */
void ChatDialog::appendMessage(const QString &from, const QString &message)
{
    if (from.isEmpty() || message.isEmpty())
        return;

    // 擷取目前文本光标的位置
    QTextCursor cursor(textEdit->textCursor());
    // 移動光标到末尾
    cursor.movePosition(QTextCursor::End);

    // 插入一行兩列的文本表格
    QTextTable *table = cursor.insertTable(1, 2, tableFormat);
    // 第一列,插入<使用者名等資訊>
    table->cellAt(0, 0).firstCursorPosition().insertText('<' + from + "> ");
    // 第二列,插入聊天資訊
    table->cellAt(0, 1).firstCursorPosition().insertText(message);
    // 擷取垂直滾動條
    QScrollBar *bar = textEdit->verticalScrollBar();
    // 設定滾動條最大值
    bar->setValue(bar->maximum());
}
           

6.4: 有新使用者加入時,接收新使用者的使用者名和端口資訊:

假設我時A使用者,B使用者登入時,會執行startBroadcasting(),告知所有區域網路的連接配接上的使用者,自己的登入資訊:

peerManager->startBroadcasting();
           

PeerManager執行開始廣播:

// 開始廣播
void PeerManager::startBroadcasting()
{
    broadcastTimer.start();
}
           

定時器隔2S告知廣播内的成員,自身的登入資訊:

// 發送廣播包
void PeerManager::sendBroadcastDatagram()
{
    QByteArray datagram;
    {
        /* QCborStreamWriter:
         * 這個類可以用來快速将CBOR内容流直接編碼到QByteArray或QIODevice。CBOR是簡潔的二進制對象表示,
         * 它是一種非常緊湊的二進制資料編碼形式,與JSON相容。它是由IETF限制的RESTful環境(CoRE)工作組
         * 建立的,該工作組在許多新的rfc中使用了它。它将與CoAP協定一起使用。*/
        QCborStreamWriter writer(&datagram);
        /*在CBOR流中啟動長度不确定的CBOR數組。每個startArray()調用必須與一個endArray()調用配對,并且
         * 目前的CBOR元素擴充到數組的末尾。
        */
        writer.startArray(2);
        writer.append(username);
        writer.append(serverPort);
        writer.endArray();
    }

    bool validBroadcastAddresses = true;
    for (const QHostAddress &address : qAsConst(broadcastAddresses)) {

        // 對擷取的廣播位址分别寫入資料
        if (broadcastSocket.writeDatagram(datagram, address,
                                          broadcastPort) == -1)
            validBroadcastAddresses = false;
    }

    if (!validBroadcastAddresses)
        updateAddresses();
}
           

七:類或函數積累:

1.QNetworkConfigurationManager說明:

QNetworkConfigurationManager提供對系統已知的網絡配置的通路,并允許應用程式在運作時檢測系統功能(關于網絡會話)。
QNetworkConfiguration抽象了一組配置選項,描述必須如何配置網絡接口以連接配接到特定的目标網絡。QNetworkConfigurationManager維護并更新QNetworkConfigurations全局清單。應用程式可以通過allConfigurations()通路和過濾這個清單。如果添加了新的配置,或者删除或更改了現有配置,則分别發出configurationAdded()、configurationRemoved()和configurationChanged()信号。
當打算立即建立一個新的網絡會話而不關心特定的配置時,可以使用defaultConfiguration()。它傳回QNetworkConfiguration::Discovered配置。如果沒有發現任何配置,則傳回無效配置。
一些配置更新可能需要一些時間來執行更新。WLAN掃描就是這樣一個例子。除非平台執行内部更新,否則可能需要通過QNetworkConfigurationManager::updateConfigurations()手動觸發配置更新。更新過程的完成通過發出updateCompleted()信号來表示。更新過程確定更新每個現有的QNetworkConfiguration執行個體。不需要通過allConfigurations()請求更新配置清單。
           

八:其他積累:

無;

九:舉一反三:

無;

十:借鑒思路:

如何利用廣播實作區域網路的多人聊天,多人通信;

十一:輔助

和Network相關的類拓撲圖:

QT5.14.2 官方例子 - Qt Network 1: Network Chat Example(網絡聊天例子)一:系列總連結:二:項目位置:三:項目描述:四:官網講解:五:解析:六:邏輯了解:七:類或函數積累:八:其他積累:九:舉一反三:十:借鑒思路:十一:輔助

繼續閱讀