【问题标题】:How to properly abort a network request in Qt?如何在 Qt 中正确中止网络请求?
【发布时间】:2019-04-20 20:16:30
【问题描述】:

我有一个对网络执行请求并解析数据的类。如何正确实现它的请求中止?

想象一下我有这样一门课:

class MyClass
{
public:
...
    void doRequest()
    {
        m_reply = m_manager.get(...);

        QEventLoop waitForResponse;
        connect(m_reply, &QNetworkReply::finished, &waitForResponse, &QEventLoop::quit);
        waitForResponse.exec();

        // Check if request was aborted (otherwise, application will crash)
        if (m_reply == nullptr)
            return;

        // Check for network errors, write result to m_data and delete m_reply;
        ...
    }
    void abort()
    {
        if (m_reply != nullptr)
            m_reply->abort();
    }
    QString data()
    {
        return m_data;
    }
...
private:
    QNetworkAccessManager *m_manager;
    QPiinter<QNetworkReply> m_reply;
    QString m_data;
}

这是一个按下按钮的例子:

class MainWindow : public QMainWindow
{
...
private slots:
    MainWindow::on_myButton_pressed()
    {
        m_myClass->abort();
        m_myClass->doRequest();
        ui->myTextEdit->setText(m_myClass->data());
    }

private:
    MyClass m_myClass;
}

当你按下按钮时,如果之前的请求没有完成,那么它就会被取消。这行得通。但在这种情况下,新请求将数据写入QTextEdit 并退出函数,然后旧请求从它自己的循环返回并再次将相同的m_data 写入QTextEdit

正确吗?也许有更正确的方法来实现这一点?

【问题讨论】:

  • 等一下...抱歉,没听清楚:您是否将 QTextEdit 文本设置了两次?但为什么?你不能在设置m_data之前检查m_reply是否被中止吗?
  • @MasterAler,是的,我有两次setText。我有error() 方法,它返回doRequest() 中发生的错误枚举。我可以在取消后设置错误,它会起作用,但它看起来很奇怪......在这种情况下,我将拥有带有有效数据的 MyClass 和来自error()Error::Aborted。这看起来不是很清楚,至于我……
  • 作为一个类设计问题,这看起来不太清楚,这是真的。我会使用 QtConcurrent::run + QFuture 之类的东西而不是事件循环,这样一键点击 == 一项任务。但是使用当前代码,您也可以在成功请求后重置错误状态(甚至使用您自己的枚举),以便在任意时刻存在一对有效且符合要求的数据和错误状态。
  • 不幸的是,由于QEventLoop,被取消的旧请求比成功的请求执行得晚。因此,我将有一个有效的m_data(使用新查询生成)和error(),它从旧查询返回错误。还有我的问题。想过QtConcurrent,但是不知道怎么和QNetworkReply集成,已经是异步的了。
  • 那么它看起来像一个设计缺陷。您有一个按钮,它发送 多个 独立请求,但它们由具有一个共同状态的 一个 类实例处理。不好。可能有不同的解决方案,但一般来说,每个请求可能应该有一个 MyClass 实例。顺便说一句,在异步调用中使用异步类似乎没有问题,afaik。

标签: c++ qt networking


【解决方案1】:

嵌套事件循环是the root of all evil。编写像doRequest 这样的函数要简单得多,而无需向用户假装它是一个同步函数。似乎您已经跟踪了调用abort() 时发生的复杂控制流,并且您了解了对doRequest() 的后续调用如何由于重新进入事件循环而最终成为嵌套调用。如果您多次重新启动请求,您的堆栈将看起来像(堆栈向下增长):

1.  main function
2.  main event loop
3.  [...] (Qt functions)
4.  MainWindow::on_myButton_pressed()
5.  MyClass::doRequest()
6.  QEventLoop::exec()
7.  [...] (Qt functions)
8.  MainWindow::on_myButton_pressed()
9.  MyClass::doRequest()
10. QEventLoop::exec()
11. [...] (Qt functions)
12. MainWindow::on_myButton_pressed()   and so on...

MainWindow::on_myButton_pressed() 的每个调用都需要在其嵌套事件循环退出时调用ui-&gt;myTextEdit-&gt;setText()

如果您需要在特定操作完成时执行某些操作,则另一种方法是使您的函数完全异步并依赖信号/插槽。这是您要实现的目标的最小实现:

#include <QtNetwork>
#include <QtWidgets>

/// A class responsible for communication with the web backend
class Backend : public QObject {
  Q_OBJECT
public:
  explicit Backend(QObject *parent = nullptr)
      : QObject(parent), m_reply(nullptr) {}
public slots:
  void doRequest() {
    // abort and delete ongoing request (if any)
    if (m_reply) {
      m_reply->abort();
      delete m_reply;
      m_reply = nullptr;
    }
    emit textUpdated(QString());
    // send request
    QUrl url("http://wtfismyip.com/text");
    QNetworkRequest request(url);
    m_reply = m_manager.get(request);
    // when the request is finished,
    QObject::connect(m_reply, &QNetworkReply::finished, [this] {
      // if the request ended successfully, read the received ip into m_lastData
      if (m_reply->error() == QNetworkReply::NoError)
        m_lastData = QString::fromUtf8(m_reply->readAll());
      // otherwise, emit errorOccured() signal (if the request has not been
      // actively canceled)
      else if (m_reply->error() != QNetworkReply::OperationCanceledError)
        emit errorOccured(m_reply->errorString());
      // in all cases, emit updateText and do cleanup
      emit textUpdated(m_lastData);
      m_reply->deleteLater();
      m_reply = nullptr;
    });
  }

  void abort() {
    if (m_reply != nullptr)
      m_reply->abort();
  }

signals:
  void textUpdated(const QString &);
  void errorOccured(const QString &);

private:
  QNetworkAccessManager m_manager;
  QNetworkReply *m_reply;
  QString m_lastData;
};

/// A minimal widget that contains a QPushButton and a QLabel
class Widget : public QWidget {
  Q_OBJECT
public:
  explicit Widget(QWidget *parent = nullptr) : QWidget(parent) {
    m_layout.addWidget(&m_pushButton);
    m_layout.addWidget(&m_label);

    connect(&m_pushButton, &QPushButton::clicked, this, &Widget::buttonClicked);
  }

signals:
  void buttonClicked();

public slots:
  void updateText(const QString &text) { m_label.setText(text); }
  void showError(const QString &error) {
    QMessageBox::warning(this, tr("Error"), error);
  }

private:
  QVBoxLayout m_layout{this};
  QPushButton m_pushButton{"Retrieve Name"};
  QLabel m_label;
};

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

  Backend backend;
  Widget widget;

  // connect components
  QObject::connect(&backend, &Backend::textUpdated, &widget,
                   &Widget::updateText);
  QObject::connect(&backend, &Backend::errorOccured, &widget,
                   &Widget::showError);
  QObject::connect(&widget, &Widget::buttonClicked, &backend,
                   &Backend::doRequest);
  widget.show();

  return a.exec();
}

#include "main.moc"

【讨论】:

  • 很遗憾,如果doRequest() 内部有多个网络请求需要在循环中顺序处理,则此方法将不起作用:( 在这种情况下我需要做什么?
  • @Shatur95,该方法可以通过使用状态进行扩展,请参阅this answer 示例。针对此类问题的generalconsensus 是为了避免嵌套事件循环(无论是通过QEventLoop 还是QApplication::processEvents()),因为从长远来看它们往往会导致更多问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-07-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-25
  • 2021-11-17
  • 1970-01-01
相关资源
最近更新 更多