QNetworkAccessManager: Configurable number of HTTP1 TCP connections

Introduces new class QHttp1Configuration.

[ChangeLog][QtNetwork][QHttp1Configuration] New class.

Fixes: QTBUG-25280
Fixes: QTBUG-108215
Change-Id: Ide6cee23946e5001befb8fab34edf10b8a66e02b
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Markus Goetz 2021-09-08 09:46:29 +02:00 committed by Marc Mutz
parent a09c33e1f7
commit be05bb749e
10 changed files with 242 additions and 5 deletions

View File

@ -121,6 +121,7 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http
access/http2/huffman.cpp access/http2/huffman_p.h
access/qabstractprotocolhandler.cpp access/qabstractprotocolhandler_p.h
access/qdecompresshelper.cpp access/qdecompresshelper_p.h
access/qhttp1configuration.cpp access/qhttp1configuration.h
access/qhttp2configuration.cpp access/qhttp2configuration.h
access/qhttp2protocolhandler.cpp access/qhttp2protocolhandler_p.h
access/qhttpmultipart.cpp access/qhttpmultipart.h access/qhttpmultipart_p.h

View File

@ -0,0 +1,125 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qhttp1configuration.h"
#include "qdebug.h"
QT_BEGIN_NAMESPACE
/*!
\class QHttp1Configuration
\brief The QHttp1Configuration class controls HTTP/1 parameters and settings.
\since 6.5
\reentrant
\inmodule QtNetwork
\ingroup network
\ingroup shared
QHttp1Configuration controls HTTP/1 parameters and settings that
QNetworkAccessManager will use to send requests and process responses.
\note The configuration must be set before the first request
was sent to a given host (and thus an HTTP/1 session established).
\sa QNetworkRequest::setHttp1Configuration(), QNetworkRequest::http1Configuration(), QNetworkAccessManager
*/
class QHttp1ConfigurationPrivate : public QSharedData
{
public:
unsigned numberOfConnectionsPerHost = 6; // QHttpNetworkConnectionPrivate::defaultHttpChannelCount
};
/*!
Default constructs a QHttp1Configuration object.
*/
QHttp1Configuration::QHttp1Configuration()
: d(new QHttp1ConfigurationPrivate)
{
}
/*!
Copy-constructs this QHttp1Configuration.
*/
QHttp1Configuration::QHttp1Configuration(const QHttp1Configuration &) = default;
/*!
Move-constructs this QHttp1Configuration from \a other
*/
QHttp1Configuration::QHttp1Configuration(QHttp1Configuration &&other) noexcept
{
swap(other);
}
/*!
Copy-assigns \a other to this QHttp1Configuration.
*/
QHttp1Configuration &QHttp1Configuration::operator=(const QHttp1Configuration &) = default;
/*!
Move-assigns \a other to this QHttp1Configuration.
*/
QHttp1Configuration &QHttp1Configuration::operator=(QHttp1Configuration &&) noexcept = default;
/*!
Destructor.
*/
QHttp1Configuration::~QHttp1Configuration()
= default;
/*!
Sets number of connections (default 6) to a http(s)://host:port
\sa numberOfConnectionsPerHost
*/
void QHttp1Configuration::setNumberOfConnectionsPerHost(unsigned number)
{
if (number == 0) {
return;
}
d->numberOfConnectionsPerHost = number;
}
/*!
Returns the number of connections (default 6) to a http(s)://host:port
\sa setNumberOfConnectionsPerHost
*/
unsigned QHttp1Configuration::numberOfConnectionsPerHost() const
{
return d->numberOfConnectionsPerHost;
}
/*!
Swaps this configuration with the \a other configuration.
*/
void QHttp1Configuration::swap(QHttp1Configuration &other) noexcept
{
d.swap(other.d);
}
/*!
\fn bool QHttp1Configuration::operator==(const QHttp1Configuration &lhs, const QHttp1Configuration &rhs) noexcept
Returns \c true if \a lhs and \a rhs represent the same set of HTTP/1
parameters.
*/
/*!
\fn bool QHttp1Configuration::operator!=(const QHttp1Configuration &lhs, const QHttp1Configuration &rhs) noexcept
Returns \c true if \a lhs and \a rhs do not represent the same set of
HTTP/1 parameters.
*/
/*!
\internal
*/
bool QHttp1Configuration::isEqual(const QHttp1Configuration &other) const noexcept
{
if (d == other.d)
return true;
return d->numberOfConnectionsPerHost == other.d->numberOfConnectionsPerHost;
}
QT_END_NAMESPACE

View File

@ -0,0 +1,51 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QHTTP1CONFIGURATION_H
#define QHTTP1CONFIGURATION_H
#include <QtNetwork/qtnetworkglobal.h>
#include <QtCore/qshareddata.h>
#ifndef Q_CLANG_QDOC
QT_REQUIRE_CONFIG(http);
#endif
QT_BEGIN_NAMESPACE
class QHttp1ConfigurationPrivate;
class Q_NETWORK_EXPORT QHttp1Configuration
{
public:
QHttp1Configuration();
QHttp1Configuration(const QHttp1Configuration &other);
QHttp1Configuration(QHttp1Configuration &&other) noexcept;
QHttp1Configuration &operator=(const QHttp1Configuration &other);
QHttp1Configuration &operator=(QHttp1Configuration &&other) noexcept;
~QHttp1Configuration();
void setNumberOfConnectionsPerHost(unsigned amount);
unsigned numberOfConnectionsPerHost() const;
void swap(QHttp1Configuration &other) noexcept;
private:
QSharedDataPointer<QHttp1ConfigurationPrivate> d;
bool isEqual(const QHttp1Configuration &other) const noexcept;
friend bool operator==(const QHttp1Configuration &lhs, const QHttp1Configuration &rhs) noexcept
{ return lhs.isEqual(rhs); }
friend bool operator!=(const QHttp1Configuration &lhs, const QHttp1Configuration &rhs) noexcept
{ return !lhs.isEqual(rhs); }
};
Q_DECLARE_SHARED(QHttp1Configuration)
QT_END_NAMESPACE
#endif // QHTTP1CONFIGURATION_H

View File

@ -32,6 +32,7 @@ QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
// Note: Only used from auto tests, normal usage is via QHttp1Configuration
const int QHttpNetworkConnectionPrivate::defaultHttpChannelCount = 6;
// The pipeline length. So there will be 4 requests in flight.
@ -69,7 +70,7 @@ QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate(quint16 connectionC
QHttpNetworkConnection::ConnectionType type)
: state(RunningState), networkLayerState(Unknown),
hostName(hostName), port(port), encrypt(encrypt), delayIpv4(true),
activeChannelCount(connectionCount), channelCount(connectionCount)
channelCount(connectionCount)
#ifndef QT_NO_NETWORKPROXY
, networkProxy(QNetworkProxy::NoProxy)
#endif
@ -77,6 +78,14 @@ QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate(quint16 connectionC
, connectionType(type)
{
channels = new QHttpNetworkConnectionChannel[channelCount];
activeChannelCount = (type == QHttpNetworkConnection::ConnectionTypeHTTP2 ||
type == QHttpNetworkConnection::ConnectionTypeHTTP2Direct)
? 1 : connectionCount;
// We allocate all 6 channels even if it's an HTTP/2-enabled
// connection: in case the protocol negotiation via NPN/ALPN fails,
// we will have normally working HTTP/1.1.
Q_ASSERT(channelCount >= activeChannelCount);
}

View File

@ -145,9 +145,9 @@ class QNetworkAccessCachedHttpConnection: public QHttpNetworkConnection,
{
// Q_OBJECT
public:
QNetworkAccessCachedHttpConnection(const QString &hostName, quint16 port, bool encrypt,
QNetworkAccessCachedHttpConnection(quint16 connectionCount, const QString &hostName, quint16 port, bool encrypt,
QHttpNetworkConnection::ConnectionType connectionType)
: QHttpNetworkConnection(hostName, port, encrypt, connectionType)
: QHttpNetworkConnection(connectionCount, hostName, port, encrypt, /*parent=*/nullptr, connectionType)
{
setExpires(true);
setShareable(true);
@ -297,7 +297,7 @@ void QHttpThreadDelegate::startRequest()
if (!httpConnection) {
// no entry in cache; create an object
// the http object is actually a QHttpNetworkConnection
httpConnection = new QNetworkAccessCachedHttpConnection(urlCopy.host(), urlCopy.port(), ssl,
httpConnection = new QNetworkAccessCachedHttpConnection(http1Parameters.numberOfConnectionsPerHost(), urlCopy.host(), urlCopy.port(), ssl,
connectionType);
if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2
|| connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2Direct) {

View File

@ -26,6 +26,7 @@
#include <QNetworkReply>
#include "qhttpnetworkrequest_p.h"
#include "qhttpnetworkconnection_p.h"
#include "qhttp1configuration.h"
#include "qhttp2configuration.h"
#include <QSharedPointer>
#include <QScopedPointer>
@ -82,6 +83,7 @@ public:
qint64 removedContentLength;
QNetworkReply::NetworkError incomingErrorCode;
QString incomingErrorDetail;
QHttp1Configuration http1Parameters;
QHttp2Configuration http2Parameters;
bool isCompressed;

View File

@ -784,6 +784,7 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq
QHttpThreadDelegate *delegate = new QHttpThreadDelegate;
// Propagate Http/2 settings:
delegate->http2Parameters = request.http2Configuration();
delegate->http1Parameters = request.http1Configuration();
if (request.attribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute).isValid())
delegate->connectionCacheExpiryTimeoutSeconds = request.attribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute).toInt();

View File

@ -7,6 +7,7 @@
#include "qnetworkcookie.h"
#include "qsslconfiguration.h"
#if QT_CONFIG(http) || defined(Q_QDOC)
#include "qhttp1configuration.h"
#include "qhttp2configuration.h"
#include "private/http2protocol_p.h"
#endif
@ -439,6 +440,7 @@ public:
#endif
peerVerifyName = other.peerVerifyName;
#if QT_CONFIG(http)
h1Configuration = other.h1Configuration;
h2Configuration = other.h2Configuration;
decompressedSafetyCheckThreshold = other.decompressedSafetyCheckThreshold;
#endif
@ -454,6 +456,7 @@ public:
maxRedirectsAllowed == other.maxRedirectsAllowed &&
peerVerifyName == other.peerVerifyName
#if QT_CONFIG(http)
&& h1Configuration == other.h1Configuration
&& h2Configuration == other.h2Configuration
&& decompressedSafetyCheckThreshold == other.decompressedSafetyCheckThreshold
#endif
@ -470,6 +473,7 @@ public:
int maxRedirectsAllowed;
QString peerVerifyName;
#if QT_CONFIG(http)
QHttp1Configuration h1Configuration;
QHttp2Configuration h2Configuration;
qint64 decompressedSafetyCheckThreshold = 10ll * 1024ll * 1024ll;
#endif
@ -854,6 +858,30 @@ void QNetworkRequest::setPeerVerifyName(const QString &peerName)
}
#if QT_CONFIG(http) || defined(Q_QDOC)
/*!
\since 6.5
Returns the current parameters that QNetworkAccessManager is
using for the underlying HTTP/1 connection of this request.
\sa setHttp1Configuration
*/
QHttp1Configuration QNetworkRequest::http1Configuration() const
{
return d->h1Configuration;
}
/*!
\since 6.5
Sets request's HTTP/1 parameters from \a configuration.
\sa http1Configuration, QNetworkAccessManager, QHttp1Configuration
*/
void QNetworkRequest::setHttp1Configuration(const QHttp1Configuration &configuration)
{
d->h1Configuration = configuration;
}
/*!
\since 5.14

View File

@ -14,6 +14,7 @@ QT_BEGIN_NAMESPACE
class QSslConfiguration;
class QHttp2Configuration;
class QHttp1Configuration;
class QNetworkRequestPrivate;
class Q_NETWORK_EXPORT QNetworkRequest
@ -144,6 +145,9 @@ public:
QString peerVerifyName() const;
void setPeerVerifyName(const QString &peerName);
#if QT_CONFIG(http) || defined(Q_QDOC)
QHttp1Configuration http1Configuration() const;
void setHttp1Configuration(const QHttp1Configuration &configuration);
QHttp2Configuration http2Configuration() const;
void setHttp2Configuration(const QHttp2Configuration &configuration);

View File

@ -44,6 +44,7 @@
#include <QtNetwork/qnetworkdiskcache.h>
#include <QtNetwork/qnetworkrequest.h>
#include <QtNetwork/qnetworkreply.h>
#include <QtNetwork/QHttp1Configuration>
#include <QtNetwork/qnetworkcookie.h>
#include <QtNetwork/QNetworkCookieJar>
#include <QtNetwork/QHttpPart>
@ -65,6 +66,7 @@ Q_DECLARE_METATYPE(QSharedPointer<char>)
#endif
#include <memory>
#include <optional>
#ifdef Q_OS_UNIX
# include <sys/types.h>
@ -445,6 +447,7 @@ private Q_SLOTS:
void varyingCacheExpiry_data();
void varyingCacheExpiry();
void amountOfHttp1ConnectionsQtbug25280_data();
void amountOfHttp1ConnectionsQtbug25280();
void dontInsertPartialContentIntoTheCache();
@ -8120,10 +8123,18 @@ public:
}
};
void tst_QNetworkReply::amountOfHttp1ConnectionsQtbug25280_data()
{
QTest::addColumn<int>("amount");
QTest::addRow("default") << 6;
QTest::addRow("minimize") << 1;
QTest::addRow("increase") << 12;
}
// Also kind of QTBUG-8468
void tst_QNetworkReply::amountOfHttp1ConnectionsQtbug25280()
{
const int amount = 6;
QFETCH(const int, amount);
QNetworkAccessManager manager; // function local instance
Qtbug25280Server server(tst_QNetworkReply::httpEmpty200Response);
server.doClose = false;
@ -8131,11 +8142,16 @@ void tst_QNetworkReply::amountOfHttp1ConnectionsQtbug25280()
QUrl url(QLatin1String("http://127.0.0.1")); // not "localhost" to prevent "Happy Eyeballs"
// from skewing the counting
url.setPort(server.serverPort());
std::optional<QHttp1Configuration> http1Configuration;
if (amount != 6) // don't set if it's the default
http1Configuration.emplace().setNumberOfConnectionsPerHost(amount);
constexpr int NumRequests = 200; // send a lot more than we have sockets
int finished = 0;
std::array<std::unique_ptr<QNetworkReply>, NumRequests> replies;
for (auto &reply : replies) {
QNetworkRequest request(url);
if (http1Configuration)
request.setHttp1Configuration(*http1Configuration);
reply.reset(manager.get(request));
QObject::connect(reply.get(), &QNetworkReply::finished,
[&finished] { ++finished; });