Qtでネットワークアプリ開発:waitForConnected()を使った同期・非同期接続の具体例

2025-05-27

QAbstractSocket::waitForConnected() は、Qtのネットワークモジュールでソケットの接続処理を行う際に使用される関数です。この関数は、非同期で動作するソケットの接続処理を、指定された時間(または無限)待機させ、接続が成功したかどうかを同期的に確認するために使われます。

主な機能と特徴

  1. ブロッキング呼び出し
    通常、Qtのネットワーク処理(例えばconnectToHost()など)は非同期に行われます。つまり、connectToHost()を呼び出してもすぐに接続が確立されるわけではなく、バックグラウンドで接続処理が進み、接続が完了するとconnected()シグナルが発せられます。 waitForConnected()は、この非同期処理を一時的に「ブロック」させ、接続が完了するまで(またはタイムアウトするまで)現在のスレッドの実行を停止します。

  2. 戻り値

    • true: 接続が正常に確立された場合に返されます。
    • false: 指定された時間内に接続が確立されなかった場合(タイムアウト)、または接続エラーが発生した場合に返されます。
  3. 引数 msecs

    • この引数は、接続を待機する最大時間をミリ秒単位で指定します。
    • デフォルト値は30000ミリ秒(30秒)です。
    • -1 を指定すると、タイムアウトなしで無限に接続を待ちます。ただし、この場合、接続が確立されるまでプログラムが応答しなくなるため、注意が必要です。

使用例(概念)

QTcpSocket socket;
socket.connectToHost("example.com", 80); // 接続を開始

if (socket.waitForConnected(5000)) { // 5秒間接続を待つ
    qDebug() << "接続成功!";
    // データの送受信など、次の処理に進む
} else {
    qDebug() << "接続失敗: " << socket.errorString();
    // エラー処理
}

なぜ使うのか?

  • テストやデバッグ
    接続がうまくいかない場合に、一時的にブロッキング処理を導入して挙動を確認するのに役立つことがあります。
  • 簡単なブロッキング処理
    非同期処理の複雑さを避け、一時的にブロッキングな動作が必要な場合に便利です。例えば、コマンドラインツールや、GUIのないバックグラウンド処理などで、接続確立を待ってから次の処理に進みたい場合に有効です。

注意点

  • 競合状態
    稀に、connectToHost()の呼び出し後、waitForConnected()を呼び出す前に接続が完了してしまうような競合状態が発生する可能性も考慮する必要があります。
  • エラーハンドリング
    waitForConnected()falseを返した場合、QAbstractSocket::error()QAbstractSocket::errorString()を使用して、何が問題だったのか(例:接続拒否、ホストが見つからないなど)を確認することが重要です。
  • メインスレッドでの使用は避ける
    GUIアプリケーションのメインスレッドでwaitForConnected()(特にタイムアウトなしの-1)を使用すると、アプリケーションがフリーズし、ユーザーインターフェースが応答しなくなります。これは、この関数がスレッドをブロックするためです。GUIアプリケーションでは、通常はシグナルとスロットによる非同期処理を使用することが推奨されます。


アプリケーションがフリーズする(GUIの場合)

エラーの症状
GUIアプリケーションのメインスレッドでwaitForConnected()を呼び出すと、アプリケーション全体が応答しなくなり、UIがフリーズする。特に、タイムアウトを長く設定したり、-1(無限待機)を設定した場合に顕著です。

原因
waitForConnected()はブロッキング関数であり、呼び出されたスレッドの実行を停止します。GUIアプリケーションのメインスレッドは、イベントループを実行し、ユーザーからの入力やシステムからのイベントを処理することでUIを更新しています。waitForConnected()がメインスレッドをブロックすると、イベントループが停止し、UIの更新も行われなくなるため、フリーズしたように見えます。

トラブルシューティング

  • 非同期処理に切り替える
    可能な限り、waitForConnected()のようなブロッキング関数ではなく、Qtのシグナルとスロット機構を利用した非同期処理に切り替えることを検討します。connectToHost()を呼び出した後、QAbstractSocket::connected()シグナルとQAbstractSocket::error()シグナルを監視し、それらが発せられたときに次の処理を実行します。

    // 非同期処理の例
    QTcpSocket* socket = new QTcpSocket(this);
    connect(socket, &QTcpSocket::connected, this, &MyClass::onConnected);
    connect(socket, &QTcpSocket::errorOccurred, this, &MyClass::onError);
    socket->connectToHost("example.com", 12345);
    
    // MyClass::onConnected() スロット
    void MyClass::onConnected() {
        qDebug() << "接続成功!";
        // データの送受信など
    }
    
    // MyClass::onError() スロット
    void MyClass::onError(QAbstractSocket::SocketError socketError) {
        qDebug() << "接続エラー: " << socket->errorString();
        // エラーメッセージの表示など
    }
    
  • 別スレッドで実行する
    ネットワーク処理(特にブロッキング処理)は、GUIのメインスレッドとは別のスレッド(QThreadなど)で実行するようにします。接続が確立されたら、connected()シグナルをメインスレッドに発行し、適切なスロットでGUIの更新を行うようにします。

waitForConnected()がすぐにfalseを返す(接続失敗)

エラーの症状
waitForConnected()が期待通りに接続を待たずに、すぐにfalseを返してしまう。

原因とトラブルシューティング

  • QEventLoopの不適切な使用
    ごく稀に、waitForConnected()QEventLoopと組み合わせて非同期処理を擬似的に同期処理にしようとする際に、イベントループの処理が不適切で期待通りに動作しないことがあります。基本的には、Qtのシグナル/スロットと、必要に応じてQThreadを使用するのが最も安全で推奨される方法です。

  • waitForConnected()のタイムアウト設定
    指定したタイムアウト時間(msecs)が短すぎる可能性があります。特にネットワークの状態が不安定な場合や、接続先が遠隔地にある場合、デフォルトの30秒でも足りないことがあります。デバッグ中は、タイムアウトを長めにするか、一時的に-1に設定して接続が成功するかどうかを確認してみると良いでしょう(ただし、フリーズに注意)。

  • error()とerrorString()の確認
    waitForConnected()falseを返した場合、必ずQAbstractSocket::error()QAbstractSocket::errorString()を呼び出して、具体的なエラーコードとメッセージを確認します。これにより、問題の原因を特定する手がかりが得られます。

    if (!socket.waitForConnected(5000)) {
        qDebug() << "接続失敗。エラーコード: " << socket.error();
        qDebug() << "エラーメッセージ: " << socket.errorString();
    }
    

    よくあるエラーコード:

    • QAbstractSocket::ConnectionRefusedError: サーバーが接続を拒否しました。サーバーが起動していないか、指定されたポートでリッスンしていない可能性があります。
    • QAbstractSocket::HostNotFoundError: 指定されたホスト名が見つかりませんでした。DNS解決の問題か、ホスト名が間違っている可能性があります。
    • QAbstractSocket::SocketAccessError: ソケットのアクセス権に関する問題です(稀)。
    • QAbstractSocket::NetworkError: ネットワーク関連の一般的なエラーです。
  • connectToHost()が呼び出されていない、または正しくない
    waitForConnected()を呼び出す前に、connectToHost()が正しく呼び出されていることを確認します。

    QTcpSocket socket;
    socket.connectToHost("example.com", 12345); // これが重要
    if (socket.waitForConnected(5000)) { /* ... */ }
    
  • 接続先が利用可能でない

    • サーバーが起動していない
      接続しようとしているサーバーアプリケーションが起動しているか確認します。
    • IPアドレスまたはポート番号が間違っている
      接続先のIPアドレス(またはホスト名)とポート番号が正しいか再確認します。Typoがないか、ファイアウォールでブロックされていないかなどを確認します。
    • ファイアウォール
      クライアント側またはサーバー側のファイアウォールが接続をブロックしている可能性があります。一時的にファイアウォールを無効にしてテストするか、該当のポートを開放する設定を確認します。
    • ネットワーク到達性
      クライアントとサーバー間でネットワーク接続があるか(pingが通るかなど)確認します。

マルチスレッド環境での問題

エラーの症状
複数のスレッドから同じQAbstractSocketオブジェクトに対してwaitForConnected()を呼び出したり、または異なるスレッドでQAbstractSocketオブジェクトを生成・使用するとクラッシュしたり、予期せぬ動作をしたりする。

原因
QAbstractSocket(およびQtの多くのQObjectベースのクラス)は、通常、それが作成されたスレッド(メインスレッドなど)のイベントループ内で動作することを前提としています。異なるスレッドから直接メソッドを呼び出すと、スレッドセーフティの問題や競合状態が発生する可能性があります。特に、new QTcpSocket() をあるスレッドで作成し、別のスレッドでそのメソッドを呼び出すと問題が発生しやすいです。

トラブルシューティング

  • QMutexなどによる同期
    waitForConnected()自体はブロッキングなので、この関数を呼び出すスレッド内での同期はあまり問題になりませんが、もしソケットオブジェクト自体が複数のスレッドからアクセスされる可能性がある場合は、QMutexなどを用いてアクセスを同期する必要があります。

  • moveToThread()を使用する
    ソケットオブジェクトを特定のワーカースレッドのイベントループに移動させるには、QObject::moveToThread()を使用します。これにより、ソケットのシグナルとスロットがそのスレッドのコンテキストで処理されるようになります。

    // WorkerThread.h
    class WorkerThread : public QThread {
        Q_OBJECT
    public:
        void run() override {
            QTcpSocket socket;
            // socket.moveToThread(this); // QThread::run() の中では不要
            // QTcpSocket は、それが作成されたスレッドのイベントループ内で動作します
            // この場合、socket は WorkerThread の中で作成されたので、このスレッド内で動く
    
            socket.connectToHost("example.com", 12345);
            if (socket.waitForConnected(5000)) {
                qDebug() << "Worker Thread: Connected!";
            } else {
                qDebug() << "Worker Thread: Connection failed: " << socket.errorString();
            }
            exec(); // このスレッドのイベントループを開始
        }
    };
    
  • 各スレッドで独自のソケットを生成する
    各スレッドが独自のQAbstractSocket(例: QTcpSocket)インスタンスを生成し、そのスレッド内で操作を完結させるのが最もシンプルな方法です。



以下に、waitForConnected() を使ったプログラミング例をいくつか示します。

例1: シンプルなTCPクライアント (コンソールアプリケーション向け)

この例は、指定されたホストとポートに接続を試み、接続の成否とエラー情報を表示するシンプルなクライアントです。

#include <QCoreApplication>
#include <QTcpSocket>
#include <QDebug>
#include <QString>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 接続先ホストとポート
    QString hostName = "localhost"; // または "127.0.0.1", "example.com" など
    quint16 port = 12345;          // サーバーがリッスンしているポート番号

    QTcpSocket socket;

    qDebug() << "接続を試みています: " << hostName << ":" << port;

    // ホストへの接続を開始(非同期)
    socket.connectToHost(hostName, port);

    // 接続が確立されるまで待機(最大5秒)
    // GUIアプリケーションのメインスレッドでは使用しないこと!
    if (socket.waitForConnected(5000)) { // 5000ミリ秒 = 5秒
        qDebug() << "接続成功!";

        // ここからデータの送受信を行う
        QByteArray dataToSend = "Hello, server!";
        socket.write(dataToSend);
        qDebug() << "データを送信しました: " << dataToSend;

        // サーバーからの応答を待機 (最大3秒)
        if (socket.waitForReadyRead(3000)) {
            QByteArray dataReceived = socket.readAll();
            qDebug() << "データを受信しました: " << dataReceived;
        } else {
            qDebug() << "サーバーからの応答がありませんでした (タイムアウト): " << socket.errorString();
        }

        // 接続を閉じる
        socket.disconnectFromHost();
        if (socket.state() == QAbstractSocket::UnconnectedState || socket.waitForDisconnected(1000)) {
            qDebug() << "接続を閉じました。";
        } else {
            qDebug() << "接続を切断できませんでした: " << socket.errorString();
        }

    } else {
        qDebug() << "接続失敗: " << socket.errorString();
        qDebug() << "ソケットの状態: " << socket.state();
    }

    return 0; // コンソールアプリケーションなので、イベントループは不要
}

このコードのポイント

  • waitForReadyRead() / waitForBytesWritten(): 接続後も、データの送受信には同様のブロッキング関数を使用できます。
  • socket.state(): ソケットの現在の状態を確認できます。接続試行中は ConnectingState、接続成功後は ConnectedState など。
  • socket.errorString(): waitForConnected()false を返した場合、この関数を呼び出すことで具体的なエラーメッセージを取得できます。これはトラブルシューティングに非常に役立ちます。
  • socket.waitForConnected(5000): ここでソケットの接続が確立されるまで、最大5000ミリ秒(5秒)待機します。
    • true が返された場合、接続は成功しています。
    • false が返された場合、タイムアウトしたか、接続エラーが発生しています。
  • socket.connectToHost(hostName, port);: 指定されたホストとポートへの接続を開始します。この呼び出しは非同期であり、すぐに戻ってきます。
  • QTcpSocket socket;: TCPソケットのインスタンスを作成します。

GUIアプリケーションのメインスレッドで waitForConnected() を使うとUIがフリーズしてしまうため、この関数は通常、専用のワーカースレッド内で使用されます。

workerthread.h

#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H

#include <QThread>
#include <QTcpSocket>
#include <QHostAddress>
#include <QDebug>

class WorkerThread : public QThread
{
    Q_OBJECT
public:
    explicit WorkerThread(QObject *parent = nullptr);
    ~WorkerThread();

    void connectToServer(const QString &host, quint16 port);

signals:
    void connectionSuccess(const QString &message);
    void connectionError(const QString &errorMessage);
    void dataReceived(const QByteArray &data);

protected:
    void run() override;

private:
    QTcpSocket *socket;
    QString targetHost;
    quint16 targetPort;
    bool quitThread;
};

#endif // WORKERTHREAD_H

workerthread.cpp

#include "workerthread.h"
#include <QEventLoop> // waitForConnectedと組み合わせる場合、内部でイベントループが回りますが、
                      // 明示的に使用するならQEventLoopも選択肢

WorkerThread::WorkerThread(QObject *parent)
    : QThread(parent),
      socket(nullptr),
      targetPort(0),
      quitThread(false)
{
    // スレッドが開始されたときにsocketを生成する
    // QObjectは作成されたスレッドにアフィニティを持つため、ここでソケットをnewすると
    // run()内でソケットのイベントループが処理されることになります。
}

WorkerThread::~WorkerThread()
{
    quitThread = true;
    if (socket) {
        socket->abort(); // 強制的に切断
        socket->deleteLater(); // スレッド終了時に削除されるようにマーク
    }
    wait(); // スレッドが終了するのを待つ
}

void WorkerThread::connectToServer(const QString &host, quint16 port)
{
    targetHost = host;
    targetPort = port;
    start(); // スレッドを開始
}

void WorkerThread::run()
{
    // スレッド内でソケットを作成 (QObjectのアフィニティをこのスレッドに設定するため)
    socket = new QTcpSocket();

    qDebug() << "ワーカースレッド: 接続を試みています: " << targetHost << ":" << targetPort;

    socket->connectToHost(targetHost, targetPort);

    // 接続が確立されるまで待機(最大10秒)
    if (socket->waitForConnected(10000)) { // 10秒待機
        emit connectionSuccess("接続成功!");
        qDebug() << "ワーカースレッド: 接続成功!";

        // 接続後もデータの送受信をブロックして待機する例
        // 実際のアプリケーションでは、readyReadシグナルを処理するイベントループを回す方が一般的です。
        QByteArray dataToSend = "Request data from server.";
        socket->write(dataToSend);
        if (socket->waitForBytesWritten(3000)) {
            qDebug() << "ワーカースレッド: データを送信しました。";
        } else {
            qDebug() << "ワーカースレッド: データ送信失敗 (タイムアウト): " << socket->errorString();
        }

        if (socket->waitForReadyRead(5000)) {
            QByteArray receivedData = socket->readAll();
            emit dataReceived(receivedData);
            qDebug() << "ワーカースレッド: データを受信しました: " << receivedData;
        } else {
            qDebug() << "ワーカースレッド: 受信データなし (タイムアウト): " << socket->errorString();
        }

    } else {
        emit connectionError("接続失敗: " + socket->errorString());
        qDebug() << "ワーカースレッド: 接続失敗: " << socket->errorString();
    }

    // ワーカースレッドのイベントループを開始(任意、必要に応じて)
    // 例えば、ソケットのシグナル(readyReadなど)を継続的に処理したい場合は exec() を呼び出す。
    // 今回の例のように、waitForConnected() で一度接続を確立し、すぐに処理を終える場合は不要。
    // exec(); // もしソケットのシグナルを処理し続ける場合は必要

    socket->disconnectFromHost();
    socket->waitForDisconnected(1000); // 切断を待機
    qDebug() << "ワーカースレッド: ソケット切断。";

    // スレッド終了時にソケットオブジェクトをクリーンアップ
    socket->deleteLater();
    socket = nullptr;
}

main.cpp (GUIアプリケーションの例)

#include <QApplication>
#include <QPushButton>
#include <QVBoxLayout>
#include <QLabel>
#include "workerthread.h" // ワーカースレッドのヘッダー

class MainWindow : public QWidget
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = nullptr) : QWidget(parent)
    {
        setupUI();
    }

private slots:
    void onConnectButtonClicked()
    {
        qDebug() << "UIスレッド: 接続ボタンがクリックされました。";
        statusLabel->setText("接続中...");
        connectButton->setEnabled(false);

        // ワーカースレッドを開始
        workerThread = new WorkerThread(this); // 親をMainWindowに設定することで、MainWindowが削除されたときに自動的に削除される
        connect(workerThread, &WorkerThread::connectionSuccess, this, &MainWindow::handleConnectionSuccess);
        connect(workerThread, &WorkerThread::connectionError, this, &MainWindow::handleConnectionError);
        connect(workerThread, &WorkerThread::dataReceived, this, &MainWindow::handleDataReceived);
        connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater); // スレッド終了時にオブジェクトを削除

        workerThread->connectToServer("localhost", 12345); // 接続開始
    }

    void handleConnectionSuccess(const QString &message)
    {
        qDebug() << "UIスレッド: 接続成功シグナルを受信しました。";
        statusLabel->setText(message);
        connectButton->setEnabled(true);
    }

    void handleConnectionError(const QString &errorMessage)
    {
        qDebug() << "UIスレッド: 接続エラーシグナルを受信しました。";
        statusLabel->setText("エラー: " + errorMessage);
        connectButton->setEnabled(true);
    }

    void handleDataReceived(const QByteArray &data)
    {
        qDebug() << "UIスレッド: データ受信シグナルを受信しました。";
        dataLabel->setText("受信データ: " + QString(data));
    }

private:
    void setupUI()
    {
        QVBoxLayout *layout = new QVBoxLayout(this);

        connectButton = new QPushButton("サーバーに接続");
        layout->addWidget(connectButton);

        statusLabel = new QLabel("ステータス: 未接続");
        layout->addWidget(statusLabel);

        dataLabel = new QLabel("受信データ: (なし)");
        layout->addWidget(dataLabel);

        connect(connectButton, &QPushButton::clicked, this, &MainWindow::onConnectButtonClicked);
    }

    QPushButton *connectButton;
    QLabel *statusLabel;
    QLabel *dataLabel;
    WorkerThread *workerThread; // ワーカースレッドのポインタ
};

#include "main.moc" // mocファイルを含める

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow window;
    window.setWindowTitle("waitForConnectedの例");
    window.resize(300, 200);
    window.show();

    return a.exec();
}
  • QObject::deleteLater(): スレッドが終了したときにワーカースレッドオブジェクトが適切にクリーンアップされるように、finished() シグナルを deleteLater() スロットに接続しています。
  • シグナルとスロット: ワーカースレッドは、処理結果(接続成功、エラー、データ受信など)をシグナルとしてメインスレッドに通知します。メインスレッドのUIオブジェクトは、これらのシグナルをスロットで受け取り、UIを更新します。これにより、UIのフリーズを防ぎつつ、ブロッキング処理を実行できます。
  • run() メソッド内での QTcpSocket の生成: QTcpSocket のインスタンスは、それを操作するスレッド内で生成することが重要です。これにより、ソケットの内部イベントループがそのスレッドで処理され、スレッドセーフティが保たれます。
  • WorkerThread クラス: QThread を継承し、run() メソッド内でブロッキング処理(waitForConnected() など)を実行します。


シグナルとスロットによる非同期処理 (最も推奨される方法)

Qt の非同期プログラミングの基本であり、最も Qt らしい方法です。connectToHost() を呼び出した後、接続が成功したり、エラーが発生したり、データが利用可能になったりしたときに、ソケットがシグナルを発行します。これらのシグナルをカスタムスロットに接続することで、イベント駆動型の処理を実現します。

利点

  • Qt の設計思想に合致
    他の Qt クラスとの連携がスムーズです。
  • 効率的
    必要のない間はリソースを消費しません。
  • UI の応答性維持
    メインスレッドがブロックされないため、GUI アプリケーションで UI がフリーズするのを防ぎます。

使用するシグナル

  • bytesWritten(qint64 bytes): データがソケットに書き込まれたときに発行されます。
  • readyRead(): 読み取り可能な新しいデータが到着したときに発行されます。
  • stateChanged(QAbstractSocket::SocketState socketState): ソケットの状態が変化したときに発行されます。
  • errorOccurred(QAbstractSocket::SocketError socketError): ソケットエラーが発生したときに発行されます。
  • disconnected(): ソケットが切断されたときに発行されます。
  • connected(): 接続が正常に確立されたときに発行されます。

コード例

#include <QApplication>
#include <QTcpSocket>
#include <QDebug>
#include <QPushButton>
#include <QVBoxLayout>
#include <QLabel>

class ClientWidget : public QWidget
{
    Q_OBJECT // シグナルとスロットを使用するために必要

public:
    explicit ClientWidget(QWidget *parent = nullptr)
        : QWidget(parent)
    {
        socket = new QTcpSocket(this); // ソケットオブジェクトを生成

        // シグナルとスロットの接続
        connect(socket, &QTcpSocket::connected, this, &ClientWidget::onConnected);
        connect(socket, &QTcpSocket::disconnected, this, &ClientWidget::onDisconnected);
        connect(socket, &QTcpSocket::readyRead, this, &ClientWidget::onReadyRead);
        // Qt 5 以降では error() シグナルは errorOccurred() に変更されています
        connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred),
                this, &ClientWidget::onErrorOccurred);
        connect(socket, &QTcpSocket::stateChanged, this, &ClientWidget::onStateChanged);


        // UIのセットアップ
        QVBoxLayout *layout = new QVBoxLayout(this);
        connectButton = new QPushButton("接続開始");
        disconnectButton = new QPushButton("切断");
        disconnectButton->setEnabled(false); // 最初は無効
        statusLabel = new QLabel("ステータス: 未接続");
        receivedDataLabel = new QLabel("受信データ: (なし)");

        layout->addWidget(connectButton);
        layout->addWidget(disconnectButton);
        layout->addWidget(statusLabel);
        layout->addWidget(receivedDataLabel);

        connect(connectButton, &QPushButton::clicked, this, &ClientWidget::startConnection);
        connect(disconnectButton, &QPushButton::clicked, socket, &QTcpSocket::disconnectFromHost);
    }

private slots:
    void startConnection()
    {
        qDebug() << "接続試行中...";
        statusLabel->setText("ステータス: 接続中...");
        connectButton->setEnabled(false);
        disconnectButton->setEnabled(false); // 接続中に切断ボタンを無効にする
        socket->connectToHost("localhost", 12345); // 非同期で接続を開始
    }

    void onConnected()
    {
        qDebug() << "接続成功!";
        statusLabel->setText("ステータス: 接続済み");
        connectButton->setEnabled(false);
        disconnectButton->setEnabled(true);

        // 接続成功後にデータを送信する例
        QByteArray data = "Hello from client!";
        socket->write(data);
        qDebug() << "データを送信しました: " << data;
    }

    void onDisconnected()
    {
        qDebug() << "切断されました。";
        statusLabel->setText("ステータス: 未接続");
        receivedDataLabel->setText("受信データ: (なし)");
        connectButton->setEnabled(true);
        disconnectButton->setEnabled(false);
    }

    void onReadyRead()
    {
        QByteArray data = socket->readAll();
        qDebug() << "データを受信しました: " << data;
        receivedDataLabel->setText("受信データ: " + QString(data));
    }

    void onErrorOccurred(QAbstractSocket::SocketError socketError)
    {
        Q_UNUSED(socketError); // 使用しない引数だが、警告を避けるため
        qDebug() << "エラーが発生しました: " << socket->errorString();
        statusLabel->setText("ステータス: エラー - " + socket->errorString());
        connectButton->setEnabled(true); // エラーが発生した場合は再接続できるようにする
        disconnectButton->setEnabled(false);
    }

    void onStateChanged(QAbstractSocket::SocketState socketState)
    {
        QString stateStr;
        switch (socketState) {
            case QAbstractSocket::UnconnectedState: stateStr = "UnconnectedState"; break;
            case QAbstractSocket::HostLookupState:  stateStr = "HostLookupState"; break;
            case QAbstractSocket::ConnectingState:  stateStr = "ConnectingState"; break;
            case QAbstractSocket::ConnectedState:   stateStr = "ConnectedState"; break;
            case QAbstractSocket::BoundState:       stateStr = "BoundState"; break;
            case QAbstractSocket::ClosingState:     stateStr = "ClosingState"; break;
            case QAbstractSocket::ListeningState:   stateStr = "ListeningState"; break; // QTcpServerで使われる
        }
        qDebug() << "ソケット状態変化: " << stateStr;
    }

private:
    QTcpSocket *socket;
    QPushButton *connectButton;
    QPushButton *disconnectButton;
    QLabel *statusLabel;
    QLabel *receivedDataLabel;
};

#include "main.moc" // MOCで生成されるファイルをインクルード

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    ClientWidget client;
    client.setWindowTitle("非同期ソケット接続例");
    client.resize(300, 200);
    client.show();

    return a.exec();
}

QEventLoop を使用した擬似的なブロッキング (特定の状況下でのみ使用)

QEventLoop を使うと、一時的に現在のスレッド内でローカルイベントループを実行し、特定のシグナルが発せられるまで待機することができます。これにより、GUI スレッドを完全にフリーズさせることなく、非同期処理を同期的に待機するような挙動を実現できます。しかし、これは慎重に使用すべきです。過度に使用すると、デッドロックや複雑なイベント処理のバグにつながる可能性があります。

利点

  • 限定的なブロッキング
    waitForConnected() のように完全にスレッドをフリーズさせるのではなく、その間にイベントループを回せるため、限定的な UI 更新は可能です(ただし推奨されません)。
  • 簡潔なコード
    シグナルとスロットを多数設定する代わりに、一部のロジックをより直線的に記述できます。

注意点

  • 複雑性の増加
    見た目はシンプルになりますが、内部のイベント処理の複雑性が増し、デバッグが難しくなることがあります。
  • 再入問題
    ローカルイベントループ中に、すでに実行中のコードが再入する可能性があり、予期せぬ動作を引き起こすことがあります。
  • デッドロックのリスク
    ローカルイベントループ中に、メインイベントループが期待するシグナルが発行されない場合、デッドロックに陥る可能性があります。
#include <QCoreApplication>
#include <QTcpSocket>
#include <QDebug>
#include <QEventLoop> // QEventLoop を使用

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QTcpSocket socket;
    QString hostName = "localhost";
    quint16 port = 12345;

    qDebug() << "接続試行中 (QEventLoopを使用): " << hostName << ":" << port;

    socket.connectToHost(hostName, port); // 接続を開始

    QEventLoop loop;
    // connected() シグナルが発せられたらループを終了
    QObject::connect(&socket, &QTcpSocket::connected, &loop, &QEventLoop::quit);
    // errorOccurred() シグナルが発せられたらループを終了
    QObject::connect(&socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred),
                     &loop, &QEventLoop::quit);

    // 指定された時間(例: 5秒)待機するか、シグナルが発せられるまでループを実行
    // loop.exec() は QEventLoop::quit() が呼び出されるまでブロックします
    QTimer::singleShot(5000, &loop, &QEventLoop::quit); // 5秒後に強制終了
    loop.exec();

    if (socket.state() == QAbstractSocket::ConnectedState) {
        qDebug() << "接続成功! (QEventLoop経由)";
        // データの送受信など
        // ...
        socket.disconnectFromHost();
    } else {
        qDebug() << "接続失敗 (QEventLoop経由): " << socket.errorString();
    }

    return 0;
}

前回の説明で触れましたが、waitForConnected() を安全に使用する最も一般的な方法は、GUI のメインスレッドとは別のワーカースレッド内で実行することです。

利点

  • 同期的なコード記述
    ワーカースレッド内では、waitForConnected()waitForReadyRead() のようなブロッキング関数を、見通しの良い同期的なスタイルで記述できます。
  • UI の応答性維持
    メインスレッドはブロックされません。
  • 分離されたロジック
    ネットワーク処理を UI から完全に分離できます。

注意点

  • オブジェクトアフィニティ
    QTcpSocket オブジェクトは、それを生成したスレッドにアフィニティを持ちます。そのため、QTcpSocket をワーカースレッド内で生成し、そのスレッド内で操作するようにしてください。
  • スレッド間の通信
    ワーカースレッドとメインスレッド間で結果をやり取りするには、シグナルとスロットを使用する必要があります。

この方法は、前回の説明の「例2: ワーカースレッドでの waitForConnected()」で詳細なコード例とともに説明されています。