Работа с сетью в Qt

QTcpSocket: подключение к серверу и обработка разрывов соединения

Введение

Сетевые соединения — это всегда работа с ненадёжной средой: сервер может быть недоступен, соединение может прерваться в любой момент, а данные приходят не целиком и не сразу. 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() в синхронном коде), иначе сокет может остаться в промежуточном состоянии.
  • Утечка данных в буфере при ошибке протокола. Если разделитель не встречается долго (например, из-за повреждённых данных), буфер будет расти бесконечно — стоит ограничивать его максимальный размер и закрывать соединение при превышении.