QUdpSocket and QDataStream
-
Hi :-)
I have a program with server-client communication via JSON data. I use TCP for this, and encapsulate the JSON data in QDataStreams, according to the (sadly inofficial) simple chat example. If I got it right, this is to ensure that the data is correct and complete, after a QDataStream transaction could be finished successfully.
Recently, I added a second small server that answers UDP broadcasts. The purpose is that a client can discover a server, it broadcasts a JSON message and the server answers with it's socket address.
Now, UDP is unreliable and connectionless. I wonder if encapsulating the JSON data in a QDataStream is also meaningful if an UDP socket is used, or if a datagram that is smaller than the maximum packet size of 65,507 bytes will be delivered as-is (or not at all) anyway.
Thanks for all enlightenment ;-)
-
Hi :-)
I have a program with server-client communication via JSON data. I use TCP for this, and encapsulate the JSON data in QDataStreams, according to the (sadly inofficial) simple chat example. If I got it right, this is to ensure that the data is correct and complete, after a QDataStream transaction could be finished successfully.
Recently, I added a second small server that answers UDP broadcasts. The purpose is that a client can discover a server, it broadcasts a JSON message and the server answers with it's socket address.
Now, UDP is unreliable and connectionless. I wonder if encapsulating the JSON data in a QDataStream is also meaningful if an UDP socket is used, or if a datagram that is smaller than the maximum packet size of 65,507 bytes will be delivered as-is (or not at all) anyway.
Thanks for all enlightenment ;-)
@l3u_ said in QUdpSocket and QDataStream:
I wonder if encapsulating the JSON data in a QDataStream is also meaningful if an UDP socket
It is. But it does nothing to guarantee the integrity of the data. The easiest way to do that is to send after the main message a checksum of the data itself. Using this example as a starting point (which I think is what you refer to in your question), you could change
ChatClient::sendMessage
to:void ChatClient::sendMessage(const QString &text) { if (text.isEmpty()) return; // We don't send empty messages // create a QDataStream operating on the socket QDataStream clientStream(m_clientSocket); // set the version so that programs compiled with different versions of Qt can agree on how to serialise clientStream.setVersion(QDataStream::Qt_5_7); // Create the JSON we want to send QJsonObject message; message["type"] = QStringLiteral("message"); message["text"] = text; // change from here on const QByteArray dataToSend = QJsonDocument(message).toJson(QJsonDocument::Compact); clientStream << dataToSend << QCryptographicHash::hash(dataToSend,QCryptographicHash::Md5); }
and change
ChatClient::onReadyRead
tovoid ChatClient::onReadyRead() { // prepare a container to hold the UTF-8 encoded JSON we receive from the socket QByteArray jsonData; QByteArray jsonDataCheckSum; //changed here // create a QDataStream operating on the socket QDataStream socketStream(m_clientSocket); // set the version so that programs compiled with different versions of Qt can agree on how to serialise socketStream.setVersion(QDataStream::Qt_5_7); // start an infinite loop for (;;) { // we start a transaction so we can revert to the previous state in case we try to read more data than is available on the socket socketStream.startTransaction(); // we try to read the JSON data socketStream >> jsonData >> jsonDataCheckSum; if (socketStream.commitTransaction()) { //changed from here if(QCryptographicHash::hash(jsonData ,QCryptographicHash::Md5)!=jsonDataCheckSum){ // the data was corrupted! continue; } // to here // we successfully read some data // we now need to make sure it's in fact a valid JSON QJsonParseError parseError; // we try to create a json document with the data we received const QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); if (parseError.error == QJsonParseError::NoError) { // if the data was indeed valid JSON if (jsonDoc.isObject()) // and is a JSON object jsonReceived(jsonDoc.object()); // parse the JSON } // loop and try to read more JSONs if they are available } else { // the read failed, the socket goes automatically back to the state it was in before the transaction started // we just exit the loop and wait for more data to become available break; } } }
-
@l3u_ said in QUdpSocket and QDataStream:
I wonder if encapsulating the JSON data in a QDataStream is also meaningful if an UDP socket
It is. But it does nothing to guarantee the integrity of the data. The easiest way to do that is to send after the main message a checksum of the data itself. Using this example as a starting point (which I think is what you refer to in your question), you could change
ChatClient::sendMessage
to:void ChatClient::sendMessage(const QString &text) { if (text.isEmpty()) return; // We don't send empty messages // create a QDataStream operating on the socket QDataStream clientStream(m_clientSocket); // set the version so that programs compiled with different versions of Qt can agree on how to serialise clientStream.setVersion(QDataStream::Qt_5_7); // Create the JSON we want to send QJsonObject message; message["type"] = QStringLiteral("message"); message["text"] = text; // change from here on const QByteArray dataToSend = QJsonDocument(message).toJson(QJsonDocument::Compact); clientStream << dataToSend << QCryptographicHash::hash(dataToSend,QCryptographicHash::Md5); }
and change
ChatClient::onReadyRead
tovoid ChatClient::onReadyRead() { // prepare a container to hold the UTF-8 encoded JSON we receive from the socket QByteArray jsonData; QByteArray jsonDataCheckSum; //changed here // create a QDataStream operating on the socket QDataStream socketStream(m_clientSocket); // set the version so that programs compiled with different versions of Qt can agree on how to serialise socketStream.setVersion(QDataStream::Qt_5_7); // start an infinite loop for (;;) { // we start a transaction so we can revert to the previous state in case we try to read more data than is available on the socket socketStream.startTransaction(); // we try to read the JSON data socketStream >> jsonData >> jsonDataCheckSum; if (socketStream.commitTransaction()) { //changed from here if(QCryptographicHash::hash(jsonData ,QCryptographicHash::Md5)!=jsonDataCheckSum){ // the data was corrupted! continue; } // to here // we successfully read some data // we now need to make sure it's in fact a valid JSON QJsonParseError parseError; // we try to create a json document with the data we received const QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); if (parseError.error == QJsonParseError::NoError) { // if the data was indeed valid JSON if (jsonDoc.isObject()) // and is a JSON object jsonReceived(jsonDoc.object()); // parse the JSON } // loop and try to read more JSONs if they are available } else { // the read failed, the socket goes automatically back to the state it was in before the transaction started // we just exit the loop and wait for more data to become available break; } } }
@VRonin Thanks for the detailed example! But doesn't the QDataStream itself add some checksum to the data? I thought it would do … because I tested my server/client connection with netem, really f**king up the connection with corrupted packets, packet loss, duplication, delay and do on, and the only thing that happened was that it took longer. But everything still worked. Or was that due to TCP being used?
-
@l3u_ said in QUdpSocket and QDataStream:
I wonder if encapsulating the JSON data in a QDataStream is also meaningful if an UDP socket
It is. But it does nothing to guarantee the integrity of the data. The easiest way to do that is to send after the main message a checksum of the data itself. Using this example as a starting point (which I think is what you refer to in your question), you could change
ChatClient::sendMessage
to:void ChatClient::sendMessage(const QString &text) { if (text.isEmpty()) return; // We don't send empty messages // create a QDataStream operating on the socket QDataStream clientStream(m_clientSocket); // set the version so that programs compiled with different versions of Qt can agree on how to serialise clientStream.setVersion(QDataStream::Qt_5_7); // Create the JSON we want to send QJsonObject message; message["type"] = QStringLiteral("message"); message["text"] = text; // change from here on const QByteArray dataToSend = QJsonDocument(message).toJson(QJsonDocument::Compact); clientStream << dataToSend << QCryptographicHash::hash(dataToSend,QCryptographicHash::Md5); }
and change
ChatClient::onReadyRead
tovoid ChatClient::onReadyRead() { // prepare a container to hold the UTF-8 encoded JSON we receive from the socket QByteArray jsonData; QByteArray jsonDataCheckSum; //changed here // create a QDataStream operating on the socket QDataStream socketStream(m_clientSocket); // set the version so that programs compiled with different versions of Qt can agree on how to serialise socketStream.setVersion(QDataStream::Qt_5_7); // start an infinite loop for (;;) { // we start a transaction so we can revert to the previous state in case we try to read more data than is available on the socket socketStream.startTransaction(); // we try to read the JSON data socketStream >> jsonData >> jsonDataCheckSum; if (socketStream.commitTransaction()) { //changed from here if(QCryptographicHash::hash(jsonData ,QCryptographicHash::Md5)!=jsonDataCheckSum){ // the data was corrupted! continue; } // to here // we successfully read some data // we now need to make sure it's in fact a valid JSON QJsonParseError parseError; // we try to create a json document with the data we received const QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); if (parseError.error == QJsonParseError::NoError) { // if the data was indeed valid JSON if (jsonDoc.isObject()) // and is a JSON object jsonReceived(jsonDoc.object()); // parse the JSON } // loop and try to read more JSONs if they are available } else { // the read failed, the socket goes automatically back to the state it was in before the transaction started // we just exit the loop and wait for more data to become available break; } } }
-
@VRonin I think the code you posted only works for a QTcpSocket. I can't do
QDataStream socketStream(m_clientSocket)
with a QUdpSocket, I have to read the data usingreadDatagram()
…@l3u_ said in QUdpSocket and QDataStream:
I have to read the data using readDatagram()
You don't if you use
connectToHost
but given you are reporting the problem I'll assume you don't want to go down that route.
What you can do is usewhile(hasPendingDatagrams())
andreadDatagram
to store the data on a temporary buffer (aQByteArray
) before using datastream on the buffer and useQByteArray::mid
to discard bytes that were already processed -
@l3u_ said in QUdpSocket and QDataStream:
I have to read the data using readDatagram()
You don't if you use
connectToHost
but given you are reporting the problem I'll assume you don't want to go down that route.
What you can do is usewhile(hasPendingDatagrams())
andreadDatagram
to store the data on a temporary buffer (aQByteArray
) before using datastream on the buffer and useQByteArray::mid
to discard bytes that were already processedOh, I thought I couldn't even
connectToHost
with an UDP socket … thanks for pointing this out ;-)But I think I can't use it anyway, because the infrastructure of what I do is quite simple: The client does an UDP broadcast with "Is there a server out there?". So I can't connectToHost here, because I don't know it yet. If the server receives such a broadcast, it answers with "Here I am, this is my socket address" and sends the data back to the sender's address.
So, speaking of the answer, I actually could do a
connectToHost
, but only for this one … and at the moment, both the server and the client are derived from the same abstract UDP communication class, so I would have to write more code just to handle half of the (frugal) communication with a "real" connection …