Сигнал Qt QTcpSocket() readReady никогда не срабатывает (слот никогда не вызывается) в многопоточном серверном приложении. Метод waitForReadyRead() работает нормально

Я пишу многопоточный TcpServer (каждый клиент в своем потоке), используя QTcpServer и QTcpSocket. Клиентское приложение работает правильно и отправляет данные каждые 3 секунды, но сигнал readReady() никогда не срабатывает, то есть моя функция receive_data() никогда не вызывается. При использовании socket->waitForReadyRead() и вызове receive_data() все работает нормально. Пожалуйста, взгляните на приведенный ниже код, возможно, я допустил ошибку с функциональностью moveToThread/connect, которую предлагает Qt.

Клиент.ч

#ifndef CLIENT_H
#define CLIENT_H

#include <QThread>
#include <QTcpSocket>
#include <QHostAddress>
#include "PacketDefinitions.h"
#include "tcpserver.h"

class Client : public QObject
{
    Q_OBJECT
public:
    explicit Client(int socket,TcpServer *parent,bool auto_disconnect = true);
    ~Client();
    bool isGameServer(){return is_gameserver;}
    GameServerPacket getGameServerData(){return gameserver;}
    void run();
private:
    QTcpSocket* client;
    TcpServer *parent_server;
    int socket;
    GameServerPacket gameserver;
    ClientPacket clientdata;
    bool is_gameserver;
    bool auto_disconnect;
    QHostAddress client_ip;
    quint16 client_port;
signals:
    void disconnected(Client *);
private slots:
    void remove_from_clientlist();
    void receive_data();
    void display_error(QAbstractSocket::SocketError error);
};

#endif // CLIENT_H

Клиент.cpp

#include "client.h"
#include "PacketDefinitions.h"
#include "time.h"
#include <iostream>

Client::Client(int _socket, TcpServer *parent,bool _auto_disconnect)
{
    auto_disconnect = _auto_disconnect;
    parent_server = parent;
    is_gameserver = false;
    socket = _socket;
}

void Client::run(){ 
    client = new QTcpSocket();

    if(client->setSocketDescriptor(socket) == false){
        std::cout << client->errorString().toStdString() << std::endl;
        remove_from_clientlist();
        return;
    }

    connect(client,SIGNAL(disconnected()),this,SLOT(remove_from_clientlist()));
    if(connect(client,SIGNAL(readyRead()),this,SLOT(receive_data()),Qt::DirectConnection) == false) return;
    connect(client,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(display_error(QAbstractSocket::SocketError)));

    client_ip = client->peerAddress();
    client_port = client->peerPort();

    std::cout << "New incomming connection " << client->peerAddress().toString().toStdString() << ":" << client->peerPort() << std::endl;

    //this works fine
//    while(client->waitForReadyRead()){
//        receive_data();
//    }
}

void Client::receive_data(){
       QDataStream stream(client);
       stream.setVersion(QDataStream::Qt_5_2);
       quint32 magic; stream >> magic;
       //interpret data
       if(magic == GAMESERVER_MAGIC){
           is_gameserver = true;
           gameserver.Read(stream);
           gameserver.port = client_port;
           gameserver.ip = client_ip;
           time(&(gameserver.last_update));
           parent_server->add_server(gameserver.ip.toString(),gameserver);
           std::cout << "GameServer " << gameserver.name << " registerd" << std::endl;
       }else if(magic == CLIENT_MAGIC){
           is_gameserver = false;
           clientdata.Read(stream);
           //get nearby servers
           GameServerListPacket server_list = parent_server->getServerList(clientdata);
           QDataStream outstream(client);
           server_list.Write(outstream);
           std::cout << "Sending ServerList(" << server_list.server_count << ") to " << client->peerAddress().toString().toStdString() << std::endl;
           if(auto_disconnect){
               //client->flush();
               client->waitForBytesWritten();
           }

       }else{
           std::cout << "Unknown package " << magic << std::endl;
       }

       //not enough data read, somthing is wrong, just for debugging
       if(client->bytesAvailable()> 0)  std::cout << "BytesAvailable " << client->bytesAvailable() << std::endl;

    if(auto_disconnect) remove_from_clientlist();//close the connection once the serverlist was deployed
}

В TcpServer.cpp add_client() вызывается, когда QTcpServer выдает newConnection():

void TcpServer::add_client(){
    while(server->hasPendingConnections()){
        QTcpSocket *socket = 0;
        if(thread_pool.size() < max_connections && (socket = server->nextPendingConnection())){
            QThread *thread = new QThread();
            Client * client = new Client(socket->socketDescriptor(),this,auto_disconnect);
            client->moveToThread(thread);
            client->run();
            thread->start();
            connect(client,SIGNAL(disconnected(Client*)),this,SLOT(remove_client(Client*)));
            WRITELOCK(thread_pool.insert(client,thread));
        }
    }
}

порядок вызова client->run() и thread->start() не имеет значения. Некоторое время назад код (не этот точный код) работал нормально, но я не могу вспомнить, что я изменил, что привело к сбою. Любая помощь приветствуется!

Заранее спасибо Фабиан

Редактировать 1:

Я получил от QTcpServer и повторно реализовал void incomingConnection(qintptr socketDescriptor), который отлично работает. Я не использую QThreadPool, это просто QMap, а remove_client(Client*) закрывает QTcpSocket, останавливает поток и удаляет его с карты. В Linux все работает нормально, в Windows я получаю следующую ошибку: QSocketNotifier: уведомители сокетов не могут быть отключены из другого потока Ошибка ASSERT в QCoreApplication::sendEvent: "Невозможно отправить события объектам, принадлежащим другому потоку....

Снимок экрана

Вызвано этим remove_client(Client*)

void TcpServer::remove_client(Client *client){
    //disconnect(client,SIGNAL(disconnected(Client*)),this,SLOT(remove_client(Client*)));
    lock.lockForWrite();    
    QMap<Client*,QThread*>::iterator itr = thread_pool.find(client);
    if(itr != thread_pool.end()){
        //delete itr.key(); causes the problem on windows
        itr.value()->quit();
        itr.value()->wait();
        delete itr.value();
        thread_pool.erase(itr);
    }
    lock.unlock();
}

Где и как я должен освободить объект Client? Если бы я использовал QThreadPool, у меня не было бы возможности перебирать клиентов, если я хочу отправить сообщение более чем одному клиенту. Я мог бы использовать список/карту, содержащую только Client *, но тогда QThreadPool может удалить их для меня прямо перед тем, как я захочу получить к нему доступ. Какие-либо предложения?


person Fabian    schedule 26.05.2014    source источник


Ответы (2)


Существует проблема с тем, как вы перемещаете свой клиентский объект в новый поток. На самом деле Client::run выполняется в том же потоке, что и TcpServer::add_client. Также клиент QTcpSocket остается в потоке по умолчанию, а его контейнер (класс Client) перемещается в новый поток. Поэтому соединение с типом Qt::DirectConnection не работает. Попробуй это:

class Client : public QObject
{
    Q_OBJECT
    ...
public slots:
    void run();
    ...
}

Client::Client(int _socket, TcpServer *parent,bool _auto_disconnect)
{
    ...
    client = new QTcpSocket(this);
}

void Client::run()
{ 
   ...
   connect(client, SIGNAL(readyRead()), this, SLOT(receive_data()));
   ...
}

И вот как вы должны переместить своего клиента в новый поток:

void TcpServer::add_client()
{
    ...
    QThread *thread = new QThread();
    Client * client = new Client(socket->socketDescriptor(),this,auto_disconnect);
    client->moveToThread(thread);
    connect(thread, SIGNAL(started()), client, SLOT(run()));
    thread->start();
    ...
}
person hank    schedule 26.05.2014
comment
Спасибо за быстрый и подробный ответ. Все работает как положено! - person Fabian; 27.05.2014

В вашем коде есть ряд ошибок.

1. У вас есть два объекта QTcpSocket, пытающихся собрать данные из одного и того же базового дескриптора сокета. Кажется, вы используете первый только для того, чтобы получить доступ к значению дескриптора сокета, которое вы затем передаете своему классу Client. Вы можете в конечном итоге потерять данные, потому что не сможете определить, какой сокет будет получать какие данные из операционной системы.

Если вы создаете производный класс от QTcpServer, скорее переопределите QTcpServer::incomingConnection(qintptr socketDescriptor) вместо существующей функции TcpServer::add_client(). Поскольку эта защищенная функция вызывается один раз для каждого нового соединения, вам не нужно устанавливать какие-либо соединения с сигналом newConnection(), а также вам не нужно зацикливаться, пока ожидаются новые соединения. У вас также будет только один QTcpSocket, подключенный к каждому дескриптору сокета, поэтому вы не потеряете данные.

2. Похоже, вы используете QThreadPool для управления потоками. Если вы сделаете Client производным классом от QRunnable (не берите того, что при множественном наследовании QObject, QObject всегда должно быть первым), вам не нужно проверять максимальное количество соединений, и вы можете исключить всю QThread обшивку котлов.

Принимая во внимание 1. и 2., ваша функция TcpServer::add_client() будет заменена на:

void TcpServer::incomingConnection(qintptr socketDescriptor){
    Client * client = new Client(socketDescriptor,this,auto_disconnect);
    connect(client,SIGNAL(disconnected(Client*)),this,SLOT(remove_client(Client*)));
    QThreadPool::globalInstance()->start(client);
}

С QThreadPool нет необходимости проверять, достигнуто ли максимальное количество потоков или нет. Если достигнут максимум, любые новые вызовы start() будут помещаться в очередь следующего соединения до тех пор, пока поток не станет доступным.

3. Причина, по которой ваш сокет не читает никаких данных, если вы не вызываете waitForReadyRead(), заключается в том, что вы выполняете функцию run() в основном потоке, создаете локальный сокет в основном потоке, вы создаете DirectConnection с экземпляром Client, а затем перемещаете client в другую тему. У вас не может быть прямых соединений между потоками.

Вам нужно будет добавить локальный QEventLoop к вашей функции run() для обработки всех событий и сигналов вашего нового потока, но не забудьте подключить сигналы к слоту quit() вашего цикла, чтобы функция run() завершилась, иначе ваш поток будет продолжать работать вечно.

person RobbieE    schedule 26.05.2014