c++qtqtcpsocketqtcpserver

Spooky QTcpSocket behaviour


I have been having nightmares while debugging the next few lines of code, there must be something hidden beyond what I am seeing.

Here's a connection between a client and a server

QByteArray Client::request(QByteArray cmd)
{
    //DEBUG << "Command: " << cmd.toStdString().c_str();
    QTcpSocket *socket = new QTcpSocket(this);
    socket->connectToHost(hostAddress, port, QIODevice::ReadWrite);
    if (socket->waitForConnected(TIMEOUT))
    {
        socket->write(cmd);
        DEBUG << "Size of bytes written :" << sizeof (cmd);
    }
    else DEBUG << "Couldn't connect to socket";
    if (socket->waitForBytesWritten(TIMEOUT)) DEBUG << "Command sent";
    else DEBUG << "Couldn't write to socket";
    if (socket->waitForReadyRead(TIMEOUT))
    {
        QByteArray data = socket->readAll();
        socket->close();
        return data;
    }
    else DEBUG << "No reply from Server";
    return QByteArray();
}

That's basically it for the client, for the server, here are the snippets that matter.

class ConnectionHandler : public QTcpServer
{
    Q_OBJECT


public:
    explicit ConnectionHandler(QObject *parent = 0);
    void write(QByteArray);

protected:
    void ConnectionHandler::incomingConnection(qintptr descriptor)
{
    DEBUG << "ConnectionHandler:" << "Incoming Connection :" << descriptor;
    ConnectionThread *thread = new ConnectionThread(this, descriptor);
    connect(thread, &QThread::finished, thread, &QThread::deleteLater);
    connect(thread, &ConnectionThread::signalIncomingMessage, this, &ConnectionHandler::slotIncomingMessage);
    connect(this, &ConnectionHandler::signalOutgoingMessage, thread, &ConnectionThread::slotOutgoingMessage);
    thread->start();
}

public slots:
    void slotIncomingMessage(QByteArray);
    void slotListen(bool checked){
    if (checked)
    {
        if (!this->listen(QHostAddress::LocalHost, PORT_NUMBER)) {
            DEBUG << "ConnectionHandler:" << "Could not start the server!";
        } else {
            DEBUG << "ConnectionHandler:" << "Listening...";
        }
    } else {
        this->close();
        DEBUG << "ConnectionHandler:" << "Connection Closed!";
    }
}

signals:
    void signalOutgoingMessage(QByteArray);
};
class ConnectionThread : public QThread
{
    Q_OBJECT
public:
    ConnectionThread(QObject* parent = 0, qintptr descriptor = -1){
    if (descriptor != -1)
    {
        socket = new QTcpSocket();

        DEBUG << "ConnectionThread: Connecting to socket number" << descriptor;
        if (!socket->setSocketDescriptor(descriptor))
        {
            DEBUG << "ConnectionThread: Connection failed.";
            DEBUG << socket->errorString();
        }
        else
        {
            DEBUG << "ConnectionThread: Connected Successfully.";
            connect(socket, &QAbstractSocket::disconnected, this, &ConnectionThread::slotSocketDisconnected);
        }
    }
    else {
        DEBUG << "ConnectionThread: Please provide a descriptor for the connection thread";
    }
}
    void run() override{
    if (socket->state() != QAbstractSocket::ConnectedState)
    {
        DEBUG << "ConnectionThread: Socket is not connected!";
        DEBUG << "ConnectionThread: Closing Thread!";
        emit signalThreadError(socket->errorString());
    }
    else
    {
        DEBUG << "ConnectionThread: Socket is Connected.";
        connect(socket, &QIODevice::readyRead, this, &ConnectionThread::slotThreadReadyRead, Qt::DirectConnection);
        exec();
    }
}

signals:
    void signalThreadError(QString);
    void signalIncomingMessage(QByteArray);

public slots:
    void slotThreadReadyRead(){
    QByteArray msg = socket->readAll();

    if (!msg.isEmpty()) {
        emit signalIncomingMessage(msg);
    }

    DEBUG << "ConnectionThread: Data in:" << msg;
}
    void slotSocketDisconnected();
    void slotOutgoingMessage(QByteArray msg)
{
    if (socket != nullptr) {
        socket->write(msg);
        if (socket->waitForBytesWritten(TIMEOUT)) {
            DEBUG << "ConnectionThread: Outgoing Message: " << msg;
        } else {
            DEBUG << "ConnectionThread: Outgoing Message Timeout";
        }
    }
}

private:
    QTcpSocket *socket = nullptr;
};

What happens basically, is it sometimes receives the incoming command from the client, and most of the time it doesn't, not only that, it actually receives some commands and ignores some different ones.

Here's a spookier behavior, notice the line //DEBUG << "Command: " << cmd.toStdString().c_str(); in the Client::request(), when this line is commented out, the server actually receives some commands, when it's not, server is deaf.

here's an output when the line was commented out:

Size of bytes written : 8
Command sent
target num = 4  (a reply was received)
Size of bytes written : 8
Command sent
No reply from Server

Here's another output when the mentioned line isn't commented out:

Command:  NUMB

Size of bytes written : 8
Command sent
No reply from Server
target num = 0
Command:  TRAN

Size of bytes written : 8
Command sent
No reply from Server

Update Solving the race that happens between socket writing at the client and thread opening did it for me, thread using was unnecessary in my case here so I just dropped it. Answered by @Botje


Solution

  • The problem was caused by a race condition:

    1. (main thread) ConnectionHandler::incomingConnection creates a new ConnectionThread
    2. (main thread) ConnectionThread constructor creates a socket, which is also owned by the main thread
    3. (main thread) returns to event loop, polling for incoming data on the socket
    4. (ConnectionThread) the run method connects a slot to the readyRead signal

    If data arrives in the window between 2 and 4, the readyRead signal would not have picked it up. There are several possible solutions, but the simplest is probably to either move the socket to the ConnectionThread after construction, or delay its construction until the ConnectionThread::run method.