Введение
Сетевые соединения — это всегда работа с ненадёжной средой: сервер может быть недоступен, соединение может прерваться в любой момент, а данные приходят не целиком и не сразу. Qt предоставляет класс QTcpSocket, который скрывает низкоуровневую работу с сокетами BSD/WinSock и даёт удобный асинхронный (событийный) интерфейс через сигналы и слоты. В этой статье разберём, как корректно подключиться к серверу, как читать данные по частям и, что важнее всего на практике, — как правильно обрабатывать разрывы соединения и переподключение.
Концепция
QTcpSocket работает асинхронно: вызов connectToHost() не блокирует поток, а инициирует подключение, о результате которого сообщают сигналы connected(), errorOccurred() и disconnected(). Чтение данных происходит по сигналу readyRead() — при этом нет гарантии, что данные придут одним куском: TCP — потоковый протокол, и сообщение может быть разбито на несколько пакетов или, наоборот, несколько сообщений могут прийти в одном пакете. Поэтому при разборе протокола важно либо использовать готовый формат на основе QDataStream с длиной сообщения в заголовке, либо буферизовать данные самостоятельно до получения разделителя.
Разрыв соединения сигнализируется через disconnected(), а ошибки — через errorOccurred(QAbstractSocket::SocketError). Грамотная обработка обоих сигналов и логика переподключения с задержкой (а не мгновенным повтором, который может создать шторм запросов) — основа устойчивого сетевого клиента.
Пример кода: QTcpSocket
#include <QCoreApplication>
#include <QTcpSocket>
#include <QTimer>
#include <QDebug>
class TcpClient : public QObject
{
Q_OBJECT
public:
explicit TcpClient(const QString &host, quint16 port, QObject *parent = nullptr)
: QObject(parent), m_host(host), m_port(port)
{
connect(&m_socket, &QTcpSocket::connected, this, &TcpClient::onConnected);
connect(&m_socket, &QTcpSocket::disconnected, this, &TcpClient::onDisconnected);
connect(&m_socket, &QTcpSocket::readyRead, this, &TcpClient::onReadyRead);
connect(&m_socket, &QTcpSocket::errorOccurred, this, &TcpClient::onErrorOccurred);
m_reconnectTimer.setSingleShot(true);
connect(&m_reconnectTimer, &QTimer::timeout, this, &TcpClient::tryConnect);
}
void start() { tryConnect(); }
private slots:
void tryConnect()
{
qDebug() << "Подключение к" << m_host << ":" << m_port;
m_socket.connectToHost(m_host, m_port);
}
void onConnected()
{
qDebug() << "Соединение установлено";
m_buffer.clear();
}
void onDisconnected()
{
qDebug() << "Соединение разорвано, повтор через 3 секунды";
m_reconnectTimer.start(3000);
}
void onReadyRead()
{
m_buffer.append(m_socket.readAll());
// Простейший протокол: сообщения разделены символом '\n'
int idx;
while ((idx = m_buffer.indexOf('\n')) != -1) {
QByteArray line = m_buffer.left(idx);
m_buffer.remove(0, idx + 1);
qDebug() << "Получено сообщение:" << line;
}
}
void onErrorOccurred(QAbstractSocket::SocketError error)
{
qDebug() << "Ошибка сокета:" << error << m_socket.errorString();
if (m_socket.state() == QAbstractSocket::UnconnectedState) {
m_reconnectTimer.start(3000);
}
}
private:
QTcpSocket m_socket;
QTimer m_reconnectTimer;
QString m_host;
quint16 m_port;
QByteArray m_buffer;
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
TcpClient client("127.0.0.1", 12345);
client.start();
return app.exec();
}
#include "main.moc"
Пояснения к коду
Класс TcpClient хранит постоянный экземпляр QTcpSocket и переподключается через таймер с задержкой 3 секунды при разрыве или ошибке. Буфер m_buffer накапливает входящие байты до тех пор, пока не встретится символ-разделитель \n — это типичный приём при работе с текстовыми построчными протоколами. Для бинарных протоколов вместо разделителя обычно используют заголовок с длиной сообщения и QDataStream.
Обратите внимание, что errorOccurred() и disconnected() могут сработать почти одновременно — оба обработчика запускают переподключение, поэтому таймер сделан setSingleShot(true), а повторный start() просто перезапускает уже запущенный таймер без накопления дублирующихся вызовов.
Подводные камни
- TCP не гарантирует границы сообщений. Никогда не предполагайте, что один
readyRead()равен одному логическому сообщению — всегда буферизуйте и разбирайте поток по протоколу. - Мгновенное переподключение без задержки при недоступном сервере создаёт busy-loop и нагружает CPU и сеть. Используйте экспоненциальную задержку при повторных неудачах.
disconnectFromHost()асинхронен. После вызова нужно дождаться сигналаdisconnected()(или использоватьwaitForDisconnected()в синхронном коде), иначе сокет может остаться в промежуточном состоянии.- Утечка данных в буфере при ошибке протокола. Если разделитель не встречается долго (например, из-за повреждённых данных), буфер будет расти бесконечно — стоит ограничивать его максимальный размер и закрывать соединение при превышении.