diff --git a/examples/testapp/coaphandler.cpp b/examples/testapp/coaphandler.cpp index 1e50117..4e69672 100644 --- a/examples/testapp/coaphandler.cpp +++ b/examples/testapp/coaphandler.cpp @@ -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, QVectorerrorReceived() == 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) diff --git a/examples/testapp/coaphandler.h b/examples/testapp/coaphandler.h index fd64eb6..893b517 100644 --- a/examples/testapp/coaphandler.h +++ b/examples/testapp/coaphandler.h @@ -62,6 +62,7 @@ public Q_SLOTS: void onFinished(QCoapReply *reply); void onNotified(QCoapReply *reply, QCoapMessage message); void onDiscovered(QCoapDiscoveryReply *reply, QVector resources); + void onResponseToMulticast(QCoapReply *reply, const QCoapMessage& message); void onError(QCoapReply *reply, QtCoap::Error error); private: diff --git a/src/coap/qcoapclient.cpp b/src/coap/qcoapclient.cpp index 4b36b80..60d13c0 100644 --- a/src/coap/qcoapclient.cpp +++ b/src/coap/qcoapclient.cpp @@ -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, QPointer(reply)), Q_ARG(QCoapConnection *, connection)); diff --git a/src/coap/qcoapclient.h b/src/coap/qcoapclient.h index 9a49e5d..7b0c636 100644 --- a/src/coap/qcoapclient.h +++ b/src/coap/qcoapclient.h @@ -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: diff --git a/src/coap/qcoapinternalrequest.cpp b/src/coap/qcoapinternalrequest.cpp index 0d1d62b..a28c2d4 100644 --- a/src/coap/qcoapinternalrequest.cpp +++ b/src/coap/qcoapinternalrequest.cpp @@ -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(responseDelay)); +} + /*! \internal Decode the \a uri provided and returns a QCoapOption. diff --git a/src/coap/qcoapinternalrequest_p.h b/src/coap/qcoapinternalrequest_p.h index 0959f47..e08953e 100644 --- a/src/coap/qcoapinternalrequest_p.h +++ b/src/coap/qcoapinternalrequest_p.h @@ -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; diff --git a/src/coap/qcoapprotocol.cpp b/src/coap/qcoapprotocol.cpp index 2edbf69..f2e430f 100644 --- a/src/coap/qcoapprotocol.cpp +++ b/src/coap/qcoapprotocol.cpp @@ -96,6 +96,19 @@ void QCoapProtocol::sendRequest(QPointer 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(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 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(uri.port())); + const auto& hostAddress = host.isEmpty() ? uri.host() : host; + request->connection()->sendRequest(requestFrame, hostAddress, static_cast(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 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 reply) { + return reply->senderAddress() != sender; + }), replies.end()); + } + std::stable_sort(std::begin(replies), std::end(replies), [](QSharedPointer a, QSharedPointer 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)); diff --git a/src/coap/qcoapprotocol.h b/src/coap/qcoapprotocol.h index 38fc375..afe9930 100644 --- a/src/coap/qcoapprotocol.h +++ b/src/coap/qcoapprotocol.h @@ -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: diff --git a/src/coap/qcoapprotocol_p.h b/src/coap/qcoapprotocol_p.h index 6651f37..24fa744 100644 --- a/src/coap/qcoapprotocol_p.h +++ b/src/coap/qcoapprotocol_p.h @@ -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); diff --git a/tests/auto/qcoapclient/tst_qcoapclient.cpp b/tests/auto/qcoapclient/tst_qcoapclient.cpp index 01563ca..266832e 100644 --- a/tests/auto/qcoapclient/tst_qcoapclient.cpp +++ b/tests/auto/qcoapclient/tst_qcoapclient.cpp @@ -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(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(spyMulticastResponse.at(0).at(1)); + QCOMPARE(message1.payload(), "Reply1"); + + QCoapMessage message2 = qvariant_cast(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(spyMulticastResponse.at(0).at(1)); + QCOMPARE(message1.payload(), "Reply1Reply2"); + + QCoapMessage message2 = qvariant_cast(spyMulticastResponse.at(1).at(1)); + QCOMPARE(message2.payload(), "Reply3Reply4"); +} + QTEST_MAIN(tst_QCoapClient) #include "tst_qcoapclient.moc" diff --git a/tests/auto/qcoapinternalrequest/tst_qcoapinternalrequest.cpp b/tests/auto/qcoapinternalrequest/tst_qcoapinternalrequest.cpp index 98c1e49..5166d69 100644 --- a/tests/auto/qcoapinternalrequest/tst_qcoapinternalrequest.cpp +++ b/tests/auto/qcoapinternalrequest/tst_qcoapinternalrequest.cpp @@ -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("url"); + QTest::addColumn("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