Bluetoothチャット
RFCOMMプロトコルを使用したBluetooth通信の仕組みを解説します。
「Bluetooth Chat」サンプルでは、 Qt Bluetooth API を使用して、Bluetooth RFCOMM プロトコルを介してリモートデバイス上の別のアプリケーションと通信する方法を示しています。

「Bluetooth Chat」サンプルは、複数の参加者間でやり取りを行うシンプルなチャットプログラムを実装しています。このアプリケーションは常にサーバーとクライアントの両方の役割を果たすため、誰が誰に接続すべきかを決定する必要がありません。
サンプルを実行する
以下の場所からサンプルを実行できます:
- Qt Creator
Welcome モードを開き、「Examples 」からサンプルを選択してください。詳細については、Qt Creator の「チュートリアル:ビルドと実行」を参照してください。
- Qt Extension for Visual Studio Code
Command Palette から `Qt: Open Qt examples ` コマンドを実行し、リストから例を選択します。詳細については、Qt Extension for Visual Studio Code の「チュートリアル:ビルドと実行」を参照してください。```
チャットサーバー
チャットサーバーは、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;
};チャットサーバーが最初に行う必要があるのは、着信するBluetooth接続をリッスンするためにQBluetoothServer のインスタンスを作成することです。新しい接続が確立されるたびに、clientConnected() スロットが呼び出されます。
rfcommServer= newQBluetoothServer(QBluetoothServiceInfo::RfcommProtocol, this);
connect(rfcommServer, &QBluetoothServer::newConnection,
this, QOverload<>::of(&ChatServer::clientConnected));
boolresult= 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"));Bluetoothでは、UUIDが一意の識別子として使用されます。チャットサービスでは、ランダムに生成されたUUIDを使用します。
static constexpr auto serviceUuid = "e8e10f95-1a70-4b27-9ccf-02010264e9c8"_L1;
serviceInfo.setServiceUuid(QBluetoothUuid(serviceUuid));Bluetoothサービスは、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() スロットは、クライアントソケットからデータを読み取る準備が整うたびに呼び出されます。このスロットは、ソケットから1行ずつデータを読み取り、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 インスタンスも削除されます。
voidChatServer::stopServer()
{
// サービスの登録を解除
serviceInfo.unregisterService();
// ソケットを閉じる
qDeleteAll(clientSockets);
clientNames.clear();
// サーバーを終了
deleterfcommServer;
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);一致するサービスが検出されると、QBluetoothServiceInfo のインスタンスをパラメータとして持つserviceDiscovered()シグナルが発信されます。このサービス情報を使用して、デバイス名とサービス名を抽出し、検出されたリモートデバイスのリストに新しいエントリを追加します:
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()シグナルに接続されます。
voidChatClient::startClient(constQBluetoothServiceInfo&remoteService)
{
if(socket)
return;
// サービスへの接続
socket= newQBluetoothSocket(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() スロットが呼び出されます。行は1行ずつ読み込まれ、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 クラスで実装されたチャットダイアログです。このクラスは、1つのChatServer と、0個以上の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 を選択します。
voidChat::connectClicked()
{
ui->connectButton->setEnabled(false);
// サービスのスキャン
constQBluetoothAddress 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();
// クライアントを作成
ChatClient*client = newChatClient(this);
connect(client, &ChatClient::messageReceived,
this, &Chat::showMessage);
connect(client, &ChatClient::disconnected,
this, QOverload<>::of(&Chat::clientDisconnected));
connect(client, QOverload<constQString&>::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();
}
}© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.