Fortuneクライアントのブロック

ネットワークサービス用のクライアントを作成する方法を示します。

ユーザーがサーバーの詳細情報を指定し、Q TcpSocketのブロッキングAPIを使用してネットワーク操作を実行します。

QTcpSocket ネットワークプログラミングには、主に以下の2つのアプローチがあります:

  • 非同期(ノンブロッキング)アプローチ。操作はスケジュールされ、制御がQtのイベントループに戻った際に実行されます。操作が完了すると、QTcpSocket がシグナルを発行します。例えば、QTcpSocket::connectToHost()は即座に返り、接続が確立されると、QTcpSocket がconnected()を発行します。
  • 同期(ブロッキング)方式。非GUIおよびマルチスレッドアプリケーションでは、シグナルに接続する代わりに、waitFor...() 関数(例:QTcpSocket::waitForConnected())を呼び出し、操作が完了するまで呼び出しスレッドを一時停止させることができます。

実装は「Fortune Client」の例と非常によく似ていますが、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はQThread のサブクラスであり、フォーチュンのリクエストをスケジューリングするためのAPIを提供するとともに、フォーチュンの配信やエラーの報告を行うシグナルを備えています。requestNewFortune()を呼び出すことで新しいフォーチュンをリクエストでき、その結果はnewFortune()シグナルによって配信されます。エラーが発生した場合は、error()シグナルが発火します。

重要な点として、requestNewFortune()はメインの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は一度に1つのリクエストしか処理できません。

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() が発行された場合、関数は true を返します。接続に失敗した場合やタイムアウトした場合(この例では 5 秒後に発生します)、false が返されます。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() が true を返すと、フォーチュンの文字列の読み込みに成功したことを意味するため、ループから抜け出します。結果として得られたフォーチュンは、newFortune() を呼び出して出力されます:

        mutex.lock();
        emit newFortune(fortune);

        cond.wait(&mutex);
        serverName = hostName;
        serverPort = port;
        mutex.unlock();
    }

ループの最後の部分では、メンバデータから安全に読み込めるよう、ミューテックスを取得します。その後、QWaitCondition::wait() を呼び出してスレッドをスリープ状態にします。この時点で、requestNewFortune() に戻り、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()` が戻ると、スレッドは終了し、破棄されます。

次に、BlockingClient クラスについて説明します。

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は、Fortune Clientの例にあるClientクラスと非常によく似ていますが、このクラスではQTcpSocket へのポインタの代わりに、FortuneThreadのメンバを格納しています。ユーザーが「Get Fortune」ボタンをクリックすると、同じスロットが呼び出されますが、その実装は少し異なります:

    connect(&thread, &FortuneThread::newFortune,
            this, &BlockingClient::showFortune);
    connect(&thread, &FortuneThread::error,
            this, &BlockingClient::displayError);

FortuneThreadの2つのシグナルであるnewFortune()とerror()(これらは前の例のQTcpSocket::readyRead()やQTcpSocket::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);
}

ここでは、引数として受け取った占いメッセージを単純に表示します。

サンプルプロジェクト @ code.qt.io

「Fortune Client」および「Fortune Server」も参照してください 。

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