蓝牙聊天
展示通过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、可发现性属性和连接参数的服务记录。
服务的文本描述存储在ServiceName
、ServiceDescription
和ServiceProvider
属性中。
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();
响应 ChatServer
的 clientConnected()
和 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); }
响应来自 ChatClient
的 connected()
信号,我们在聊天会话中显示 "加入了 X 对话。"。
void Chat::connected(const QString &name) { ui->chat->insertPlainText(QString::fromLatin1("Joined chat with %1.\n").arg(name)); }
通过发送 sendMessage()
信号,消息通过 ChatServer
和 ChatClient
实例发送到所有远程设备。
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(); } }
© 2024 The Qt Company Ltd. 本地izacion中所包含的文档贡献的版权归各自的所有者。本文件中提供的内容是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 条款许可的。Qt 和相关徽标是 The Qt Company Ltd. 在芬兰和/或其他国家的商标。所有其他商标均为其各自所有者的财产。