Qtの基礎 - ネットワーク

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要



HTTPリクエスト / HTTPレスポンス (非同期)

一定のタイミングで取得

  1. ネットワークアクセスマネージャの設定
    QNetworkAccessManagerクラスを使用して、HTTPリクエストを送信して、HTTPレスポンスを受信する。
    QNetworkAccessManagerクラスは非同期で動作し、QNetworkReplyクラスのオブジェクトを通じて結果を返す。
  2. タイマの設定
    QTimerクラスを使用して、指定した間隔ごとにスロット(関数)を実行する。
    このスロット内でHTTPリクエストを発行する。
  3. マルチスレッドの実装
    Qtのマルチスレッド処理の方法はいくつか存在する。
    しかし、QNetworkAccessManagerクラス自身は非同期処理を行うため、マルチスレッドを明示的に扱う必要はない。
    もし、ダウンロード処理のみを別のスレッドで行う場合は、QThreadクラスまたはQtConcurrentクラスを使用して、その中でQNetworkAccessManagerクラスのオブジェクトを作成および使用する。
 #include <QCoreApplication>
 #include <QTimer>
 #include <QNetworkAccessManager>
 #include <QNetworkRequest>
 #include <QNetworkReply>
 #include <QObject>
 
 class Downloader : public QObject {
    Q_OBJECT
 
 private:
    QNetworkAccessManager m_Manager;
    QTimer m_Timer;
 
 public:
    Downloader() {
       connect(&m_Timer, &QTimer::timeout, this, &Downloader::startDownload);
       connect(&m_Manager, &QNetworkAccessManager::finished, this, &Downloader::downloadFinished);
       m_Timer.start(60000);  // 1分ごと
    }
 
 private slots:
    void startDownload() {
        QUrl url("https://www.google.com");
        QNetworkRequest request(url);
        m_Manager.get(request);
    }
 
    void downloadFinished(QNetworkReply *reply) {
       if (reply->error()) {
           qDebug() << "Download error:" << reply->errorString();
           return;
       }
 
       QString data = reply->readAll();
 
       qDebug() << data;
    }
 };
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    Downloader downloader;
 
    return a.exec();
 }


以下の例では、ダウンロード処理を実行するために専用のスレッドを作成して、メインスレッドはタイマを使用して定期的にこのダウンロード処理をトリガしている。
1分ごとにDownloader::downloadメソッドを呼び出し、ダウンロードが完了するとダウンロードしたコンテンツがコンソールに表示される。

※注意
実際の設計では、適切なエラーハンドリングとリソース管理 (例: QThread::quitとQThread::waitを実行してスレッドを適切に終了させる処理)が必要となる。

 #include <QNetworkAccessManager>
 #include <QNetworkRequest>
 #include <QNetworkReply>
 #include <QThread>
 #include <QTimer>
 #include <QUrl>
 #include <QObject>
 
 class Downloader : public QObject {
    Q_OBJECT
 
 private:
    QNetworkAccessManager m_Manager;
 
 public:
    Downloader(QObject *parent = nullptr) : QObject(parent) {
       connect(&m_Manager, &QNetworkAccessManager::finished, this, &Downloader::onDownloadFinished);
    }
 
    void download(const QUrl &url) {
       QNetworkRequest request(url);
       m_Manager.get(request);
    }
 
 signals:
    void downloadFinished(const QString &result);
 
 private slots:
    void onDownloadFinished(QNetworkReply *reply) {
       if (reply->error()) {
          std::cerr << QString("Download error : %1").arg(reply->errorString()).toStdString() << std::endl;
          emit downloadFinished(reply->errorString());
       }
       else {
          QString data = reply->readAll();
          emit downloadFinished(data);
       }
       reply->deleteLater();
    }
 };
 
 int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);
 
    QThread downloaderThread;
    Downloader downloader;
    downloader.moveToThread(&downloaderThread);
    downloaderThread.start();
 
    QTimer timer;
    QObject::connect(&timer, &QTimer::timeout, [&downloader](){
       downloader.download(QUrl("http://www.google.com"));
    });
 
    QObject::connect(&downloader, &Downloader::downloadFinished, [](const QString &result){
       std::cout << QString("Downloaded content : %1").arg(result).toStdString() << std::endl;
    });
 
    // 1分ごとにダウンロード
    timer.start(60000);
 
    return app.exec();
 }


非同期で取得 (タイムアウト設定あり)

以下の例では、QNetworkAccessManagerクラスを使用して、Webページのデータを非同期に取得して、QTimerクラスでタイムアウトを実装している。
QTimer::timeoutシグナルを使用して、指定した時間が経過した後に接続を中断 (reply->abort()) することにより、タイムアウト処理を行っている。

また、QNetworkReply::finishedシグナルを使用して、リクエストの完了を検知して、結果を出力している。

 #include <QNetworkAccessManager>
 #include <QNetworkReply>
 #include <QTimer>
 
 // QNetworkAccessManagerのインスタンスを作成
 QNetworkAccessManager manager;
 
 // Webページを取得するためのリクエストを作成
 QNetworkRequest request(QUrl("http://www.example.com"));
 
 // リクエストを送信し、応答を受け取るためのQNetworkReplyオブジェクトを取得
 QNetworkReply *reply = manager.get(request);
 
 // タイマオブジェクトを生成して、3秒後にタイムアウトするように設定
 QTimer timer;
 timer.setSingleShot(true);
 timer.start(3000); // 3秒後にタイムアウト
 
 // タイマがタイムアウトした場合の処理を記述
 QObject::connect(&timer, &QTimer::timeout, [&]() {
        if (reply->isRunning()) {
           // リクエストがまだ完了していない場合、接続を中断
           reply->abort();
           std::cerr << QString("タイムアウトしました").toStdString() << std::endl;
        }
 });
 
 // リクエストが完了した場合の処理を接続
 QObject::connect(reply, &QNetworkReply::finished, [&]() {
        if (reply->error() == QNetworkReply::NoError) {
           // リクエストが成功して、エラーがない場合は成功
           QByteArray responseData = reply->readAll();
           std::cout << QString("受信データ : %1").arg(responseData).toStdString() << std::endl;
        }
        else if (reply->error() == QNetworkReply::OperationCanceledError) {
           std::cerr << QString("リクエストがキャンセルされました").toStdString() << std::endl;
        }
        else {
           std::cerr << QString("エラー : %1").arg(reply->errorString()).toStdString() << std::endl;
        }
 
        reply->deleteLater();
 });


非同期で取得 (マルチスレッド)

以下の例では、マルチスレッドで2つの異なるWebサイト (https://www.example1.com および https://www.example2.com) とTCP/IP通信を行っている。

QtConcurrent::runメソッドを使用して、2つの異なるWebサイトとの通信を並行して行い、
接続を確立した後、各サーバにHTTP GETリクエストを送信する。

また、このサンプルコードでは、以下に示すような仕様がある。

  • SSL証明書の検証
    QSslSocket::VerifyPeerモードを設定して、ピア証明書の検証を有効にする。
    onEncryptedスロットにより、ピア証明書の有効性を追加で確認している。
    接続を確立した場合、ピア証明書の有効性を確認する。
  • ネットワークエラーとタイムアウトの処理
    接続タイムアウト (10秒) を設定している。
    onErrorスロットでエラーをログに記録して、接続を切断する。
  • バッファリングの実装
    受信したデータをバッファリングしている。
    onReadyReadスロットにより、バッファからデータを行単位で処理する。
    完全なレスポンスを受信した時に接続を切断する。
 // NetworkManager.hファイル
 
 #include <QCoreApplication>
 #include <QSslSocket>
 #include <QSslCertificate>
 #include <QtConcurrent>
 #include <QFuture>
 #include <QTimer>
 #include <QDebug>
 
 class NetworkManager : public QObject
 {
    Q_OBJECT
 
 private:
    QByteArray m_buffer;
 
 public:
    explicit NetworkManager(QObject *parent = nullptr) : QObject(parent) {}
 
    void connectToHostEncrypted(const QString &hostName, quint16 port)
    {
       QSslSocket *socket = new QSslSocket(this);
 
       connect(socket, &QSslSocket::encrypted, this, &NetworkManager::onEncrypted);
       connect(socket, &QSslSocket::disconnected, this, &NetworkManager::onDisconnected);
       connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::errorOccurred),
               this, &NetworkManager::onError);
       connect(socket, &QSslSocket::readyRead, this, &NetworkManager::onReadyRead);
       connect(socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors),
               this, &NetworkManager::onSslErrors);
 
       // Set up SSL configuration
       QSslConfiguration sslConfig = socket->sslConfiguration();
       sslConfig.setPeerVerifyMode(QSslSocket::VerifyPeer);
       socket->setSslConfiguration(sslConfig);
 
       // Set up timeout
       QTimer *timer = new QTimer(this);
       timer->setSingleShot(true);
       connect(timer, &QTimer::timeout, this, [=]() {
               if (socket->state() != QAbstractSocket::ConnectedState) {
                  qDebug() << "Connection timeout for" << hostName;
                  socket->abort();
               }
       });
       timer->start(10000);  // 10 seconds timeout
 
       socket->connectToHostEncrypted(hostName, port);
    }
 
 private slots:
    void onEncrypted()
    {
       QSslSocket *socket = qobject_cast<QSslSocket*>(sender());
       qDebug() << "Encrypted connection established with:" << socket->peerName();
 
       // Verify certificate
       QSslCertificate peerCert = socket->peerCertificate();
       if (peerCert.isNull()) {
          qDebug() << "Invalid peer certificate for" << socket->peerName();
          socket->disconnectFromHost();
          return;
       }
 
       // Send HTTP GET request
       QString request = "GET / HTTP/1.1\r\nHost: " + socket->peerName() + "\r\n\r\n";
       socket->write(request.toUtf8());
    }
 
    void onDisconnected()
    {
       QSslSocket *socket = qobject_cast<QSslSocket*>(sender());
       qDebug() << "Disconnected from:" << socket->peerName();
       socket->deleteLater();
    }
 
    void onError(QAbstractSocket::SocketError socketError)
    {
       QSslSocket *socket = qobject_cast<QSslSocket*>(sender());
       qDebug() << "Error occurred for" << socket->peerName() << ":" << socket->errorString();
       socket->disconnectFromHost();
    }
 
    void onReadyRead()
    {
       QSslSocket *socket = qobject_cast<QSslSocket*>(sender());
       m_buffer.append(socket->readAll());
 
       // Process the buffer
       while (m_buffer.size() > 0) {
          // Find the end of a line
          int endOfLine = m_buffer.indexOf("\r\n");
          if (endOfLine == -1) {
             // We don't have a complete line yet
             break;
          }
 
          // Extract and process the line
          QByteArray line = m_buffer.left(endOfLine);
          m_buffer = m_buffer.mid(endOfLine + 2);
 
          qDebug() << "Received data from" << socket->peerName() << ":" << line;
       }
 
       // If we've received the entire response, disconnect
       if (m_buffer.contains("\r\n\r\n")) {
          socket->disconnectFromHost();
       }
    }
 
    void onSslErrors(const QList<QSslError> &errors)
    {
       QSslSocket *socket = qobject_cast<QSslSocket*>(sender());
       qDebug() << "SSL Errors occurred for" << socket->peerName() << ":";
       for (const QSslError &error : errors) {
          qDebug() << error.errorString();
       }
 
       // In a production environment, you might want to handle specific errors differently
       // For demonstration purposes, we're disconnecting on any SSL error
       socket->disconnectFromHost();
    }
 };


 // main.cppファイル
 
 #include "NetworkManager.h"
 
 int main(int argc, char *argv[])
 {
    QCoreApplication a(argc, argv);
 
    NetworkManager manager;
 
    QFuture<void> future1 = QtConcurrent::run([&manager]() {
       manager.connectToHostEncrypted("www.example1.com", 443);
    });
 
    QFuture<void> future2 = QtConcurrent::run([&manager]() {
       manager.connectToHostEncrypted("www.example2.com", 443);
    });
 
    future1.waitForFinished();
    future2.waitForFinished();
 
    return a.exec();
 }



HTTPリクエスト / HTTPレスポンス (同期的)

同期的に取得 1

QNetworkAccessManagerクラスを使用してGETリクエストを送信した後、レスポンスを同期的に待機する場合、
QEventLoopクラスを使用してレスポンスが完了するまでメインスレッドをブロックする。
HTTPレスポンスを受信した後、エラーチェックを行い、必要に応じてレスポンスを処理する。

ただし、同期的なHTTPリクエストはUIスレッドでブロックするため、長時間の処理や大量のリクエストの場合は非推奨である。
代わりに、非同期的なHTTPリクエストを使用することが推奨される。

 #include <QNetworkAccessManager>
 #include <QNetworkRequest>
 #include <QNetworkReply>
 
 QNetworkAccessManager manager;
 
 // 同期的なGETリクエスト
 QNetworkReply *reply = manager.get(QNetworkRequest(QUrl("https://www.google.com")));
 
 // レスポンス待機
 QEventLoop loop;
 QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
 loop.exec();
 
 if (reply->error() == QNetworkReply::NoError) {
    qDebug() << "Success!";
    qDebug() << "Response:" << reply->readAll();
 }
 else {
    qDebug() << "Error:" << reply->errorString();
 }
 
 reply->deleteLater();


同期的に取得 2

非公式な方法ではあるが、QNetworkRequestクラスのSynchronousRequestAttributeアトリビューションを有効にすることで実現することもできる。

 #include <QNetworkAccessManager>
 #include <QNetworkRequest>
 #include <QNetworkReply>
 
 QNetworkAccessManager manager;
 QNetworkRequest request(QUrl(QStringLiteral("https://www.google.com")));
 request.setAttribute(QNetworkRequest::SynchronousRequestAttribute, true);
 
 // 同期的なGETリクエスト
 QNetworkReply *reply = manager.get(request);
 
 if (reply->error() == QNetworkReply::NoError) {
    qDebug() << "Success!";
    qDebug() << "Response:" << reply->readAll();
 }
 else {
    qDebug() << "Error:" << reply->errorString();
 }
 
 reply->deleteLater();


同期的に取得 (タイムアウト設定あり)

以下の例では、QNetworkAccessManagerクラスを使用してHTTPリクエストを送信して、レスポンスを待機している。
QEventLoopQTimerを使用して、レスポンスを受信する、または、3秒経過するまで待機する。
3秒経過してもレスポンスを受信しない場合は、タイムアウトとして処理する。

この方法により、非同期APIを使用しながらも同期的な振る舞いを実現することができる。

 #include <QNetworkAccessManager>
 #include <QNetworkRequest>
 #include <QNetworkReply>
 #include <QTimer>
 #include <QEventLoop>
 
 QNetworkAccessManager manager;
 QNetworkRequest request(QUrl("http://www.example.com"));
 QNetworkReply *reply = manager.get(request);
 
 // イベントループを作成
 QEventLoop loop;
 
 // タイマオブジェクトを生成して、3秒後にタイムアウトするように設定
 QTimer timer;
 timer.setSingleShot(true);
 
 // タイマのタイムアウトシグナルとイベントループのquitメソッドを接続
 QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
 
 // レスポンスが完了したらイベントループを終了するように接続
 QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
 
 // タイマを開始 (3秒後にタイムアウト)
 timer.start(3000);
 
 // イベントループを開始 (リクエストが完了する、または、タイムアウトするまで待機)
 loop.exec();
 
 if (timer.isActive()) {
    // タイマがアクティブな場合、リクエストが成功しているとする
    timer.stop(); // タイマを停止
    if (reply->error() == QNetworkReply::NoError) {
       QByteArray responseData = reply->readAll();
       std::cout << QString("Response received : %1").arg(responseData).toStdString() << std::endl;
    }
    else {
       std::cerr << QString("エラー : %1").arg(reply->errorString()).toStdString() << std::endl;
    }
 }
 else {
    // タイマが非アクティブな場合、タイムアウトが発生しているものとする
    std::cerr << QString("エラー : タイムアウト").toStdString() << std::endl;
 }
 
 reply->deleteLater();



POST

URLエンコード

POSTリクエストにてデータを送信する場合、データ内容に&#+、半角スペース等が存在する可能性があるため、URLエンコードする必要がある。

 QString message = "& + #";
 QByteArray encoded = QUrl::toPercentEncoding(message.toUtf8());


また、Content-Typeヘッダにはapplication/x-www-form-urlencodedを指定する必要がある。

 // ContentTypeHeaderをHTTPリクエストに設定
 QNetworkRequest request(QUrl("https://www.example.com"));
 
 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
 request.setHeader(QNetworkRequest::ContentLengthHeader, QString::number(encoded.length()));