포춘 클라이언트 차단하기
네트워크 서비스용 클라이언트를 만드는 방법을 보여줍니다.
QTcpSocket 네트워크 프로그래밍에 대한 두 가지 일반적인 접근 방식을 지원합니다:
- 비동기(비 블로킹) 접근 방식. 제어가 Qt의 이벤트 루프로 돌아올 때 작업이 예약되고 수행됩니다. 연산이 완료되면 QTcpSocket 은 신호를 방출합니다. 예를 들어 QTcpSocket::connectToHost()는 즉시 반환되고, 연결이 설정되면 QTcpSocket 은 connected()를 반환합니다.
- 동기식(블로킹) 접근 방식. 비GUI 및 멀티스레드 애플리케이션에서는
waitFor...()
함수(예: QTcpSocket::waitForConnected())를 호출하여 신호에 연결하는 대신 작업이 완료될 때까지 호출 스레드를 일시 중단할 수 있습니다.
구현은 포춘 클라이언트 예제와 매우 유사하지만 QTcpSocket 을 메인 클래스의 멤버로 두고 메인 스레드에서 비동기 네트워킹을 수행하는 대신 모든 네트워크 작업을 별도의 스레드에서 수행하고 QTcpSocket 의 블로킹 API를 사용합니다.
이 예제의 목적은 사용자 인터페이스의 응답성을 잃지 않으면서 네트워킹 코드를 단순화하는 데 사용할 수 있는 패턴을 보여주기 위한 것입니다. Qt의 블로킹 네트워크 API를 사용하면 코드를 간소화할 수 있지만, 블로킹 동작으로 인해 사용자 인터페이스가 멈추는 것을 방지하기 위해 비-GUI 스레드에서만 사용해야 합니다. 그러나 많은 사람들이 생각하는 것과는 달리 QThread 에서 스레드를 사용한다고 해서 애플리케이션에 관리할 수 없는 복잡성이 추가되는 것은 아닙니다.
네트워크 코드를 처리하는 FortuneThread 클래스부터 시작하겠습니다.
class FortuneThread : public QThread { Q_OBJECT public: FortuneThread(QObject *parent = nullptr); ~FortuneThread(); void requestNewFortune(const QString &hostName, quint16 port); void run() override; signals: void newFortune(const QString &fortune); void error(int socketError, const QString &message); private: QString hostName; quint16 port; QMutex mutex; QWaitCondition cond; bool quit; };
FortuneThread는 운세 요청을 예약하기 위한 API를 제공하는 QThread 서브클래스로, 운세를 전달하고 오류를 보고하는 시그널을 가지고 있습니다. 요청NewFortune()을 호출하여 새로운 운세를 요청할 수 있으며, 그 결과는 newFortune() 신호로 전달됩니다. 오류가 발생하면 error() 신호가 전송됩니다.
요청NewFortune()은 메인 GUI 스레드에서 호출되지만 저장되는 호스트 이름과 포트 값은 FortuneThread의 스레드에서 액세스된다는 점에 유의해야 합니다. 서로 다른 스레드에서 FortuneThread의 데이터 멤버를 동시에 읽고 쓸 것이므로 QMutex 을 사용하여 액세스를 동기화합니다.
void FortuneThread::requestNewFortune(const QString &hostName, quint16 port) { QMutexLocker locker(&mutex); this->hostName = hostName; this->port = port; if (!isRunning()) start(); else cond.wakeOne(); }
requestNewFortune() 함수는 포춘 서버의 호스트 이름과 포트를 멤버 데이터로 저장하고, 이 데이터를 보호하기 위해 QMutexLocker 으로 뮤텍스를 잠급니다. 그런 다음 스레드가 이미 실행 중이 아니라면 스레드를 시작합니다. 나중에 QWaitCondition::wakeOne() 호출로 다시 돌아오겠습니다.
void FortuneThread::run() { mutex.lock(); QString serverName = hostName; quint16 serverPort = port; mutex.unlock();
run() 함수에서는 뮤텍스 잠금을 획득하고 멤버 데이터에서 호스트 이름과 포트를 가져온 다음 다시 잠금을 해제하는 것으로 시작합니다. 우리가 보호하고자 하는 경우는 데이터를 가져오는 동시에 requestNewFortune()
이 호출될 수 있다는 것입니다. QString 은 재진입은 가능하지만 스레드 안전하지 않으며, 한 요청에서 호스트 이름과 다른 요청의 포트를 읽을 가능성이 있는 위험도 피해야 합니다. 그리고 짐작하셨겠지만 FortuneThread는 한 번에 하나의 요청만 처리할 수 있습니다.
이제 run() 함수가 루프에 들어갑니다:
while (!quit) { const int Timeout = 5 * 1000; QTcpSocket socket; socket.connectToHost(serverName, serverPort);
이 루프는 quit이 false인 한 운세를 계속 요청합니다. 스택에 QTcpSocket 을 생성하여 첫 번째 요청을 시작한 다음 connectToHost()을 호출합니다. 이렇게 하면 비동기 연산이 시작되고, 제어가 Qt의 이벤트 루프로 돌아간 후 QTcpSocket 가 connected() 또는 error()를 방출하게 됩니다.
if (!socket.waitForConnected(Timeout)) { emit error(socket.error(), socket.errorString()); return; }
하지만 비 GUI 스레드에서 실행 중이므로 사용자 인터페이스를 차단하는 것에 대해 걱정할 필요가 없습니다. 따라서 이벤트 루프에 들어가는 대신 QTcpSocket::waitForConnected()를 호출하기만 하면 됩니다. 이 함수는 QTcpSocket 가 connected()를 반환하거나 오류가 발생할 때까지 호출 스레드를 차단하고 대기합니다. connected()가 반환되면 참을 반환하고, 연결이 실패하거나 시간 초과(이 예에서는 5초 후에 발생)가 발생하면 거짓을 반환합니다. QTcpSocket::waitForConnected()는 다른 waitFor...()
함수와 마찬가지로 QTcpSocket 의 차단 API의 일부입니다.
이 문 뒤에 작업할 소켓이 연결되었습니다.
QDataStream in(&socket); in.setVersion(QDataStream::Qt_6_5); QString fortune;
이제 QDataStream 객체를 생성하여 QDataStream 의 생성자에 소켓을 전달하고 다른 클라이언트 예제에서와 마찬가지로 스트림 프로토콜 버전을 QDataStream::Qt_6_5 로 설정합니다.
do { if (!socket.waitForReadyRead(Timeout)) { emit error(socket.error(), socket.errorString()); return; } in.startTransaction(); in >> fortune; } while (!in.commitTransaction());
QTcpSocket::waitForReadyRead()를 호출하여 행운 문자열 데이터를 기다리는 루프를 시작하여 진행합니다. false를 반환하면 작업을 중단합니다. 이 문이 끝나면 스트림 읽기 트랜잭션을 시작합니다. QDataStream::commitTransaction ()가 참을 반환하면 루프를 종료하며, 이는 운세 문자열 로딩에 성공했음을 의미합니다. 결과 운세는 newFortune()을 통해 전달됩니다:
mutex.lock(); emit newFortune(fortune); cond.wait(&mutex); serverName = hostName; serverPort = port; mutex.unlock(); }
루프의 마지막 부분은 멤버 데이터에서 안전하게 읽을 수 있도록 뮤텍스를 획득하는 것입니다. 그런 다음 QWaitCondition::wait()를 호출하여 스레드를 절전 모드로 전환합니다. 이 시점에서 요청NewFortune()으로 돌아가서 wakeOne() 호출을 자세히 살펴볼 수 있습니다:
void FortuneThread::requestNewFortune(const QString &hostName, quint16 port) { ... if (!isRunning()) start(); else cond.wakeOne(); }
여기서 일어난 일은 스레드가 새 요청을 기다리면서 잠자기 상태가 되었기 때문에 새 요청이 도착하면 다시 깨워야 한다는 것입니다. QWaitCondition 는 이와 같은 깨우기 호출을 알리기 위해 스레드에서 자주 사용됩니다.
FortuneThread::~FortuneThread() { mutex.lock(); quit = true; cond.wakeOne(); mutex.unlock(); wait(); }
FortuneThread 연습을 마치면서, 이것은 quit을 true로 설정하고, 스레드를 깨우고, 스레드가 종료될 때까지 기다렸다가 반환하는 소멸자입니다. 이렇게 하면 run()의 while
루프가 현재 반복을 완료할 수 있습니다. run()이 반환되면 스레드는 종료되고 소멸됩니다.
이제 블록킹클라이언트 클래스를 살펴보겠습니다:
class BlockingClient : public QWidget { Q_OBJECT public: BlockingClient(QWidget *parent = nullptr); private slots: void requestNewFortune(); void showFortune(const QString &fortune); void displayError(int socketError, const QString &message); void enableGetFortuneButton(); private: QLabel *hostLabel; QLabel *portLabel; QLineEdit *hostLineEdit; QLineEdit *portLineEdit; QLabel *statusLabel; QPushButton *getFortuneButton; QPushButton *quitButton; QDialogButtonBox *buttonBox; FortuneThread thread; QString currentFortune; };
BlockingClient는 포춘 클라이언트 예제의 클라이언트 클래스와 매우 유사하지만 이 클래스에서는 QTcpSocket 에 대한 포인터 대신 FortuneThread 멤버를 저장합니다. 사용자가 "Get Fortune" 버튼을 클릭하면 동일한 슬롯이 호출되지만 그 구현은 약간 다릅니다:
connect(&thread, &FortuneThread::newFortune, this, &BlockingClient::showFortune); connect(&thread, &FortuneThread::error, this, &BlockingClient::displayError);
이전 예제의 QTcpSocket::readyRead() 및 QTcpSocket::error()와 다소 유사한 FortuneThread의 두 신호 newFortune() 및 error()를 requestNewFortune() 및 displayError()에 연결합니다.
void BlockingClient::requestNewFortune() { getFortuneButton->setEnabled(false); thread.requestNewFortune(hostLineEdit->text(), portLineEdit->text().toInt()); }
requestNewFortune() 슬롯은 요청을 스케줄링하는 FortuneThread::requestNewFortune()을 호출합니다. 스레드가 새로운 포춘을 수신하고 newFortune()을 방출하면 showFortune() 슬롯이 호출됩니다:
void BlockingClient::showFortune(const QString &nextFortune) { if (nextFortune == currentFortune) { requestNewFortune(); return; } currentFortune = nextFortune; statusLabel->setText(currentFortune); getFortuneButton->setEnabled(true); }
여기서는 인자로 받은 포춘을 표시하기만 하면 됩니다.
© 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.