Введение
QTcpServer — серверный аналог QTcpSocket: он принимает входящие подключения и для каждого клиента создаёт отдельный объект QTcpSocket. В отличие от классических серверов на блокирующих сокетах с потоком на клиента, Qt-сервер по умолчанию однопоточный и обслуживает всех клиентов асинхронно в одном event loop — это снижает накладные расходы и упрощает совместный доступ к данным. В статье разберём, как принимать подключения, рассылать сообщения и отслеживать отключение клиентов.
Концепция
Сервер слушает порт через listen(), а каждое новое подключение сигнализируется через newConnection(). В обработчике этого сигнала нужно вызвать nextPendingConnection(), который возвращает указатель на QTcpSocket для конкретного клиента. Дальше с этим сокетом работают как с обычным QTcpSocket: подписываются на readyRead() и disconnected().
Поскольку все клиенты обслуживаются в одном потоке, нет проблем гонок данных при обращении к общим структурам (например, списку подключённых клиентов) — но любая долгая синхронная операция в обработчике (тяжёлые вычисления, блокирующий ввод-вывод) заблокирует весь сервер для всех клиентов одновременно. Для CPU-интенсивной нагрузки используют QThreadPool или выносят обработку в отдельные потоки с moveToThread.
Пример кода: QTcpServer
#include <QCoreApplication>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
#include <QList>
class ChatServer : public QObject
{
Q_OBJECT
public:
explicit ChatServer(quint16 port, QObject *parent = nullptr) : QObject(parent)
{
connect(&m_server, &QTcpServer::newConnection, this, &ChatServer::onNewConnection);
if (!m_server.listen(QHostAddress::Any, port)) {
qWarning() << "Не удалось запустить сервер:" << m_server.errorString();
return;
}
qDebug() << "Сервер слушает порт" << port;
}
private slots:
void onNewConnection()
{
while (m_server.hasPendingConnections()) {
QTcpSocket *client = m_server.nextPendingConnection();
m_clients.append(client);
qDebug() << "Новый клиент:" << client->peerAddress().toString();
connect(client, &QTcpSocket::readyRead, this, [this, client]() {
const QByteArray data = client->readAll();
broadcast(data, client);
});
connect(client, &QTcpSocket::disconnected, this, [this, client]() {
qDebug() << "Клиент отключился:" << client->peerAddress().toString();
m_clients.removeOne(client);
client->deleteLater();
});
}
}
private:
void broadcast(const QByteArray &data, QTcpSocket *sender)
{
for (QTcpSocket *client : m_clients) {
if (client != sender) {
client->write(data);
}
}
}
QTcpServer m_server;
QList<QTcpSocket *> m_clients;
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
ChatServer server(12345);
return app.exec();
}
#include "main.moc"
Пояснения к коду
При каждом сигнале newConnection() сервер может иметь несколько ожидающих подключений в очереди, поэтому корректно обрабатывать их в цикле while (hasPendingConnections()), а не ограничиваться одним вызовом nextPendingConnection(). Для каждого клиента подключаются лямбда-обработчики на readyRead и disconnected, захватывающие указатель на сокет — это удобный способ избежать создания отдельного класса-обёртки для каждого подключения, если логика простая.
Метод broadcast() рассылает полученные данные всем клиентам, кроме отправителя — классическая основа простого чат-сервера.
Подводные камни
- Удаление сокета через
deleteLater(), а неdelete. Сокет мог инициировать сигнал, который сейчас обрабатывается — немедленное удаление может привести к use-after-free внутри текущего стека вызовов. QList<QTcpSocket*> m_clientsхранит «сырые» указатели. Если забыть удалить указатель из списка при отключении, последующая рассылка попытается писать в уже удалённый объект. Всегда синхронизируйте список с реальным состоянием подключений.- Один поток — одна точка отказа. Тяжёлая синхронная обработка в обработчике
readyReadодного клиента замораживает обслуживание всех остальных. Для нагруженных серверов рассмотритеQTcpServer::incomingConnection()с раздачей клиентов по пулу потоков. - Backlog подключений. По умолчанию
listen()использует ограниченную очередь ожидающих подключений; при резком наплыве клиентов часть из них может получить отказ на уровне TCP до того, как сервер успеет их принять.