ブロック・フォーチュン・クライアント

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

QTcpSocket ネットワーク・プログラミングの2つの一般的なアプローチをサポートする:

  • 非同期(ノンブロッキング)アプローチ。操作はスケジュールされ、Qt のイベントループに制御が戻ったときに実行されます。操作が終了すると、QTcpSocket からシグナルが出力されます。例えば、QTcpSocket::connectToHost() はすぐに戻り、接続が確立されると、QTcpSocketconnected() を発信します。
  • 同期(ブロッキング)アプローチ。非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()関数は、fortuneサーバーのホスト名とポートをメンバー・データとして格納し、このデータを保護するために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()が発行されると、この関数はtrueを返し、接続に失敗したりタイムアウトしたりすると(この例では5秒後に発生する)、falseが返される。QTcpSocket::waitForConnected()は、他のwaitFor...() 関数と同様、QTcpSocket'sblocking API の一部である。

この文の後、接続されたソケットを扱うことができる。

        QDataStream in(&socket);
        in.setVersion(QDataStream::Qt_6_5);
        QString fortune;

QDataStream QDataStream::Qt_6_5 のコンストラクタにソケットを渡し、 オブジェクトを作成することができる。QDataStream

        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も参照してください

©2024 The Qt Company Ltd. 本文書に含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundationによって発行されたGNU Free Documentation License version 1.3の条項の下でライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。