블루투스 채팅

RFCOMM 프로토콜을 사용하여 블루투스를 통한 통신을 표시합니다.

Bluetooth 채팅 예제는 Qt Bluetooth API를 사용하여 원격 장치의 다른 애플리케이션과 블루투스 RFCOMM 프로토콜을 사용하여 통신하는 방법을 보여줍니다.

Bluetooth 채팅 예제는 여러 당사자 간의 간단한 채팅 프로그램을 구현합니다. 애플리케이션은 항상 서버와 클라이언트 역할을 모두 수행하므로 누가 누구와 연결해야 하는지 결정할 필요가 없습니다.

예제 실행하기

에서 예제를 실행하려면 Qt Creator에서 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 예제 빌드 및 실행하기를 참조하세요.

채팅 서버

채팅 서버는 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);

앞서 언급했듯이 들어오는 연결은 readyRead() 및 disconnected() 신호에 연결되는 보류 중인 연결이 있는 clientConnected() 슬롯에서 처리됩니다. 이 신호는 새 클라이언트가 연결되었음을 다른 사람들에게 알립니다.

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(); // 서버 닫기 삭제 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";
    소켓-> 연결 서비스(원격 서비스);    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();

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); // 서비스 검색 const QBluetoothAddress adapter = localAdapters.isEmpty() ?QBluetoothAddress() : localAdapters.at(currentAdapterIndex).address(); RemoteSelector remoteSelector(adapter);#ifdef Q_OS_ANDROID // QTBUG-61392Q_UNUSED(serviceUuid); remoteSelector.startDiscovery(QBluetoothUuid(reverseUuid));#elseremoteSelector.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); }

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

© 2025 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.