qtqt5qtcpsocketqtcpserver

Reading from QTcpSocket fails if QTcpServer has not been closed


Here is a simple minimal working example that reproduces behavior I find weird (probably due to my misunderstanding of something):

main.cpp

#include <QApplication>
#include "dialog.h"

int main(int argc, char *argv[]) {
   QApplication app(argc, argv);
   Dialog dialog;
   dialog.show();
   return app.exec();
}

dialog.h

#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>
class QLabel;
class QTcpServer;
class QTcpSocket;

class Dialog : public QDialog {
   Q_OBJECT

public:
   Dialog(QWidget *parent = 0);

public slots:

   void acceptConnection();
   void readClientData();

private:
   QLabel *label;
   QTcpServer *tcpServer;
   QTcpSocket *socket;
};

#endif

dialog.cpp

#include <QtWidgets>
#include <QtNetwork>
#include "dialog.h"

Dialog::Dialog(QWidget *parent) : QDialog(parent) {
   label = new QLabel("Server is listening...");
   QVBoxLayout *mainLayout = new QVBoxLayout;
   mainLayout->addWidget(label);
   setLayout(mainLayout);

   tcpServer = new QTcpServer;
   connect(tcpServer, SIGNAL(newConnection()), this, SLOT(acceptConnection()));
   tcpServer->listen(QHostAddress::LocalHost, 10055);
}


void Dialog::acceptConnection() {
   socket = tcpServer->nextPendingConnection();
   connect(socket, SIGNAL(readyRead()),  this, SLOT(readClientData()));
}


void Dialog::readClientData() {

   QString data;
   while (socket->canReadLine())
    data += socket->readLine();

   label->setText(data);

   socket->close();
   tcpServer->close();
   tcpServer->deleteLater();
}

After compiling and running, I get the dialog, then I go to my browser, enter the URL http://localhost:10055 and... nothing. The server accepts the connection, but no data (HTTP headers) is read and shown in the label.

Only if I place tcpServer->close(); at the end of the acceptConnection() slot (rather than in readClientData()), the data is read normally (the label shows the header: GET /HTTP/1.1 etc.).

I don't understand it at all: why server should stop listening for the first connection to read data normally?


Solution

  • In a web request several transactions are carried out so the connection is not unique, and in your case you are waiting for a single socket to exist but in reality there can be several at the same time, and that is what is happening in your case, the Solution is to handle the socket dynamically using a sender() to get the correct socket in readClientData() as shown below:

    dialog.h

    #ifndef DIALOG_H
    #define DIALOG_H
    
    #include <QDialog>
    
    QT_BEGIN_NAMESPACE
    class QLabel;
    class QTcpServer;
    class QTcpSocket;
    QT_END_NAMESPACE
    
    class Dialog : public QDialog {
       Q_OBJECT
    
    public:
       Dialog(QWidget *parent = nullptr);
    
    private slots:
       void acceptConnection();
       void readClientData();
    
    private:
       QLabel *label;
       QTcpServer *tcpServer;
    };
    
    #endif
    

    dialog.cpp

    #include "dialog.h"
    
    #include <QLabel>
    #include <QTcpServer>
    #include <QVBoxLayout>
    #include <QTcpSocket>
    
    Dialog::Dialog(QWidget *parent) : QDialog(parent) {
        label = new QLabel("Server is listening...");
        QVBoxLayout *mainLayout = new QVBoxLayout(this);
        mainLayout->addWidget(label);
    
        tcpServer = new QTcpServer;
        connect(tcpServer, &QTcpServer::newConnection, this, &Dialog::acceptConnection);
        tcpServer->listen(QHostAddress::LocalHost, 10055);
    }
    
    
    void Dialog::acceptConnection() {
        QTcpSocket *socket = tcpServer->nextPendingConnection();
        connect(socket, &QTcpSocket::readyRead,  this, &Dialog::readClientData);
    }
    
    
    void Dialog::readClientData() {
        QTcpSocket *socket = dynamic_cast<QTcpSocket *>(sender());
        if(socket){
            QString data;
            while (socket->canReadLine())
                data += socket->readLine();
    
            label->setText(data);
    
            socket->close();
        }
    }
    

    why server should stop listening for the first connection to read data normally?

    Because before closing the server you have to process all events such as obtaining data, so you get that effect.

    Explanation:

    To better understand the operation I will add some prints:

    void Dialog::acceptConnection() {
        QTcpSocket *socket = tcpServer->nextPendingConnection();
        connect(socket, &QTcpSocket::readyRead,  this, &Dialog::readClientData);
        qDebug()<< __PRETTY_FUNCTION__<< socket;
    }
    
    
    void Dialog::readClientData() {
        QTcpSocket *socket = dynamic_cast<QTcpSocket *>(sender());
        if(socket){
    
            QString data;
            while (socket->canReadLine())
                data += socket->readLine();
    
            qDebug()<< __PRETTY_FUNCTION__<< socket<<data;
    
            label->setText(data);
             socket->close();
        }
    }
    

    Output:

    void Dialog::acceptConnection() QTcpSocket(0x7fb1e4007600)
    void Dialog::acceptConnection() QTcpSocket(0x559d7f3cb830)
    void Dialog::readClientData() QTcpSocket(0x7fb1e4007600) "GET / HTTP/1.1\r\nHost: localhost:10055\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36\r\nDNT: 1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: en-US,en;q=0.9,hu;q=0.8\r\n\r\n"
    void Dialog::readClientData() QTcpSocket(0x559d7f3cb830) "GET / HTTP/1.1\r\nHost: localhost:10055\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36\r\nDNT: 1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: en-US,en;q=0.9,hu;q=0.8\r\n\r\n"
    void Dialog::acceptConnection() QTcpSocket(0x7fb1e40071e0)
    void Dialog::readClientData() QTcpSocket(0x7fb1e40071e0) "GET / HTTP/1.1\r\nHost: localhost:10055\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36\r\nDNT: 1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: en-US,en;q=0.9,hu;q=0.8\r\n\r\n"
    

    If you notice you notice that 2 sockets have been created, why have 2 sockets been created? because a browser persists several times.

    In your first code, the socket variable will first take the value of (0x7fb1e4007600), then the second socket will set it to (0x559d7f3cb830), a time later it will call the slot readClientData() associated with (0x7fb1e4007600) but you will be reading the data of (0x559d7f3cb830) that has no data even though that call will be lost, then a third socket will come and it will do the same with the second one. That is why his method fails

    On the other hand my method does not overwrite the variable socket since it does not save it, it gets it in the slot directly

    So in conclusion, if the objects that have the signals that invoke the slot are several, as in this case it is best to obtain the object that emits it using sender().

    but some of them (for whatever reason) will return a null pointer from the sender() in readClientData()?

    If it is a slot probably not, but it can happen, for example you can call readClientData() directly so there will be no sender() or reference to any other object, so for security I verify it.