Fortune 클라이언트

네트워크 서비스용 클라이언트를 만드는 방법을 보여줍니다.

이 예제는 QTcpSocket 를 사용하며, Fortune 서버 예제나 스레드 기반 Fortune 서버 예제와 함께 실행하도록 설계되었습니다.

Fortune Client 예제의 스크린샷

이 예제는 간단한 QDataStream 기반 데이터 전송 프로토콜을 사용하여 ( Fortune Server 예제에 포함된) 포춘 서버로부터 한 줄의 텍스트를 요청합니다. 클라이언트는 단순히 서버에 연결하는 방식으로 포춘 문구를 요청합니다. 그러면 서버는 포춘 문구가 포함된 QString 객체를 응답으로 반환합니다.

QTcpSocket 네트워크 프로그래밍에 대한 두 가지 일반적인 접근 방식을 지원합니다:

  • 비동기(비차단) 방식. 작업은 Qt의 이벤트 루프로 제어권이 돌아올 때 스케줄링되어 수행됩니다. 작업이 완료되면 ` QTcpSocket `가 신호를 발생시킵니다. 예를 들어, ` QTcpSocket::connectToHost()`는 즉시 반환되며, 연결이 설정되면 ` QTcpSocket `가 ` connected()` 신호를 발생시킵니다.
  • 동기식(블로킹) 방식. 비 GUI 및 멀티스레드 애플리케이션에서는 신호에 연결하는 대신 waitFor...() 함수(예: QTcpSocket::waitForConnected())를 호출하여 작업이 완료될 때까지 호출 스레드를 일시 중지할 수 있습니다.

이 예제에서는 비동기식 접근 방식을 시연할 것입니다. ‘Blocking Fortune Client’ 예제는 동기식 접근 방식을 보여줍니다.

이 클래스에는 일부 데이터와 몇 가지 비공개 슬롯이 포함되어 있습니다.

class Client : public QDialog
{
    Q_OBJECT

public:
    explicit Client(QWidget *parent = nullptr);

private slots:
    void requestNewFortune();
    void readFortune();
    void displayError(QAbstractSocket::SocketError socketError);
    void enableGetFortuneButton();

private:
    QComboBox *hostCombo = nullptr;
    QLineEdit *portLineEdit = nullptr;
    QLabel *statusLabel = nullptr;
    QPushButton *getFortuneButton = nullptr;

    QTcpSocket *tcpSocket = nullptr;
    QDataStream in;
    QString currentFortune;
};

GUI를 구성하는 위젯 외에도, 데이터 멤버에는 QTcpSocket 포인터, 소켓을 조작하는 QDataStream 객체, 그리고 현재 표시된 운세 텍스트의 복사본이 포함됩니다.

소켓은 Client 생성자에서 초기화됩니다. 소켓을 삭제하는 것에 대해 신경 쓸 필요가 없도록 메인 위젯을 부모로 전달하겠습니다:

Client::Client(QWidget *parent)
    : QDialog(parent)
    , hostCombo(new QComboBox)
    , portLineEdit(new QLineEdit)
    , getFortuneButton(new QPushButton(tr("Get Fortune")))
    , tcpSocket(new QTcpSocket(this))
{
    ...
    in.setDevice(tcpSocket);
    in.setVersion(QDataStream::Qt_6_5);

이 프로토콜은 QDataStream 을 기반으로 하므로, 스트림 디바이스를 새로 생성된 소켓으로 설정합니다. 그런 다음 클라이언트와 서버가 어떤 버전의 Qt를 사용하든 상관없이 포춘 서버와 동일한 버전을 사용하도록 보장하기 위해 스트림의 프로토콜 버전을 명시적으로 QDataStream::Qt_6_5 로 설정합니다.

이 예제에서 필요한 QTcpSocket 신호는 데이터 수신을 나타내는 QTcpSocket::readyRead()와, 연결 오류를 감지하는 데 사용할 QTcpSocket::errorOccurred()뿐입니다:

    ...
    connect(tcpSocket, &QIODevice::readyRead, this, &Client::readFortune);
    connect(tcpSocket, &QAbstractSocket::errorOccurred,
    ...
}

Get Fortune 버튼을 클릭하면 requestNewFortune() 슬롯이 호출됩니다:

void Client::requestNewFortune()
{
    getFortuneButton->setEnabled(false);
    tcpSocket->abort();
    tcpSocket->connectToHost(hostCombo->currentText(),
                             portLineEdit->text().toInt());
}

사용자가 이전 연결이 완전히 종료되기 전에 ‘ Get Fortune ’ 버튼을 클릭할 수 있도록 허용하기 때문에, 먼저 QTcpSocket::abort()을 호출하여 이전 연결을 중단합니다. (연결되지 않은 소켓에서는 이 함수가 아무런 작업도 수행하지 않습니다.) 그런 다음 사용자 인터페이스에서 입력한 호스트명과 포트를 인수로 전달하여 QTcpSocket::connectToHost()를 호출함으로써 포춘 서버에 연결을 진행합니다.

connectToHost()를 호출하면 다음 두 가지 중 하나가 발생합니다:

  • 연결이 성공합니다. 이 경우 서버에서 fortune을 보내옵니다. QTcpSocket 는 데이터 블록을 수신할 때마다 readyRead()를 반환합니다.
  • 오류가 발생합니다. 연결에 실패했거나 연결이 끊어진 경우 사용자에게 이를 알려야 합니다. 이 경우 ` QTcpSocket `는 ` errorOccurred()`를 발생시키고, ` Client::displayError() `가 호출됩니다.

먼저 errorOccurred()의 경우를 살펴보겠습니다:

void Client::displayError(QAbstractSocket::SocketError socketError)
{
    switch (socketError) {
    case QAbstractSocket::RemoteHostClosedError:
        break;
    case QAbstractSocket::HostNotFoundError:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The host was not found. Please check the "
                                    "host name and port settings."));
        break;
    case QAbstractSocket::ConnectionRefusedError:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The connection was refused by the peer. "
                                    "Make sure the fortune server is running, "
                                    "and check that the host name and port "
                                    "settings are correct."));
        break;
    default:
        QMessageBox::information(this, tr("Fortune Client"),
                                 tr("The following error occurred: %1.")
                                 .arg(tcpSocket->errorString()));
    }

    getFortuneButton->setEnabled(true);
}

QMessageBox::information()를 사용하여 모든 오류를 대화 상자에 표시합니다. QTcpSocket::RemoteHostClosedError는 포춘 서버 프로토콜이 서버 측에서 연결을 종료하는 것으로 끝나기 때문에 아무런 메시지 없이 무시됩니다.

이제 readyRead() 대안에 대해 살펴보겠습니다. 이 신호는 Client::readFortune() 에 연결되어 있습니다:

void Client::readFortune()
{
    in.startTransaction();

    QString nextFortune;
    in >> nextFortune;

    if (!in.commitTransaction())
        return;

    if (nextFortune == currentFortune) {
        QTimer::singleShot(0, this, &Client::requestNewFortune);
        return;
    }

    currentFortune = nextFortune;
    statusLabel->setText(currentFortune);
    getFortuneButton->setEnabled(true);
}

TCP는 데이터 스트림을 전송하는 방식을 기반으로 하므로, 포춘 메시지를 한 번에 모두 받을 수 있다고 기대할 수는 없습니다. 특히 네트워크 속도가 느린 경우, 데이터는 여러 개의 작은 조각으로 수신될 수 있습니다. ` QTcpSocket `는 들어오는 모든 데이터를 버퍼에 저장하고, 새로운 블록이 도착할 때마다 ` readyRead()` 신호를 발생시킵니다. 따라서 구문 분석을 시작하기 전에 필요한 모든 데이터를 수신했는지 확인하는 것은 우리의 몫입니다.

이를 위해 QDataStream 의 읽기 트랜잭션을 사용합니다. 이 트랜잭션은 스트림 데이터를 내부 버퍼로 계속 읽어들이며, 읽기가 완료되지 않은 경우 롤백을 수행합니다. 먼저 startTransaction()을 호출하여 시작하는데, 이 호출은 소켓에서 새로운 데이터가 수신되었음을 나타내기 위해 스트림 상태를 재설정하기도 합니다. 이어서 ` QDataStream`의 스트리밍 연산자를 사용하여 소켓에서 운세 메시지를 읽어 ` QString`에 저장합니다. 읽기가 완료되면 ` QDataStream::commitTransaction()`를 호출하여 트랜잭션을 완료합니다. 전체 패킷을 수신하지 못한 경우, 이 함수는 스트림 데이터를 초기 위치로 복원하며, 이후 새로운 `readyRead()` 신호를 기다릴 수 있습니다.

읽기 트랜잭션이 성공적으로 완료되면, QLabel::setText()를 호출하여 운세를 표시합니다.

예제 프로젝트 @ code.qt.io

관련 항목: 운세 서버 및 차단형 운세 클라이언트.

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