蓝牙聊天

展示通过RFCOMM协议使用蓝牙进行通信。

蓝牙聊天示例展示了如何使用Qt蓝牙 API通过蓝牙RFCOMM协议与远程设备上的另一个应用程序通信。

蓝牙聊天示例实现了多方之间的简单聊天程序。该应用程序始终作为服务器和客户端同时存在,消除确定应是哪一个连接到哪一个的需要。

运行示例

要从Qt Creator运行此示例,打开欢迎模式,然后从示例中选择示例。有关更多信息,请访问构建和运行示例

聊天服务器

聊天服务器通过ChatServer类实现。将ChatServer类声明为

class ChatServer : public QObject
{
    Q_OBJECT

public:
    explicit ChatServer(QObject *parent = nullptr);
    ~ChatServer();

    void startServer(const QBluetoothAddress &localAdapter = QBluetoothAddress());
    void stopServer();

public slots:
    void sendMessage(const QString &message);

signals:
    void messageReceived(const QString &sender, const QString &message);
    void clientConnected(const QString &name);
    void clientDisconnected(const QString &name);

private slots:
    void clientConnected();
    void clientDisconnected();
    void readSocket();

private:
    QBluetoothServer *rfcommServer = nullptr;
    QBluetoothServiceInfo serviceInfo;
    QList<QBluetoothSocket *> clientSockets;
    QMap<QBluetoothSocket *, QString> clientNames;
};

聊天服务器需要做的第一件事是创建一个QBluetoothServer实例以监听传入的蓝牙连接。每当创建新的连接时,将调用clientConnected()槽。

rfcommServer = new QBluetoothServer(QBluetoothServiceInfo::RfcommProtocol, this);
connect(rfcommServer, &QBluetoothServer::newConnection,
        this, QOverload<>::of(&ChatServer::clientConnected));
bool result = rfcommServer->listen(localAdapter);
if (!result) {
    qWarning() << "Cannot bind chat server to" << localAdapter.toString();
    return;
}

如果其他人知道它的存在,聊天服务器才有用。为了使其他设备能够发现它,需要在系统的SDP(服务发现协议)数据库中发布描述服务的记录。QBluetoothServiceInfo类封装了服务记录。

我们将发布一个包含一些服务文本描述、唯一标识服务的UUID、可发现性属性和连接参数的服务记录。

服务的文本描述存储在ServiceNameServiceDescriptionServiceProvider属性中。

serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceName, tr("Bt Chat Server"));
serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceDescription,
                         tr("Example bluetooth chat server"));
serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceProvider, tr("qt-project.org"));

蓝牙使用UUID作为唯一标识符。聊天服务使用随机生成的UUID。

static constexpr auto serviceUuid = "e8e10f95-1a70-4b27-9ccf-02010264e9c8"_L1;
serviceInfo.setServiceUuid(QBluetoothUuid(serviceUuid));

如果服务位于PublicBrowseGroup,蓝牙服务才是可发现的。

const auto groupUuid = QBluetoothUuid(QBluetoothUuid::ServiceClassUuid::PublicBrowseGroup);
QBluetoothServiceInfo::Sequence publicBrowse;
publicBrowse << QVariant::fromValue(groupUuid);
serviceInfo.setAttribute(QBluetoothServiceInfo::BrowseGroupList, publicBrowse);

ProtocolDescriptorList属性用于发布远程设备连接到我们的服务所需的连接参数。在这里,我们指定使用Rfcomm协议,并将端口号设置为我们的rfcommServer实例侦听的端口。

QBluetoothServiceInfo::Sequence protocolDescriptorList;
QBluetoothServiceInfo::Sequence protocol;
protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ProtocolUuid::L2cap));
protocolDescriptorList.append(QVariant::fromValue(protocol));
protocol.clear();
protocol << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::ProtocolUuid::Rfcomm))
         << QVariant::fromValue(quint8(rfcommServer->serverPort()));
protocolDescriptorList.append(QVariant::fromValue(protocol));
serviceInfo.setAttribute(QBluetoothServiceInfo::ProtocolDescriptorList,
                         protocolDescriptorList);

最后,我们将服务记录注册到系统中。

serviceInfo.registerService(localAdapter);

如前所述,传入的连接在clientConnected()槽中处理,其中挂起的连接连接到readyRead()和disconnected()信号。这些信号告知其他人有新客户连接。

void ChatServer::clientConnected()
{
    QBluetoothSocket *socket = rfcommServer->nextPendingConnection();
    if (!socket)
        return;

    connect(socket, &QBluetoothSocket::readyRead, this, &ChatServer::readSocket);
    connect(socket, &QBluetoothSocket::disconnected,
            this, QOverload<>::of(&ChatServer::clientDisconnected));
    clientSockets.append(socket);
    clientNames[socket] = socket->peerName();
    emit clientConnected(socket->peerName());
}

当从客户端套接字中准备好读取数据时,将调用readSocket()槽。槽从套接字中读取单个行,将它们从UTF-8转换为UTF-8,并发出messageReceived()信号。

void ChatServer::readSocket()
{
    QBluetoothSocket *socket = qobject_cast<QBluetoothSocket *>(sender());
    if (!socket)
        return;

    while (socket->canReadLine()) {
        QByteArray line = socket->readLine().trimmed();
        emit messageReceived(clientNames[socket],
                             QString::fromUtf8(line.constData(), line.length()));
    }
}

当客户端与服务断开连接时,会调用 clientDisconnected() 插槽。该插槽发出一个信号通知其他人客户端已断开,并删除套接字。

void ChatServer::clientDisconnected()
{
    QBluetoothSocket *socket = qobject_cast<QBluetoothSocket *>(sender());
    if (!socket)
        return;

    emit clientDisconnected(clientNames[socket]);

    clientSockets.removeOne(socket);
    clientNames.remove(socket);

    socket->deleteLater();
}

使用 sendMessage() 插槽向所有连接的客户端发送消息。消息转换成 UTF-8 并且在发送给所有客户端之前添加一个换行符。

void ChatServer::sendMessage(const QString &message)
{
    QByteArray text = message.toUtf8() + '\n';

    for (QBluetoothSocket *socket : std::as_const(clientSockets))
        socket->write(text);
}

停止聊天服务器时,将从系统 SDP 数据库中删除服务记录,删除所有连接的客户端套接字,并删除 rfcommServer 实例。

void ChatServer::stopServer()
{
    // Unregister service
    serviceInfo.unregisterService();

    // Close sockets
    qDeleteAll(clientSockets);
    clientNames.clear();

    // Close server
    delete rfcommServer;
    rfcommServer = nullptr;
}

服务发现

在连接到服务器之前,客户端需要扫描附近的设备并查找正在广播聊天服务的设备。这是通过 RemoteSelector 类实现的。

要开始服务查找,RemoteSelector 创建 QBluetoothServiceDiscoveryAgent 的一个实例,并连接到其信号。

    m_discoveryAgent = new QBluetoothServiceDiscoveryAgent(localAdapter);

    connect(m_discoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered,
            this, &RemoteSelector::serviceDiscovered);
    connect(m_discoveryAgent, &QBluetoothServiceDiscoveryAgent::finished,
            this, &RemoteSelector::discoveryFinished);
    connect(m_discoveryAgent, &QBluetoothServiceDiscoveryAgent::canceled,
            this, &RemoteSelector::discoveryFinished);

设置了一个 UUID 过滤器,使得服务发现只显示广播所需服务的设备。之后,启动 FullDiscovery

    m_discoveryAgent->setUuidFilter(uuid);
    m_discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);

当发现匹配的服务时,会发出 serviceDiscovered() 信号,并将一个 QBluetoothServiceInfo 的实例作为参数。这个服务信息用于提取设备名称和服务名称,并将新条目添加到已发现的远程设备列表。

    QString remoteName;
    if (serviceInfo.device().name().isEmpty())
        remoteName = address.toString();
    else
        remoteName = serviceInfo.device().name();

    QListWidgetItem *item =
        new QListWidgetItem(QString::fromLatin1("%1 %2").arg(remoteName,
                                                             serviceInfo.serviceName()));

    m_discoveredServices.insert(item, serviceInfo);
    ui->remoteDevices->addItem(item);

之后,用户可以从列表中选择一个设备并尝试连接。

聊天客户端

聊天客户端由 ChatClient 类实现。声明 ChatClient 类的代码如下:

class ChatClient : public QObject
{
    Q_OBJECT

public:
    explicit ChatClient(QObject *parent = nullptr);
    ~ChatClient();

    void startClient(const QBluetoothServiceInfo &remoteService);
    void stopClient();

public slots:
    void sendMessage(const QString &message);

signals:
    void messageReceived(const QString &sender, const QString &message);
    void connected(const QString &name);
    void disconnected();
    void socketErrorOccurred(const QString &errorString);

private slots:
    void readSocket();
    void connected();
    void onSocketErrorOccurred(QBluetoothSocket::SocketError);

private:
    QBluetoothSocket *socket = nullptr;
};

客户端创建一个新的 QBluetoothSocket,并连接到由 remoteService 参数描述的远程服务。将插槽连接到套接字的 readyRead(),connected() 和 disconnected() 信号。

void ChatClient::startClient(const QBluetoothServiceInfo &remoteService)
{
    if (socket)
        return;

    // Connect to service
    socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);
    qDebug() << "Create socket";
    socket->connectToService(remoteService);
    qDebug() << "ConnectToService done";

    connect(socket, &QBluetoothSocket::readyRead, this, &ChatClient::readSocket);
    connect(socket, &QBluetoothSocket::connected, this, QOverload<>::of(&ChatClient::connected));
    connect(socket, &QBluetoothSocket::disconnected, this, &ChatClient::disconnected);
    connect(socket, &QBluetoothSocket::errorOccurred, this, &ChatClient::onSocketErrorOccurred);
}

在套接字连接成功后,发出一个信号通知其他用户。

void ChatClient::connected()
{
    emit connected(socket->peerName());
}

与聊天服务器类似,当从套接字接收到数据时,会调用 readSocket() 插槽。逐行读取并将UTF-8 转换。发出 messageReceived() 信号。

void ChatClient::readSocket()
{
    if (!socket)
        return;

    while (socket->canReadLine()) {
        QByteArray line = socket->readLine().trimmed();
        emit messageReceived(socket->peerName(),
                             QString::fromUtf8(line.constData(), line.length()));
    }
}

使用 sendMessage() 插槽将消息发送到远程设备。消息转换为 UTF-8 并添加换行符。

void ChatClient::sendMessage(const QString &message)
{
    QByteArray text = message.toUtf8() + '\n';
    socket->write(text);
}

要从远程聊天服务断开连接,删除 QBluetoothSocket 实例。

void ChatClient::stopClient()
{
    delete socket;
    socket = nullptr;
}

聊天对话框

此示例的主窗口是聊天对话框,由 Chat 类实现。此类显示单个 ChatServer 与零个或多个 ChatClient 之间的聊天会话。声明 Chat 类的代码如下:

class Chat : public QDialog
{
    Q_OBJECT

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

signals:
    void sendMessage(const QString &message);

private slots:
    void connectClicked();
    void sendClicked();

    void showMessage(const QString &sender, const QString &message);

    void clientConnected(const QString &name);
    void clientDisconnected(const QString &name);
    void clientDisconnected();
    void connected(const QString &name);
    void reactOnSocketError(const QString &error);

    void newAdapterSelected();

    void initBluetooth();

    void updateIcons(Qt::ColorScheme scheme);

private:
    int adapterFromUserSelection() const;
    int currentAdapterIndex = 0;
    Ui::Chat *ui;

    ChatServer *server = nullptr;
    QList<ChatClient *> clients;
    QList<QBluetoothHostInfo> localAdapters;

    QString localName;
};

首先,我们构建用户界面

ui->setupUi(this);

connect(ui->connectButton, &QPushButton::clicked, this, &Chat::connectClicked);
connect(ui->sendButton, &QPushButton::clicked, this, &Chat::sendClicked);

我们创建一个 ChatServer 实例,并响应其 clientConnected()clientDiconnected()messageReceived() 信号。

server = new ChatServer(this);
connect(server, QOverload<const QString &>::of(&ChatServer::clientConnected),
        this, &Chat::clientConnected);
connect(server, QOverload<const QString &>::of(&ChatServer::clientDisconnected),
        this,  QOverload<const QString &>::of(&Chat::clientDisconnected));
connect(server, &ChatServer::messageReceived,
        this,  &Chat::showMessage);
connect(this, &Chat::sendMessage, server, &ChatServer::sendMessage);
server->startServer();

响应 ChatServerclientConnected()clientDisconnected() 信号,在聊天会话中显示典型的 "X 已加入聊天。" 和 "Y 已离开。" 消息。

void Chat::clientConnected(const QString &name)
{
    ui->chat->insertPlainText(QString::fromLatin1("%1 has joined chat.\n").arg(name));
}

void Chat::clientDisconnected(const QString &name)
{
    ui->chat->insertPlainText(QString::fromLatin1("%1 has left.\n").arg(name));
}

处理连接到 ChatServer 的客户端的传入消息是在 showMessage() 插槽中进行的。在聊天会话中显示带有远程设备名称标签的消息文本。

void Chat::showMessage(const QString &sender, const QString &message)
{
    ui->chat->moveCursor(QTextCursor::End);
    ui->chat->insertPlainText(QString::fromLatin1("%1: %2\n").arg(sender, message));
    ui->chat->ensureCursorVisible();
}

点击连接按钮后,应用程序开始服务发现,并在远程设备上展示发现的聊天服务列表。用户选择对应服务的 ChatClient

void Chat::connectClicked()
{
    ui->connectButton->setEnabled(false);

    // scan for services
    const QBluetoothAddress adapter = localAdapters.isEmpty() ?
                                           QBluetoothAddress() :
                                           localAdapters.at(currentAdapterIndex).address();

    RemoteSelector remoteSelector(adapter);
#ifdef Q_OS_ANDROID
    // QTBUG-61392
    Q_UNUSED(serviceUuid);
    remoteSelector.startDiscovery(QBluetoothUuid(reverseUuid));
#else
    remoteSelector.startDiscovery(QBluetoothUuid(serviceUuid));
#endif
    if (remoteSelector.exec() == QDialog::Accepted) {
        QBluetoothServiceInfo service = remoteSelector.service();

        qDebug() << "Connecting to service" << service.serviceName()
                 << "on" << service.device().name();

        // Create client
        ChatClient *client = new ChatClient(this);

        connect(client, &ChatClient::messageReceived,
                this, &Chat::showMessage);
        connect(client, &ChatClient::disconnected,
                this, QOverload<>::of(&Chat::clientDisconnected));
        connect(client, QOverload<const QString &>::of(&ChatClient::connected),
                this, &Chat::connected);
        connect(client, &ChatClient::socketErrorOccurred,
                this, &Chat::reactOnSocketError);
        connect(this, &Chat::sendMessage, client, &ChatClient::sendMessage);
        client->startClient(service);

        clients.append(client);
    }

    ui->connectButton->setEnabled(true);
}

响应来自 ChatClientconnected() 信号,我们在聊天会话中显示 "加入了 X 对话。"。

void Chat::connected(const QString &name)
{
    ui->chat->insertPlainText(QString::fromLatin1("Joined chat with %1.\n").arg(name));
}

通过发送 sendMessage() 信号,消息通过 ChatServerChatClient 实例发送到所有远程设备。

void Chat::sendClicked()
{
    ui->sendButton->setEnabled(false);
    ui->sendText->setEnabled(false);

    showMessage(localName, ui->sendText->text());
    emit sendMessage(ui->sendText->text());

    ui->sendText->clear();

    ui->sendText->setEnabled(true);
    ui->sendButton->setEnabled(true);
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
    // avoid keyboard automatically popping up again on mobile devices
    ui->sendButton->setFocus();
#else
    ui->sendText->setFocus();
#endif
}

当远程设备强制断开连接时,我们需要清理 ChatClient 实例。

void Chat::clientDisconnected()
{
    ChatClient *client = qobject_cast<ChatClient *>(sender());
    if (client) {
        clients.removeOne(client);
        client->deleteLater();
    }
}

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd. 本地izacion中所包含的文档贡献的版权归各自的所有者。本文件中提供的内容是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 条款许可的。Qt 和相关徽标是 The Qt Company Ltd. 在芬兰和/或其他国家的商标。所有其他商标均为其各自所有者的财产。