블루투스 채팅
RFCOMM 프로토콜을 사용하여 블루투스를 통한 통신 방식을 보여줍니다.
블루투스 채팅 예제는 Qt Bluetooth API를 사용하여 블루투스 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;
};채팅 서버가 가장 먼저 해야 할 일은 들어오는 블루투스 연결을 수신 대기하기 위해 ` 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로 변환한 후, 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()
{
// 서비스 등록 해제
serviceInfo.unregisterService();
// 소켓 닫기
qDeleteAll(clientSockets);
clientNames.clear();
// 서버 종료
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);일치하는 서비스가 발견되면, ` 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()` 신호에 연결됩니다.
void ChatClient::startClient(const QBluetoothServiceInfo&remoteService)
{
if (socket)
return;
// 서비스에 연결
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 와 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 를 선택합니다.
void Chat::connectClicked()
{
ui->connectButton->setEnabled(false);
// 서비스 스캔
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();
// 클라이언트 생성
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();
}
}© 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.