В общем накидал многопоточный сервер на Qt
Асинхронный - да. Многопоточный - нет. Главный недостаток такой реализации в том, что любой метод, который выполняется дольше всех - блокирует основной цикл событий и как следствие все клиенты вынуждены ждать ответа от сервера до тех пор пока управление не вернется в этот цикл событий. Пример: сервер пытается передать файл клиенту. В этот момент срабатывает Касперский и просит пользователя принять решение, что делать с этим файлом. Если пользователя в этот момент нет за компьютером, то диалог Касперского будет висеть вечно, а операционная система не возвратит управление с ошибкой в приложение, которое пытается открыть файл до того момента пока антивирус этого не разрешит. В случае с многопоточной системой несколько циклов событий в каждом потоке могли бы запускать таймер по истечении которого грохать нерадивый поток, застравший на чтении файла, а клиентов просить переподключитсья, чтобы поймать их в новом потоке. Файл можно пометить как "плохой" и не предпринимать попытки чтения до того как придет хозяин и не разберется с этим.
Эти инклуды замедляют процесс компиляции:
C++ (Qt)
#include <QTcpServer>
#include <QTcpSocket>
#include <QBuffer>
#include <QMessageBox>
На эти классы QTcpServer, QTcpSocket, QBuffer в заголовках используются только указатели, на которые известны размеры компилятору. Поэтому достаточно Forward Declaration:
C++ (Qt)
class QTcpServer;
class QTcpSocket;
class QBuffer;
Но в .cpp файле нужно будет сделать включение их инклудов, а инклуд QMessageBox перенести вообще в .cpp файл. Часто получается так, что используется один заголовок на несколько .cpp файлов, причем некоторым из них не нужны инклуды классов, которые они используют.
C++ (Qt)
foreach (QTcpSocket* connection, connections)
foreach делает неявную копию всех элементов списка. В случае указателей наверно это небольшая потеря производительности, но она нам нужна?
C++ (Qt)
if (ui->lineEdit->text() == "")
Возможно компилятор это и сможет оптимизировать, но может лучше isEmpty()?
C++ (Qt)
ui->lineEdit->setValidator(new QRegExpValidator(regExp, this));
Может лучше QIntValidator? Он позволяет задать и минимум, и максимум, и отсеять все, что не целочисленное. Да и от этих дополнительных проверок избавится:
C++ (Qt)
if (ui->lineEdit->text().toInt() > 65535)
А вообще, может лучше обычный QSpinBox поставить, чтобы не приходилось столько проверок делать?:
C++ (Qt)
if (ui->lineEdit->text() == "")
Вместо пустоты там вполне может находится минимальное значение для порта и проверка не понадобится.
Может быть сделать смену состояний через QStateMachine и вынести в конструктор? Будет легче поддерживать приложение, если добавятся или пропадут кнопки/виджеты.
void MainWindow::slotStartClicked()
{
...
ui->pushButton->setDisabled(true);
ui->pushButton_2->setDisabled(false);
ui->lineEdit->setDisabled(true);
...
}
void MainWindow::slotStopClicked()
{
ui->pushButton->setEnabled(true);
ui->pushButton_2->setEnabled(false);
ui->lineEdit->setDisabled(false);
Аналогично вслед за сменой состояния можно динамически менять текст на QLabel:
C++ (Qt)
ui->label->setText(tr("Server stopped"));
Item based widgets довольно медленны на больших объемах элементов. Лучше уж QPlainTextEdit, там хоть можно выставить setMaximumBlockCount() и сервер не рухнет, если ты его решишь на недельку другую оставить без присмотра.
C++ (Qt)
ui->listWidget->addItem(ui->label->text());
Не нужно распихивать tr() для динамических строк. Я не думаю, что ты потом захочешь перевести английский текст типа "%1" на русский, например "Процент 1", а когда речь идет о цифре (peerPort()), то я думаю американцев в школе тоже учат арабским цифрам. Или ты хочешь обозначать цифры буквами как в Древней Руси?
C++ (Qt)
tmpClientAddressPort << tr("%1").arg(serverSocket->peerPort());
str_buf = tr("%1").arg(serverSocket->peerPort());
C++ (Qt)
QList<QTcpSocket*> connections;
QHash<QTcpSocket*, QBuffer*> buffers;
Вместо хранения обычных указателей в списках можно хранить QSharedPointer, при удалении любого из таких элементов из списка память освободится сама и в этом коде потребность отпадет:
C++ (Qt)
buffer->deleteLater();
...
socket->deleteLater();
Базовый класс в QTcpSocket это QObject, думаю тут правильней будет использовать qobject_cast вместо static_cast:
C++ (Qt)
QTcpSocket* socket = static_cast<QTcpSocket*>(sender());
Следующий код по меньшей мере странный:
C++ (Qt)
void MainWindow::slotSocketRead()
{
QTcpSocket* socket = static_cast<QTcpSocket*>(sender());
QBuffer* buffer = buffers.value(socket);
ui->label->setText("readyread");
qint64 bytes = buffer->write(socket->readAll());
buffer->seek(buffer->pos() - bytes);
QByteArray line = buffer->readAll();
ui->textEdit->append(line);
sendToClients(line);
}
Когда приходят данные ты добавляешь их в буффер и выводишь в textEdit. Запросто может произойти ситуация, когда данные приходят частями, тогда твой textEdit будет выглядеть так:
He
Hell
Hello
Hello W
Hello World
Это уже касается самого протокола. Обычно люди шлют размер данных, который собираются передавать и лишь когда буффер станет равный этому размеру выводят результат. Стоит учесть, что в Unicode один символ - 2 байта, поэтому QString::length() или size() вернут значение в 2 раза меньше.
Попытка записи в закрытый сокет? Или "извини пользователь, я не могу тебе дать отключиться, твой компьютер мой раб навеки!"?
C++ (Qt)
void MainWindow::slotClientDisconnected()
{
...
QByteArray msg = "Client disconnected";
foreach (QTcpSocket* connection, connections)
{
connection->write(msg);
}
}
В потоках я не очень, поэтому хочу посоветоваться, какие функции раскидать по потокам. Очень нужна Ваша помощь.
Возвращаясь к главному вопросу. Потоков сделай изначально 5 (вообще нужно писать систему, которая бы сама вычисляла производительность сервера в зависимости от количества доступной памяти и потоков, но для начала можно поставить и фиксированное значение). Клиентов раскидывать равномерно по потокам. Соединение/разъединение всех клиентов можно осуществлять в одном потоке, в то время как обработку данных в остальных пяти. Наша задача вынести самые долги функции в отдельные потоки. Как правило это функции подготовки данных на запись в поток. Чтение с винчестера, долгие математические вычесления и тому подобное. В общем всё то, что может заморозить основной цикл событий и как следствие привести к отказу обслуживания сервером вместо того, чтобы "кормить" клиента обещаниями "вот вот, уже скоро, еще 5 минут и я тебе пришлю твои фотки". Исходя из этого у тебя сейчас только один, потенциальный, участок, который в будущем может стать "узким горлышком":
C++ (Qt)
void MainWindow::slotSocketRead()
{
...
QByteArray line = buffer->readAll();
ui->textEdit->append(line);
sendToClients(line);
}
Тут у тебя данные читаются из сокета и затем отправляются обратно. То есть получился echo server. На его месте могла бы быть пересылка тяжелого файла, поэтому в строках предшествующих buffer->readAll() должен происходить эмит сигнала о необходимости подготовки данных, который отправляется другому потоку. Соответственно строчка sendToClients() должна будет перенесена в слот, который будет ответом на сигнал из подготавливающего потока о том, что данные готовы к пересылке. Кстати убери весь код, относящийся к работе с GUI из слотов обрабатывающих сокеты. Все общение с GUI нужно реализовать посредством сигналов, а не прямых вызовов слотов виджетов. Только в этом случае мы можем гарантировать то, что никакое взаимодействие с виджетами не тормозит работу сервера, так как сигналы будут вставать в очередь и гарантировать нам то, что их обработка основым, GUI'шным потоком не блокируют работу, то есть не будет необходимости ждать завершения работы слотов типа ui->textEdit->append(line); Нам ведь не нужен возвращаемый результат? Тогда нафига ждать?