Различные статьи по Qt и c++

QUdpSocket простыми словами: основы UDP-сокетов в Qt

Введение

UDP — более простой, чем TCP, протокол передачи данных: без установления соединения, без гарантии доставки, но быстрый и подходящий для задач, где допустима потеря отдельных сообщений (обнаружение устройств в сети, простая телеметрия). В статье разберём самый базовый сценарий — отправку и приём UDP-сообщений через QUdpSocket.

Концепция

В отличие от QTcpSocket, для UDP не нужно «подключаться» к серверу заранее — достаточно привязать сокет к локальному порту через bind() (для приёма сообщений) и отправлять данные напрямую на конкретный адрес и порт через writeDatagram(), указывая получателя при каждой отправке. Приём данных, как и в TCP, происходит через сигнал readyRead(), но читать пришедшие данные нужно через readDatagram() (или более удобный receiveDatagram()), который вместе с данными сообщает адрес отправителя.

Пример кода: QUdpSocket простыми словами

#include <QUdpSocket>
#include <QApplication>
#include <QDebug>

// Отправитель
class UdpSender : public QObject
{
    Q_OBJECT
public:
    void sendMessage(const QString &text, const QString &host, quint16 port)
    {
        const QByteArray data = text.toUtf8();
        const qint64 bytesSent = m_socket.writeDatagram(data, QHostAddress(host), port);

        if (bytesSent == -1) {
            qDebug() << "Не удалось отправить сообщение:" << m_socket.errorString();
        } else {
            qDebug() << "Сообщение отправлено, байт:" << bytesSent;
        }
    }

private:
    QUdpSocket m_socket;
};

// Получатель
class UdpReceiver : public QObject
{
    Q_OBJECT
public:
    UdpReceiver()
    {
        // Привязываем сокет к порту 45000, чтобы принимать сообщения, отправленные на этот порт
        if (m_socket.bind(QHostAddress::Any, 45000)) {
            qDebug() << "Слушаем порт 45000...";
        } else {
            qDebug() << "Не удалось привязаться к порту:" << m_socket.errorString();
        }

        connect(&m_socket, &QUdpSocket::readyRead, this, &UdpReceiver::onReadyRead);
    }

private slots:
    void onReadyRead()
    {
        while (m_socket.hasPendingDatagrams()) {
            QByteArray buffer;
            buffer.resize(m_socket.pendingDatagramSize());

            QHostAddress senderAddress;
            quint16 senderPort;
            m_socket.readDatagram(buffer.data(), buffer.size(), &senderAddress, &senderPort);

            qDebug() << "Получено от" << senderAddress.toString() << ":" << senderPort
                     << "сообщение:" << buffer;
        }
    }

private:
    QUdpSocket m_socket;
};

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    UdpReceiver receiver;
    UdpSender sender;

    // Отправляем сообщение самому себе на localhost для проверки работы
    sender.sendMessage("Привет по UDP!", "127.0.0.1", 45000);

    return app.exec();
}

#include "main.moc"

Пояснения к коду

UdpSender::sendMessage() отправляет данные одним вызовом writeDatagram(), указывая адрес и порт получателя непосредственно в этом вызове — никакого предварительного «подключения» не требуется, в отличие от TCP. Каждый вызов writeDatagram() отправляет одно независимое сообщение (датаграмму); если нужно отправить несколько сообщений, метод вызывается несколько раз.

UdpReceiver сначала привязывается к конкретному порту через bind(QHostAddress::Any, 45000) — это означает «слушать на этом порту входящие сообщения с любого адреса». В обработчике onReadyRead() цикл while (hasPendingDatagrams()) важен, поскольку за один сигнал readyRead() может накопиться несколько отдельных датаграмм — нужно забрать их все, а не только одну. readDatagram() дополнительно сообщает адрес и порт отправителя через выходные параметры — это полезно, когда нужно знать, кто именно отправил сообщение, особенно при широковещательной (broadcast) рассылке от множества источников.

Подводные камни

  • Ожидание гарантированной доставки сообщений, как в TCP. UDP не гарантирует, что сообщение дойдёт до получателя вообще, и не гарантирует порядок доставки нескольких сообщений — для задач, где это критично, нужен либо TCP, либо собственный механизм подтверждений поверх UDP.
  • Забытый bind() перед попыткой принимать сообщения. Без привязки к порту сокет может отправлять данные, но не получит входящие сообщения, адресованные на конкретный порт — bind() обязателен именно для роли «получателя».
  • Чтение только одной датаграммы за обработчик readyRead() без цикла while. Если за одно срабатывание сигнала накопилось несколько сообщений, а код читает только одно через простой if вместо while (hasPendingDatagrams()), оставшиеся сообщения будут обработаны только при следующем срабатывании сигнала (что может быть с заметной задержкой) или вовсе потеряны при неаккуратной логике.
  • Использование одного и того же номера порта несколькими программами одновременно без понимания последствий. Если порт уже занят другим процессом, bind() завершится неудачно — стоит выбирать порты из диапазона, не зарезервированного системой и другими известными службами (как правило, выше 1024 для непривилегированных портов на большинстве ОС).