蓝牙聊天

Shows communication through Bluetooth using RFCOMM protocol.

蓝牙聊天范例展示如何使用 Qt Bluetooth API to communicate with another application on a remote device using Bluetooth RFCOMM protocol.

The Bluetooth Chat example implements a simple chat program between multiple parties. The application always acts as both a server and a client eliminating the need to determine who should connect to whom.

运行范例

要运行范例从 Qt Creator ,打开 欢迎 模式,然后选择范例从 范例 。更多信息,拜访 构建和运行范例 .

聊天服务器

The chat server is implemented by the ChatServer 类。 ChatServer class is declared as:

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;
};
					

The first thing the chat server needs to do is create an instance of QBluetoothServer to listen for incoming Bluetooth connections. The clientConnected() slot will be called whenever a new connection is created.

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;
}
					

The chat server is only useful if others know that it is there. To enable other devices to discover it, a record describing the service needs to be published in the system's SDP (Service Discovery Protocol) database. The QBluetoothServiceInfo class encapsulates a service record.

We will publish a service record that contains some textual descriptions of the services, a UUID that uniquely identifies the service, the discoverability attribute, and connection parameters.

The textual description of the service is stored in the 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"));
					

Bluetooth uses UUIDs as unique identifiers. The chat service uses a randomly generated UUID.

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

A Bluetooth service is only discoverable if it is in the PublicBrowseGroup .

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

The ProtocolDescriptorList attribute is used to publish the connection parameters that the remote device requires to connect to our service. Here we specify that the Rfcomm protocol is used and set the port number to the port that our rfcommServer instance is listening to.

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);
					

Finally, we register the service record with the system.

serviceInfo.registerService(localAdapter);
					

As mentioned earlier, incoming connections are handled in the clientConnected() slot where pending connections are connected to the readyRead () 和 disconnected () signals. The signals notify others that a new client has connected.

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());
}
					

The readSocket() slot is called whenever data is ready to be read from a client socket. The slot reads individual lines from the socket, converts them from UTF-8, and emits the 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()));
    }
}
					

The clientDisconnected() slot is called whenever a client disconnects from the service. The slot emits a signal to notify others that a client has disconnected, and deletes the socket.

void ChatServer::clientDisconnected()
{
    QBluetoothSocket *socket = qobject_cast<QBluetoothSocket *>(sender());
    if (!socket)
        return;
    emit clientDisconnected(clientNames[socket]);
    clientSockets.removeOne(socket);
    clientNames.remove(socket);
    socket->deleteLater();
}
					

The sendMessage() slot is used to send a message to all connected clients. The message is converted into UTF-8 and appended with a newline before being sent to all clients.

void ChatServer::sendMessage(const QString &message)
{
    QByteArray text = message.toUtf8() + '\n';
    for (QBluetoothSocket *socket : std::as_const(clientSockets))
        socket->write(text);
}
					

When the chat server is stopped, the service record is removed from the system SDP database, all connected client sockets are deleted, and the rfcommServer instance is deleted.

void ChatServer::stopServer()
{
    // Unregister service
    serviceInfo.unregisterService();
    // Close sockets
    qDeleteAll(clientSockets);
    clientNames.clear();
    // Close server
    delete rfcommServer;
    rfcommServer = nullptr;
}
					

Service Discovery

Before connecting to the server, the client needs to scan the nearby devices and search for the device that is advertising the chat service. This is done by the RemoteSelector 类。

To start service lookup, the RemoteSelector 创建实例为 QBluetoothServiceDiscoveryAgent and connects to its signals.

    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);
					

An UUID filter is set, so that the service discovery only shows the devices that advertise the needed service. After that a FullDiscovery is started:

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

When a matching service is discovered, a serviceDiscovered () signal is emitted with an instance of QBluetoothServiceInfo as a parameter. This service info is used to extract the device name and the service name, and add a new entry to the list of discovered remote devices:

    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);
					

Later the user can select one of the devices from the list and try to connect to it.

聊天客户端

The chat client is implemented by the ChatClient 类。 ChatClient class is declared as:

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;
};
					

The client creates a new QBluetoothSocket and connects to the remote service described by the remoteService parameter. Slots are connected to the socket's 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);
}
					

On successful socket connection we emit a signal to notify other users.

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

Similarly to the chat server, the readSocket() slot is called when data is available from the socket. Lines are read individually and converted from UTF-8. The 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()));
    }
}
					

The sendMessage() slot is used to send a message to the remote device. The message is converted to UTF-8 and a newline is appended.

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

To disconnect from the remote chat service, the QBluetoothSocket instance is deleted.

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

聊天对话框

The main window of this example is the chat dialog, implemented in the 聊天 class. This class displays a chat session between a single ChatServer and zero or more ChatClient s. The 聊天 class is declared as:

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;
};
					

First we construct the user interface

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

We create an instance of the ChatServer and respond to its clientConnected() , clientDiconnected() ,和 messageReceived() signals.

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();
					

In response to the clientConnected() and clientDisconnected() signals of the ChatServer , we display the typical "X has joined chat." and "Y has left." messages in the chat session.

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));
}
					

Incoming messages from clients connected to the ChatServer are handled in the showMessage() slot. The message text tagged with the remote device name is displayed in the chat session.

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();
}
					

In response to the connect button being clicked, the application starts service discovery and presents a list of discovered chat services on remote devices. A ChatClient for the service is selected by the user.

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);
}
					

In reponse to the connected() signals from ChatClient , we display the "Joined chat with X." message in the chat session.

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

Messages are sent to all remote devices via the ChatServer and ChatClient instances by emitting the sendMessage() 信号。

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
}
					

We need to clean up ChatClient instances when the remote device forces a disconnect.

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

范例工程 @ code.qt.io