End to End GUI Development with Qt5
上QQ阅读APP看书,第一时间看更新

Web Requests

If you haven't worked with the HTTP protocol before, it boils down to a conversation between a client and a server consisting of requests and responses.  For example, we can make a request to www.bbc.co.uk in our favorite web browser, and we will receive a response containing various news items and articles. In the get() method of our NetworkAccessManager wrapper, we reference a QNetworkRequest (our request to a server) and a QNetworkReply (the server's response back to us). While we won’t directly hide QNetworkRequest and QNetworkReply behind their own independent interfaces, we will take the concept of a web request and corresponding response and create an interface and implementation for that interaction. Still in cm-lib/source/networking, create an interface header file i-web-request.h:

#ifndef IWEBREQUEST_H
#define IWEBREQUEST_H
#include <QUrl>
namespace cm { namespace networking {
class IWebRequest { public: IWebRequest(){} virtual ~IWebRequest(){}
virtual void execute() = 0; virtual bool isBusy() const = 0; virtual void setUrl(const QUrl& url) = 0; virtual QUrl url() const = 0; };
}}
#endif

The key piece of information for an HTTP request is the URL the request is to be sent to, represented by the QUrl Qt class. We provide an url() accessor and setUrl() mutator for the property. The other two methods are to check whether the isBusy() web request object is making a request or receiving a response and also to execute() or send the request to the network. Again, with the interface in place, let’s move on straight to the implementation with a new WebRequest class in the same folder.

web-request.h:

#ifndef WEBREQUEST_H
#define WEBREQUEST_H
#include <QList> #include <QObject> #include <QSslError> #include <networking/i-network-access-manager.h> #include <networking/i-web-request.h>
namespace cm { namespace networking {
class WebRequest : public QObject, public IWebRequest { Q_OBJECT
public: WebRequest(QObject* parent, INetworkAccessManager* networkAccessManager, const QUrl& url); WebRequest(QObject* parent = nullptr) = delete; ~WebRequest();
public: void execute() override; bool isBusy() const override; void setUrl(const QUrl& url) override; QUrl url() const override;
signals: void error(QString message); void isBusyChanged(); void requestComplete(int statusCode, QByteArray body); void urlChanged();
private slots: void replyDelegate(); void sslErrorsDelegate( const QList<QSslError>& _errors );
private: class Implementation; QScopedPointer<Implementation> implementation; };
}}
#endif

web-request.cpp:

#include "web-request.h"

#include <QMap>
#include <QNetworkReply>
#include <QNetworkRequest>
namespace cm { namespace networking { // Private Implementation
static const QMap<QNetworkReply::NetworkError, QString> networkErrorMapper = { {QNetworkReply::ConnectionRefusedError, "The remote server refused the connection (the server is not accepting requests)."}, /* ...section shortened in print for brevity...*/ {QNetworkReply::UnknownServerError, "An unknown error related to the server response was detected."} };
class WebRequest::Implementation { public: Implementation(WebRequest* _webRequest, INetworkAccessManager* _networkAccessManager, const QUrl& _url) : webRequest(_webRequest) , networkAccessManager(_networkAccessManager) , url(_url) { }
WebRequest* webRequest{nullptr}; INetworkAccessManager* networkAccessManager{nullptr}; QUrl url {}; QNetworkReply* reply {nullptr};
public: bool isBusy() const { return isBusy_; }
void setIsBusy(bool value) { if (value != isBusy_) { isBusy_ = value; emit webRequest->isBusyChanged(); } }
private: bool isBusy_{false}; }; }
namespace networking { // Structors WebRequest::WebRequest(QObject* parent, INetworkAccessManager* networkAccessManager, const QUrl& url) : QObject(parent) , IWebRequest() { implementation.reset(new WebRequest::Implementation(this, networkAccessManager, url)); }
WebRequest::~WebRequest() { } }
namespace networking { // Methods void WebRequest::execute() { if(implementation->isBusy()) { return; } if(!implementation->networkAccessManager->isNetworkAccessible()) { emit error("Network not accessible"); return; }
implementation->setIsBusy(true); QNetworkRequest request; request.setUrl(implementation->url); implementation->reply = implementation->networkAccessManager->get(request);
if(implementation->reply != nullptr) { connect(implementation->reply, &QNetworkReply::finished, this, &WebRequest::replyDelegate); connect(implementation->reply, &QNetworkReply::sslErrors, this, &WebRequest::sslErrorsDelegate); } }
bool WebRequest::isBusy() const { return implementation->isBusy(); }
void WebRequest::setUrl(const QUrl& url) { if(url != implementation->url) { implementation->url = url; emit urlChanged(); } }
QUrl WebRequest::url() const { return implementation->url; } }
namespace networking { // Private Slots void WebRequest::replyDelegate() { implementation->setIsBusy(false);
if (implementation->reply == nullptr) { emit error("Unexpected error - reply object is null"); return; }
disconnect(implementation->reply, &QNetworkReply::finished, this, &WebRequest::replyDelegate); disconnect(implementation->reply, &QNetworkReply::sslErrors, this, &WebRequest::sslErrorsDelegate);
auto statusCode = implementation->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); auto responseBody = implementation->reply->readAll(); auto replyStatus = implementation->reply->error(); implementation->reply->deleteLater();
if (replyStatus != QNetworkReply::NoError) { emit error(networkErrorMapper[implementation->reply->error()]); }
emit requestComplete(statusCode, responseBody); }
void WebRequest::sslErrorsDelegate(const QList<QSslError>& errors) { QString sslError; for (const auto& error : errors) { sslError += error.errorString() + "n"; } emit error(sslError); }
}}

The implementation looks more complicated than it is purely because of the lengthy error code map. In the event of some sort of problem, Qt will report the error using an enumerator. The purpose of the map is simply to match the enumerator to a human readable error description that we can present to the user or write to the console or a log file.

In addition to the interface methods, we also have a handful of signals that we can use to tell any interested observers about events that have happened:

  • error() will be emitted in the event of a problem and will pass the error description as a parameter
  • isBusyChanged() is fired when a request starts or finishes and the request becomes either busy or idle
  • requestComplete() is emitted when the response has been received and processed and will contain the HTTP status code and an array of bytes representing the response body
  • urlChanged() will be fired when the URL is updated

We also have a couple of private slots that will be the delegates for processing a reply and handling any SSL errors. They are connected to signals on the QNetworkReply object when we execute a new request and disconnected again when we receive the reply.

The meat of the implementation is really two methods—execute() to send the request and replyDelegate() to process the response.

When executing, we first ensure that we are not already busy executing another request and then check with the network access manager that we have an available connection. Assuming that we do, we then set the busy flag and construct a QNetworkRequest using the currently set URL. We then pass the request onto our network access manager (injected as an interface, so we can change its behavior) and finally, we connect our delegate slots and wait for a response.

When we receive the reply, we unset the busy flag and disconnect our slots before reading the response details we are interested in, principally the HTTP status code and response body. We check that the reply completed successfully (note that a “negative” HTTP response code in the ranges 4xx or 5xx still count as successfully complete requests in this context) and emit the details for any interested parties to capture and process.