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广播来发现它的对等点。(换言之,局域网的聊天室)

项目效果:

 

四:官网讲解:

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:界面布局:

5.3:结构设计:

如原理分析中,每个执行端都作为客户端,同时也作为服务端,来完成局域网聊天室的功能;

5.4:类解析:

ChatDialog: 界面显示;

Client: 控制本机,并开始广播;

PeerManager: 管理本机同时作为客户端和服务端的工作,接收和发送;

Server: 继承QTcpServer,如果当前有新的连接,则创建连接;

Connection: 继承QTcpSocket,创建连接;

 

六:逻辑理解:

6.1:总体的逻辑:

通过Chatdialog创建一个对话框,在对话框中创建一个客户端Client,这个客户端会创建同级点之间的管理者PeerManager,来处理接收和发送的任务。

 

6.2:发消息给局域网其他成员的过程:

lineEdit输入文字,回车键按下, ChatDialog调用returnPressed槽来响应:

/* 在lineEdit输入文本,并敲回车键后,执行该函数*/
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);

    // 敲lineEdit回车键,执行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相关的类拓扑图:

 

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Command line: -prefix /home/liuyh/workspace/qt5.14.2-arm -opensource -confirm-license -release -strip -shared -xplatform linux-arm-gnueabi-g++ -optimized-qmake -c++std c++11 --rpath=no -pch -skip qt3d -skip qtactiveqt -skip qtandroidextras -skip qtcanvas3d -skip qtconnectivity -skip qtdatavis3d -skip qtdoc -skip qtgamepad -skip qtlocation -skip qtmacextras -skip qtnetworkauth -skip qtpurchasing -skip qtremoteobjects -skip qtscript -skip qtscxml -skip qtsensors -skip qtspeech -skip qtsvg -skip qttools -skip qttranslations -skip qtwayland -skip qtwebengine -skip qtwebview -skip qtwinextras -skip qtx11extras -skip qtxmlpatterns -make libs -make examples -nomake tools -nomake tests -gui -widgets -dbus-runtime --glib=no --iconv=no --pcre=qt --zlib=qt -no-openssl --freetype=qt --harfbuzz=qt -no-opengl -linuxfb --xcb=no -tslib --libpng=qt --libjpeg=qt --sqlite=qt -plugin-sql-sqlite -I/opt/tslib/include -L/opt/tslib/lib -recheck-all executing config test machineTuple + arm-linux-gnueabi-g++ -dumpmachine > sh: 1: arm-linux-gnueabi-g++: not found test config.qtbase.tests.machineTuple FAILED executing config test verifyspec + cd /home/liuyh/workspace/QT5.14.2/qt-everywhere-src-5.14.2/config.tests/verifyspec && /home/liuyh/workspace/QT5.14.2/qt-everywhere-src-5.14.2/qtbase/bin/qmake "CONFIG -= qt debug_and_release app_bundle lib_bundle" "CONFIG += shared warn_off console single_arch" 'QMAKE_LIBDIR += /opt/tslib/lib' 'INCLUDEPATH += /opt/tslib/include' -early "CONFIG += cross_compile" /home/liuyh/workspace/QT5.14.2/qt-everywhere-src-5.14.2/qtbase/config.tests/verifyspec + cd /home/liuyh/workspace/QT5.14.2/qt-everywhere-src-5.14.2/config.tests/verifyspec && MAKEFLAGS= /usr/bin/make clean && MAKEFLAGS= /usr/bin/make > rm -f verifyspec.o > rm -f *~ core *.core > arm-linux-gnueabi-g++ -c -O2 -march=armv7-a -mtune=cortex-a7 -mfpu=neon -mfloat-abi=hard -O2 -march=armv7-a -mtune=cortex-a7 -mfpu=neon -mfloat-abi=hard -pipe -O2 -w -fPIC -I/home/liuyh/workspace/QT5.14.2/qt-everywhere-src-5.14.2/qtbase/config.tests/verifyspec -I. -I/opt/tslib/include -I/home/liuyh/workspace/QT5.14.2/qt-everywhere-src-5.14.2/qtbase/mkspecs/linux-arm-gnueabi-g++ -o verifyspec.o /home/liuyh/workspace/QT5.14.2/qt-everywhere-src-5.14.2/qtbase/config.tests/verifyspec/verifyspec.cpp > make:arm-linux-gnueabi-g++:命令未找到 > make: *** [Makefile:172:verifyspec.o] 错误 127
06-09
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值