Russian Qt Forum

Qt => Многопоточное программирование, процессы => Тема начата: dead_vip от Октябрь 26, 2011, 19:08



Название: Запись сокета из разных потоков
Отправлено: dead_vip от Октябрь 26, 2011, 19:08
Делаю лабу - чат с использованием tcp-сокетов. Написал 2 программки - клиент и сервер (сервер работает в единственном потоке), все работает, но для лучшего понимания хочу переделать сервер следующим образом: онсновной поток должен только принимать данные (обрабатывать сигналы по-сути) и передавать в отдельный поток, который будет обрабатывать данные и писать в сокет. Проблема в том, что сокеты создаются в главном потоке и сигналы обрабатываются тоже в главном потоке, при создании потока для обработки туда передается указатель на сокет, в теле run() вызывается QTcpSocket.write и вываливается ошибка "QObject: Cannot create children for a parent that is in a different thread.". Не пойму что не так, ведь слоты сокета обрабатываются в том же потоке, откуда идет сигнал (в главном потоке), а в паралельных потоках я всего лишь записываю в сокет...

Я много искал в интернете по поводу того, как адекватно реализовать мою задумку, но ничего толкового так и не нашел. Везде пишут элементарные вещи типа: как ответить одному клиенту (там обычно сокет создается в паралельном потоке и висит при помощи exec(), обрабатывая сигналы), в примере threaded fortune server, который идет вместе с qt вообще тупо создается поток, в теле run создается сокет, отсылает 1 строчку и закрывает сокет, имхо это бред, а не пример.

Для тех кто так и не понял в чем вопрос, кратко его можно сформулировать: "как передать в поток список открытых сокетов (или же один сокет), чтобы он записал во все сокеты (или один) данные и уничтожился, не закрывая сокеты?"

Вот куски кода (во вложении все)
Код:
//myserver.cpp - тут отлавливаются сигналы incomingConnection и создается обьекты MyClient
//MyServer - производный от QTcpServer
void MyServer::incomingConnection(int handle)
{
    MyClient *client = new MyClient(this, &_clients);

    client->setSocket(handle);//назначить дескриптор (QTcpSocket.setSocketDescriptor())
    _clients.append(client);//добавить в QList
}

Код:
//myclient.cpp - тут (главный поток) обрабатываются сигналы
//MyClient - не производный от QTcpSocket, тут есть переменная-сокет QTcpSocket *_sok;
void MyClient::setSocket(int desc)
{
    _sok = new QTcpSocket(this);
    _sok->setSocketDescriptor(desc);
//тут были пляски с типами соединений
    connect(_sok, SIGNAL(connected()), this, SLOT(onConnect()), Qt::QueuedConnection);
    connect(_sok, SIGNAL(disconnected()), this, SLOT(onDisconnect()), Qt::QueuedConnection);
    connect(_sok, SIGNAL(readyRead()), this, SLOT(onReadyRead()), Qt::DirectConnection);
    connect(_sok, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onError(QAbstractSocket::SocketError)), Qt::QueuedConnection);
}

void MyClient::onReadyRead()
{
//считывается размер входящего блока и пока он не прийдет полностью обработка не начинается
    QDataStream in(_sok);
    if (_blockSize == 0) {
        if (_sok->bytesAvailable() < (int)sizeof(quint16))
            return;
        in >> _blockSize;
        qDebug() << "_blockSize now " << _blockSize;
    }
    if (_sok->bytesAvailable() < _blockSize)
        return;
    if (_sok->bytesAvailable() >= _blockSize)
        _blockSize = 0;
//теккущая команда серверу - второй байт в блоке
    quint8 command;
    in >> command;
    qDebug() << "received command " << command;
//все команды кроме запроса авторизации от неавторизованного пользователя игнорируются
    if (!_isAutched && command != comAutchReq)
        return;
//попытка сделать через QThread
    t *t1 = new t(NULL, this);
    t1->start();
//попытка сделать через QThreadPool
    //MyTask *task = new MyTask(this, command);
    //task->setAutoDelete(true);

    //QThreadPool::globalInstance()->start(task);
    /*
    switch(command)
    {
    //тут обработка данных идет в главном потоке, нужно ее перенести в паралельный поток (выше попытки сделать это)
    }*/

    //for (long long i = 0; i < 4000000000; ++i){}
}

Код:
//t.cpp - обработка в потоке
t::t(QObject *parent, MyClient *c) : QThread(parent)
{
//передан указатель на MyClient
    _c = c;
}

void t::run()
{
    _c->_sok->write("fgds");//QObject: Cannot create children...
    //вызов метода MyClient тот же результат
    //_c->doSendUsersOnline();//QObject: Cannot create children
    qDebug() << "gfdg";
    exec();
}



Название: Re: Запись сокета из разных потоков
Отправлено: Странник от Октябрь 26, 2011, 20:12
вызывать методы QTcpSocket можно только из того потока, в котором он живет. еще вопросы?


Название: Re: Запись сокета из разных потоков
Отправлено: BRE от Октябрь 26, 2011, 20:17
Подобные темы точно были. Поищи по форуму и найдешь решение.


Название: Re: Запись сокета из разных потоков
Отправлено: dead_vip от Октябрь 26, 2011, 22:09
вызывать методы QTcpSocket можно только из того потока, в котором он живет. еще вопросы?
Похоже так и есть... Получается палка о двух концах - я не могу делать moveToThread потому, что слоты disconnect(), readyRead() должны срабатывать в главном потоке, не могу создать еще один сокет с таким же дескриптором, и я не могу использовать уже открытый сокет в другом потоке, но почему? Именно это мне и нужно...


Название: Re: Запись сокета из разных потоков
Отправлено: BRE от Октябрь 26, 2011, 22:23
Лучше начать сначала... Что ты хочешь получить?
Основной поток принимает подключение, создает объект MyClient, дальше этот объект можно переместить в отдельную нить используя moveToThread, где он и будет работать.


Название: Re: Запись сокета из разных потоков
Отправлено: dead_vip от Октябрь 26, 2011, 22:41
Верно, но тогда сигнал readyRead будет обрабатываться в этой отдельной нити и она должна будет висеть пока не отключится клиент, это плохо и задумка не такая.
Основной поток принимает подключение, создает объект MyClient, когда срабатывает слот MyClient.onReadyRead (в основной нити), в его теле создается отделная нить, которая должна принять указатель на сокет, в который она должна записать данные и уничтожиться. При этом (после создания отдельной нити, передав туда указатель на сокет) основная нить дальше обрабатывает слоты onReadyRead, не задумываясь об ответе клиентам (это перекладывается на отдельные нити).


Название: Re: Запись сокета из разных потоков
Отправлено: BRE от Октябрь 26, 2011, 22:55
Верно, но тогда сигнал readyRead будет обрабатываться в этой отдельной нити и она должна будет висеть пока не отключится клиент, это плохо и задумка не такая.
Основной поток принимает подключение, создает объект MyClient, когда срабатывает слот MyClient.onReadyRead (в основной нити), в его теле создается отделная нить, которая должна принять указатель на сокет, в который она должна записать данные и уничтожиться. При этом (после создания отдельной нити, передав туда указатель на сокет) основная нить дальше обрабатывает слоты onReadyRead, не задумываясь об ответе клиентам (это перекладывается на отдельные нити).
Операция QTcpSocket::write на самом деле просто копирует данные во внутренний буфер. Сама отправка будет происходить асинхронно, во время работы цикла обработки событий. Дальше данные будут помещаться в буфер отправки tcp-стека и только потом резаться на пакеты и отправляться клиенту. Это я к тому, что нет смысла выполнять запись в сокет в отдельном потоке, эта операция, как правило, выполняется очень быстро.
Можно делать так: одна нить занимается подключением/чтением/записью для всех клиентов, а все долгие операции по подготовке данных для отправки - выполняются в рабочих нитках.


Название: Re: Запись сокета из разных потоков
Отправлено: Странник от Октябрь 26, 2011, 23:13
либо основной поток занимается подключением клиентов, а чтение, обработка и запись выполняются в отдельном потоке для каждого клиента. хотя в случае с чатом многопоточность не так актуальна - объемы данных как правило невелики, и особой обработки им не требуется.


Название: Re: Запись сокета из разных потоков
Отправлено: dead_vip от Октябрь 26, 2011, 23:43
Можно делать так: одна нить занимается подключением/чтением/записью для всех клиентов, а все долгие операции по подготовке данных для отправки - выполняются в рабочих нитках.
Видел туториал на ютубе, где такое реализовывали, сначала подумал зачем все так запутанно, но все-равно сделал (после обработки данных в отделной ните, она посылает сигнал основной и оттуда вызывается write(), а отдельная уничтожается), но потом рассказал эту схему преподaвателю, он сказал, что сделал бы так, как описано в первом посте, и я решил попробовать сделать именно так. Дело в том, что он ориентируется на Delphi, а там похоже там совсем другой принцип.

Спасибо за ответы.


Название: Re: Запись сокета из разных потоков
Отправлено: andrew.k от Октябрь 26, 2011, 23:44
2 BRE , ну это все-таки лаба) Поэтому почему бы не быть потокам?

В MyClient передавай не указатель на сокет, а socketDescriptor.
А сам сокет создавай в MyClient. И там же все и пиши и читай, зачем основному потоку заниматься записью данных других потоков? Что вообще тогда делают эти потоки?

Ниже примерный псевдокод.
Код
C++ (Qt)
class MyClient : public QThread
{
 MyClient(int socket )
 {
    _socket = socket;
 }
 void run()
 {
    QTcpSocket socket;
    socket.setSocketDescriptor( _socket );
    ...
    exec();
 }
}
...
MyServerSocket::incomingConnection( int socketId )
{
  MyClient * c = new MyClient( socketId );
  connect( c, SIGNAL( finished() ), c, SLOT( deleteLater() ) );
  c->start();
}


Название: Re: Запись сокета из разных потоков
Отправлено: dead_vip от Октябрь 27, 2011, 00:27
В MyClient передавай не указатель на сокет, а socketDescriptor.
А сам сокет создавай в MyClient. И там же все и пиши и читай, зачем основному потоку заниматься записью данных других потоков?
Это вариант, но тогда в памяти будет висеть столько потоков (даже не просто висеть, а тратить время процессора?), сколько подключенных клиентов, даже если никто никому ничего не пишет.
Что вообще тогда делают эти потоки?
В теле идет разбор данных от клиента - если это общее сообщение поток должен разослать его всем активным сокетам, если приватное - то только тем, кому оно послано, если запрос на авторизацию - считать и проверить имя и возможно отправить список имен "кто онлайн".
Фишка в том, что если есть активность - создаются потоки, отрабатывают и уничтожаются, а иначе висит лишь один поток, а не много.


Название: Re: Запись сокета из разных потоков
Отправлено: andrew.k от Октябрь 27, 2011, 01:06
тогда я не понимаю зачем этим потокам вообще указатель на сокет это раз, если они в них не пишут, только данные обрабатывают.
не понимаю зачем потоки для таких элементарных операций это два.
они создаваться и удаляться будут дольше чем реально работать.


Название: Re: Запись сокета из разных потоков
Отправлено: dead_vip от Октябрь 27, 2011, 01:34
Похоже сделать так, чтобы потоки еще и писали данные нельзя (но идея была именно, чтобы писали).

А нужны они просто для понимания (просто я подумал: "А если операции будут не элементарные", и решил переделать с использованием потоков).


Название: Re: Запись сокета из разных потоков
Отправлено: andrew.k от Октябрь 27, 2011, 02:57
почему нельзя то? смотри ответ #9.
я че зря писал? че ты ничего не понял?
в такой реализации пиши себе на здоровье в сокет.


Название: Re: Запись сокета из разных потоков
Отправлено: Странник от Октябрь 27, 2011, 08:24
не понимаю зачем потоки для таких элементарных операций это два.
ну это все-таки лаба) Поэтому почему бы не быть потокам? (C) <= )

баловство это все. даже чисто для понимания - не поймешь ничего толком, когда реализация на задаче смотрится примерно как на корове седло. хочешь разобраться - напиши в свободное время какой-нибудь более нагруженный сервер, например, файлы клиентам раздающий. тогда плюсы и минусы каждого решения прочувствуешь на себе.


Название: Re: Запись сокета из разных потоков
Отправлено: dead_vip от Октябрь 27, 2011, 17:13
почему нельзя то? смотри ответ #9.
я че зря писал? че ты ничего не понял?
в такой реализации пиши себе на здоровье в сокет.
По-моему я в полной мере ответил в посте #10. Да так можно сделать, но меня интересует возможно ли сделать иначе: сокет создается в основном потоке, сигнал readyRead ловится тоже в основном потоке, в слоте onReadyRead создается поток, который должен записать в сокет, который уже создан, отработать и уничтожиться, не закрывая сокет... почему так сделать нельзя?


Название: Re: Запись сокета из разных потоков
Отправлено: andrew.k от Октябрь 27, 2011, 17:43
можно. почему нельзя?
нельзя из функции run.
можно попробовать moveToThread.
но придется каждый раз двигать туда-сюда.
и наверное не получится)

а вообще это то же самое, что удалять гланды через жопу.
я понимаю, что это лаба, но все таки.
если поток нужен,чтобы обработать данные, пусть их и обрабатывает. писать должен тот, кто создал сокет.


Название: Re: Запись сокета из разных потоков
Отправлено: andrew.k от Октябрь 27, 2011, 17:44
двигать наверное можно пока сокет не открыт.


Название: Re: Запись сокета из разных потоков
Отправлено: dead_vip от Октябрь 28, 2011, 17:43
баловство это все. даже чисто для понимания - не поймешь ничего толком, когда реализация на задаче смотрится примерно как на корове седло. хочешь разобраться - напиши в свободное время какой-нибудь более нагруженный сервер, например, файлы клиентам раздающий. тогда плюсы и минусы каждого решения прочувствуешь на себе.
Самый дельный совет оказался =) Оказывается, вызывать методы сокета действительно можно только в том потоке, где он создан, делать иначе не получится. Все мой педантизм... дальше будет лаба ftp-сервер, раздающий файлы.

Спасибо всем за ответы.