Add support for multicast CoAP requests

Change-Id: I9cf6d4f97c863c232b17bc8e560c6b62c3f39624
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io>
Reviewed-by: Alex Blasche <alexander.blasche@qt.io>
This commit is contained in:
Sona Kurazyan 2019-02-11 17:35:15 +01:00
parent 18cccdd7ba
commit c0c8dfbfeb
11 changed files with 289 additions and 16 deletions

View File

@ -39,6 +39,8 @@ CoapHandler::CoapHandler(QObject *parent) : QObject(parent)
{
connect(&m_coapClient, &QCoapClient::finished, this, &CoapHandler::onFinished);
connect(&m_coapClient, &QCoapClient::error, this, &CoapHandler::onError);
connect(&m_coapClient, &QCoapClient::responseToMulticastReceived,
this, &CoapHandler::onResponseToMulticast);
}
bool CoapHandler::runGet(const QUrl &url)
@ -108,6 +110,14 @@ void CoapHandler::onDiscovered(QCoapDiscoveryReply *reply, QVector<QCoapResource
qDebug() << "Discovered resource: " << res.path() << res.title();
}
void CoapHandler::onResponseToMulticast(QCoapReply *reply, const QCoapMessage& message)
{
if (reply->errorReceived() == QtCoap::NoError)
qDebug() << "Got a response for multicast request: " << message.payload();
else
qDebug() << "Multicast request failed";
}
void CoapHandler::onError(QCoapReply *reply, QtCoap::Error error)
{
if (reply)

View File

@ -62,6 +62,7 @@ public Q_SLOTS:
void onFinished(QCoapReply *reply);
void onNotified(QCoapReply *reply, QCoapMessage message);
void onDiscovered(QCoapDiscoveryReply *reply, QVector<QCoapResource> resources);
void onResponseToMulticast(QCoapReply *reply, const QCoapMessage& message);
void onError(QCoapReply *reply, QtCoap::Error error);
private:

View File

@ -268,6 +268,17 @@ QtCoap::Error QtCoap::responseCodeError(QtCoap::ResponseCode code)
\sa error(), QCoapReply::finished(), QCoapReply::error()
*/
/*!
\fn void QCoapClient::responseToMulticastReceived(QCoapReply *reply,
const QCoapMessage& message)
This signal is emitted when a unicast response to a multicast request
arrives. The \a reply parameter contains a pointer to the reply that has just
been received, and \a message contains the payload and the message details.
\sa error(), QCoapReply::finished(), QCoapReply::error()
*/
/*!
\fn void QCoapClient::error(QCoapReply *reply, QtCoap::Error error)
@ -336,6 +347,8 @@ QCoapClient::QCoapClient(QCoapProtocol *protocol, QCoapConnection *connection, Q
connect(d->protocol, &QCoapProtocol::finished,
this, &QCoapClient::finished);
connect(d->protocol, &QCoapProtocol::responseToMulticastReceived,
this, &QCoapClient::responseToMulticastReceived);
connect(d->protocol, &QCoapProtocol::error,
this, &QCoapClient::error);
}
@ -678,6 +691,15 @@ bool QCoapClientPrivate::send(QCoapReply *reply)
return false;
}
// According to https://tools.ietf.org/html/rfc7252#section-8.1,
// multicast requests MUST be Non-confirmable.
if (QHostAddress(reply->url().host()).isMulticast()
&& reply->request().type() == QCoapMessage::Confirmable) {
qWarning("QCoapClient: Failed to send request, "
"multicast requests must be non-confirmable.");
return false;
}
QMetaObject::invokeMethod(protocol, "sendRequest", Qt::QueuedConnection,
Q_ARG(QPointer<QCoapReply>, QPointer<QCoapReply>(reply)),
Q_ARG(QCoapConnection *, connection));

View File

@ -45,6 +45,7 @@ class QCoapRequest;
class QCoapProtocol;
class QCoapConnection;
class QCoapSecurityConfiguration;
class QCoapMessage;
class QIODevice;
class QCoapClientPrivate;
@ -90,6 +91,7 @@ public:
Q_SIGNALS:
void finished(QCoapReply *reply);
void responseToMulticastReceived(QCoapReply *reply, const QCoapMessage& message);
void error(QCoapReply *reply, QtCoap::Error error);
protected:

View File

@ -65,6 +65,10 @@ QCoapInternalRequest::QCoapInternalRequest(QObject *parent) :
d->maxTransmitWaitTimer = new QTimer(this);
connect(d->maxTransmitWaitTimer, &QTimer::timeout,
[this]() { emit maxTransmissionSpanReached(this); });
d->multicastExpireTimer = new QTimer(this);
connect(d->multicastExpireTimer, &QTimer::timeout, this,
[this]() { emit multicastRequestExpired(this); });
}
/*!
@ -485,16 +489,33 @@ void QCoapInternalRequest::restartTransmission()
/*!
\internal
Marks the transmission as not running, after a successful reception, or an
error. It resets the retranmission count and stops all timeout timers.
Starts the timer for keeping the multicast request \e alive.
*/
void QCoapInternalRequest::startMulticastTransmission()
{
Q_ASSERT(isMulticast());
Q_D(QCoapInternalRequest);
d->multicastExpireTimer->start();
}
/*!
\internal
Marks the transmission as not running, after a successful reception or an
error. It resets the retransmission count if needed and stops all timeout timers.
*/
void QCoapInternalRequest::stopTransmission()
{
Q_D(QCoapInternalRequest);
d->transmissionInProgress = false;
d->retransmissionCounter = 0;
d->maxTransmitWaitTimer->stop();
d->timeoutTimer->stop();
if (isMulticast()) {
d->multicastExpireTimer->stop();
} else {
d->transmissionInProgress = false;
d->retransmissionCounter = 0;
d->maxTransmitWaitTimer->stop();
d->timeoutTimer->stop();
}
}
/*!
@ -556,6 +577,17 @@ bool QCoapInternalRequest::isObserveCancelled() const
return d->observeCancelled;
}
/*!
\internal
Returns \c true if the request is multicast, returns \c false otherwise.
*/
bool QCoapInternalRequest::isMulticast() const
{
const QHostAddress hostAddress(targetUri().host());
return hostAddress.isMulticast();
}
/*!
\internal
Returns the value of the retransmission counter.
@ -638,6 +670,24 @@ void QCoapInternalRequest::setMaxTransmissionWait(int duration)
d->maxTransmitWaitTimer->setInterval(duration);
}
/*!
\internal
Sets the timeout interval in milliseconds for keeping the multicast request
\e alive.
In the unicast case, receiving a response means that the request is finished.
In the multicast case it is not known how many responses will be received, so
the response, along with its token, will be kept for
NON_LIFETIME + MAX_LATENCY + MAX_SERVER_RESPONSE_DELAY time, as suggested
in \l {RFC 7390 - Section 2.5}.
*/
void QCoapInternalRequest::setMulticastTimeout(uint responseDelay)
{
Q_D(QCoapInternalRequest);
d->multicastExpireTimer->setInterval(static_cast<int>(responseDelay));
}
/*!
\internal
Decode the \a uri provided and returns a QCoapOption.

View File

@ -85,6 +85,7 @@ public:
QtCoap::Method method() const;
bool isObserve() const;
bool isObserveCancelled() const;
bool isMulticast() const;
QCoapConnection *connection() const;
int retransmissionCounter() const;
void setMethod(QtCoap::Method method);
@ -94,12 +95,15 @@ public:
void setTargetUri(QUrl targetUri);
void setTimeout(uint timeout);
void setMaxTransmissionWait(int timeout);
void setMulticastTimeout(uint responseDelay);
void restartTransmission();
void startMulticastTransmission();
void stopTransmission();
Q_SIGNALS:
void timeout(QCoapInternalRequest*);
void maxTransmissionSpanReached(QCoapInternalRequest*);
void multicastRequestExpired(QCoapInternalRequest*);
protected:
QCoapOption uriHostOption(const QUrl &uri) const;
@ -123,6 +127,7 @@ public:
int retransmissionCounter = 0;
QTimer *timeoutTimer = nullptr;
QTimer *maxTransmitWaitTimer = nullptr;
QTimer *multicastExpireTimer = nullptr;
bool observeCancelled = false;
bool transmissionInProgress = false;

View File

@ -96,6 +96,19 @@ void QCoapProtocol::sendRequest(QPointer<QCoapReply> reply, QCoapConnection *con
internalRequest->setMaxTransmissionWait(maxTransmitWait());
connect(reply, &QCoapReply::finished, this, &QCoapProtocol::finished);
if (internalRequest->isMulticast()) {
connect(internalRequest.data(), &QCoapInternalRequest::multicastRequestExpired, this,
[this](QCoapInternalRequest *request) {
Q_D(QCoapProtocol);
d->onMulticastRequestExpired(request);
});
// The timeout interval is chosen based on
// https://tools.ietf.org/html/rfc7390#section-2.5
internalRequest->setMulticastTimeout(nonConfirmLifetime()
+ static_cast<uint>(maxLatency())
+ maxServerResponseDelay());
}
// Set a unique Message Id and Token
QCoapMessage *requestMessage = internalRequest->message();
internalRequest->setMessageId(d->generateUniqueMessageId());
@ -136,9 +149,11 @@ void QCoapProtocol::sendRequest(QPointer<QCoapReply> reply, QCoapConnection *con
/*!
\internal
Encodes and sends the given \a request to the server.
Encodes and sends the given \a request to the server. If \a host is not empty,
sends the request to \a host, instead of using the host address from the request.
The \a host parameter is relevant for multicast blockwise transfers.
*/
void QCoapProtocolPrivate::sendRequest(QCoapInternalRequest *request) const
void QCoapProtocolPrivate::sendRequest(QCoapInternalRequest *request, const QString& host) const
{
Q_Q(const QCoapProtocol);
Q_ASSERT(QThread::currentThread() == q->thread());
@ -148,10 +163,15 @@ void QCoapProtocolPrivate::sendRequest(QCoapInternalRequest *request) const
return;
}
request->restartTransmission();
if (request->isMulticast())
request->startMulticastTransmission();
else
request->restartTransmission();
QByteArray requestFrame = request->toQByteArray();
QUrl uri = request->targetUri();
request->connection()->sendRequest(requestFrame, uri.host(), static_cast<quint16>(uri.port()));
const auto& hostAddress = host.isEmpty() ? uri.host() : host;
request->connection()->sendRequest(requestFrame, hostAddress, static_cast<quint16>(uri.port()));
}
/*!
@ -191,6 +211,29 @@ void QCoapProtocolPrivate::onRequestMaxTransmissionSpanReached(QCoapInternalRequ
onRequestError(request, QtCoap::TimeOutError);
}
/*!
\internal
This slot is called when the multicast request expires, meaning that no
more responses are expected for the multicast \a request. As a result of this
call, the request token is \e {freed up} and the \l finished() signal is emitted.
*/
void QCoapProtocolPrivate::onMulticastRequestExpired(QCoapInternalRequest *request)
{
Q_ASSERT(request->isMulticast());
request->stopTransmission();
QPointer<QCoapReply> userReply = userReplyForToken(request->token());
if (userReply) {
QMetaObject::invokeMethod(userReply, "_q_setFinished", Qt::QueuedConnection,
Q_ARG(QtCoap::Error, QtCoap::NoError));
} else {
qWarning().nospace() << "QtCoap: Reply for token '" << request->token()
<< "' is not registered, reply is null.";
}
forgetExchange(request);
}
/*!
\internal
@ -268,7 +311,8 @@ void QCoapProtocolPrivate::onFrameReceived(const QByteArray &data, const QHostAd
return;
}
request->stopTransmission();
if (!request->isMulticast())
request->stopTransmission();
addReply(request->token(), reply);
if (QtCoap::isError(reply->responseCode())) {
@ -293,9 +337,13 @@ void QCoapProtocolPrivate::onFrameReceived(const QByteArray &data, const QHostAd
} else if (reply->hasMoreBlocksToReceive()) {
request->setToRequestBlock(reply->currentBlockNumber() + 1, reply->blockSize());
request->setMessageId(generateUniqueMessageId());
sendRequest(request);
// In case of multicast blockwise transfers, according to
// https://tools.ietf.org/html/rfc7959#section-2.8, further blocks should be retrieved
// via unicast requests. So instead of using the multicast request address, we need
// to use the sender address for getting the next blocks.
sendRequest(request, sender.toString());
} else {
onLastMessageReceived(request);
onLastMessageReceived(request, sender);
}
}
@ -395,7 +443,8 @@ QCoapInternalRequest *QCoapProtocolPrivate::findRequestByMessageId(quint16 messa
associated QCoapReply and emits the
\l{QCoapProtocol::finished(QCoapReply*)}{finished(QCoapReply*)} signal.
*/
void QCoapProtocolPrivate::onLastMessageReceived(QCoapInternalRequest *request)
void QCoapProtocolPrivate::onLastMessageReceived(QCoapInternalRequest *request,
const QHostAddress &sender)
{
Q_ASSERT(request);
if (!request || !isRequestRegistered(request))
@ -423,6 +472,16 @@ void QCoapProtocolPrivate::onLastMessageReceived(QCoapInternalRequest *request)
// Merge payloads for blockwise transfers
if (replies.size() > 1) {
// In multicast case, multiple hosts will reply to the same multicast request.
// We are interested only in replies coming from the sender.
if (request->isMulticast()) {
replies.erase(std::remove_if(replies.begin(), replies.end(),
[sender](QSharedPointer<QCoapInternalReply> reply) {
return reply->senderAddress() != sender;
}), replies.end());
}
std::stable_sort(std::begin(replies), std::end(replies),
[](QSharedPointer<QCoapInternalReply> a, QSharedPointer<QCoapInternalReply> b) -> bool {
return (a->currentBlockNumber() < b->currentBlockNumber());
@ -452,6 +511,9 @@ void QCoapProtocolPrivate::onLastMessageReceived(QCoapInternalRequest *request)
if (request->isObserve()) {
QMetaObject::invokeMethod(userReply, "_q_setNotified", Qt::QueuedConnection);
forgetExchangeReplies(request->token());
} else if (request->isMulticast()) {
Q_Q(QCoapProtocol);
emit q->responseToMulticastReceived(userReply, *lastReply->message());
} else {
QMetaObject::invokeMethod(userReply, "_q_setFinished", Qt::QueuedConnection,
Q_ARG(QtCoap::Error, QtCoap::NoError));

View File

@ -70,6 +70,7 @@ public:
Q_SIGNALS:
void finished(QCoapReply *reply);
void responseToMulticastReceived(QCoapReply *reply, const QCoapMessage& message);
void error(QCoapReply *reply, QtCoap::Error error);
public Q_SLOTS:

View File

@ -70,15 +70,16 @@ public:
void sendAcknowledgment(QCoapInternalRequest *request) const;
void sendReset(QCoapInternalRequest *request) const;
void sendRequest(QCoapInternalRequest *request) const;
void sendRequest(QCoapInternalRequest *request, const QString& host = QString()) const;
void onLastMessageReceived(QCoapInternalRequest *request);
void onLastMessageReceived(QCoapInternalRequest *request, const QHostAddress &sender);
void onRequestError(QCoapInternalRequest *request, QCoapInternalReply *reply);
void onRequestError(QCoapInternalRequest *request, QtCoap::Error error,
QCoapInternalReply *reply = nullptr);
void onRequestTimeout(QCoapInternalRequest *request);
void onRequestMaxTransmissionSpanReached(QCoapInternalRequest *request);
void onMulticastRequestExpired(QCoapInternalRequest *request);
void onFrameReceived(const QByteArray &data, const QHostAddress &sender);
void onConnectionError(QAbstractSocket::SocketError error);
void onRequestAborted(const QCoapToken &token);

View File

@ -72,6 +72,9 @@ private Q_SLOTS:
void discover();
void observe_data();
void observe();
void confirmableMulticast();
void multicast();
void multicast_blockwise();
};
class QCoapQUdpConnectionSocketTestsPrivate : public QCoapQUdpConnectionPrivate
@ -132,6 +135,42 @@ public:
}
};
class QCoapConnectionMulticastTests : public QCoapConnection
{
public:
~QCoapConnectionMulticastTests() override = default;
void bind(const QString &host, quint16 port) override
{
Q_UNUSED(host);
Q_UNUSED(port);
// Do nothing
}
void writeData(const QByteArray &data, const QString &host, quint16 port) override
{
Q_UNUSED(data);
Q_UNUSED(host);
Q_UNUSED(port);
// Do nothing
}
};
class QCoapClientForMulticastTests : public QCoapClient
{
public:
QCoapClientForMulticastTests() :
QCoapClient(new QCoapProtocol, new QCoapConnectionMulticastTests)
{}
QCoapConnection *connection()
{
QCoapClientPrivate *privateClient = static_cast<QCoapClientPrivate *>(d_func());
return privateClient->connection;
}
};
class Helper : public QObject
{
Q_OBJECT
@ -733,6 +772,62 @@ void tst_QCoapClient::observe()
}
}
void tst_QCoapClient::confirmableMulticast()
{
QCoapClient client;
const auto reply = client.get(QCoapRequest("224.0.1.187", QCoapMessage::Confirmable));
QVERIFY2(!reply, "Confirmable multicast request didn't fail as expected.");
}
void tst_QCoapClient::multicast()
{
QCoapClientForMulticastTests client;
QCoapRequest request = QCoapRequest(QUrl("224.0.1.187"));
request.setToken("abc");
QCoapReply *reply = client.get(request);
QVERIFY(reply);
// Simulate sending unicast responses to the multicast request
emit client.connection()->readyRead("SE\xAD/abc\xC0\xFFReply1", QHostAddress("10.20.30.40"));
emit client.connection()->readyRead("SE\xAD/abc\xC0\xFFReply2", QHostAddress("10.20.30.41"));
QSignalSpy spyMulticastResponse(&client, &QCoapClient::responseToMulticastReceived);
QTRY_COMPARE(spyMulticastResponse.count(), 2);
QCoapMessage message1 = qvariant_cast<QCoapMessage>(spyMulticastResponse.at(0).at(1));
QCOMPARE(message1.payload(), "Reply1");
QCoapMessage message2 = qvariant_cast<QCoapMessage>(spyMulticastResponse.at(1).at(1));
QCOMPARE(message2.payload(), "Reply2");
}
void tst_QCoapClient::multicast_blockwise()
{
QCoapClientForMulticastTests client;
QCoapRequest request = QCoapRequest(QUrl("224.0.1.187"));
request.setToken("abc");
QCoapReply *reply = client.get(request);
QVERIFY(reply);
QHostAddress host1("10.20.30.40");
QHostAddress host2("10.20.30.41");
// Simulate blockwise transfer responses coming from two different hosts
emit client.connection()->readyRead("SE#}abc\xC0\xB1\x1D\xFFReply1", host1);
emit client.connection()->readyRead("SE#}abc\xC0\xB1\x1D\xFFReply3", host2);
emit client.connection()->readyRead("SE#~abc\xC0\xB1%\xFFReply2", host1);
emit client.connection()->readyRead("SE#~abc\xC0\xB1%\xFFReply4", host2);
QSignalSpy spyMulticastResponse(&client, &QCoapClient::responseToMulticastReceived);
QTRY_COMPARE(spyMulticastResponse.count(), 2);
QCoapMessage message1 = qvariant_cast<QCoapMessage>(spyMulticastResponse.at(0).at(1));
QCOMPARE(message1.payload(), "Reply1Reply2");
QCoapMessage message2 = qvariant_cast<QCoapMessage>(spyMulticastResponse.at(1).at(1));
QCOMPARE(message2.payload(), "Reply3Reply4");
}
QTEST_MAIN(tst_QCoapClient)
#include "tst_qcoapclient.moc"

View File

@ -49,6 +49,8 @@ private Q_SLOTS:
void urlOptions();
void invalidUrls_data();
void invalidUrls();
void isMulticast_data();
void isMulticast();
};
void tst_QCoapInternalRequest::requestToFrame_data()
@ -290,6 +292,28 @@ void tst_QCoapInternalRequest::invalidUrls()
QVERIFY(internalRequest.message()->options().empty());
}
void tst_QCoapInternalRequest::isMulticast_data()
{
QTest::addColumn<QString>("url");
QTest::addColumn<bool>("result");
QTest::newRow("ipv4_multicast") << QString("coap://224.0.1.187") << true;
QTest::newRow("ipv4_multicast_resource") << QString("coap://224.0.1.187/path") << true;
QTest::newRow("ipv6_multicast_link_local") << "coap://[ff02::fd]" << true;
QTest::newRow("ipv6_multicast_site_local") << "coap://[ff05::fd]" << true;
QTest::newRow("not_multicast") << QString("coap://127.0.0.1") << false;
}
void tst_QCoapInternalRequest::isMulticast()
{
QFETCH(QString, url);
QFETCH(bool, result);
const QCoapRequest request(url);
const QCoapInternalRequest internalRequest(request);
QCOMPARE(internalRequest.isMulticast(), result);
}
#else
class tst_QCoapInternalRequest : public QObject