Введение
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 для непривилегированных портов на большинстве ОС).