Примечание: этот ответ касается только Linux, но то же самое должно относиться к любой другой системе, которая реализует UDP в соответствии с IETF RFC.
TL; DR: используйте connectToHost
и write
Вы должны QUdpSocket::connectToHost
, а затем QIODevice::write
, например
QUdpSocket socket;
socket.connectToHost(target_address, 0);
socket.write(magic_datagram, magic_datagram_size);
Это связано с реализацией sendmsg
в ядре Linux. Однако, учитывая, что sendmsg
и _8 _ + _ 9_ (или connectToHost
и write
), вероятно, не должны отличаться по своему поведению, вам не следует считать connectToHost
и `писать 'работающими вечно. В конце концов, WoL - это Ethernet-фрейм.
Почему QUdpSocket::sendTo
терпит неудачу?
Прогулка по сетевому стеку
IANA назначает порты как для UDP, так и для TCP. Наш порт назначения 0
указан в регистрации IANA как зарезервированный. Это естественно, поскольку нулевой порт источника четко определен в спецификации UDP как не использовал.
Однако зарезервированное значение редко мешает нам просто ввести его, и Qt случайно его принимает. Так что что-то в процессе должно помешать нам отправить дейтаграмму.
Наша дейтаграмма проходит несколько уровней, прежде чем наконец выйдет в сеть:
- Сетевой стек Qt
- Сокет библиотеки GNU C (glibc) (обычно небольшой слой вокруг ядра)
- ядро Linux
- сетевая карта (которая действительно не должна заботиться в этот момент)
Управление ошибками Qt и ошибки в стиле C
Прежде чем мы углубимся в проблему, мы должны сначала проверить, есть ли на втором уровне дополнительная информация через errno
и perror()
:
if (writtenSize != magicPacketLength)
{
if(errno)
{
int err = errno;
perror("Underlying error in UDP");
fprintf(stderr "Error number: %d\n", err);
}
result = { false, "writtenSize(" + QString::number(writtenSize)
+ ") != magicPacketLength(" + QString::number(magicPacketLength) + "): "
+ socket.errorString() };
}
Это действительно будет сообщать
Underlying error in UDP: Invalid argument
Error number: 22
Ошибка 22 - -EINVAL
, недопустимый аргумент. Поскольку Qt обычно нормально сообщает о неверных аргументах (а не просто о невозможности отправить сообщение), мы можем пропустить его реализацию и вместо этого заглянуть в glibc или даже в ядро.
Мы также можем воссоздать поведение без Qt:
int main(int argc, char* argv[]) {
int sockfd;
int not_ok = 0;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET, SOCK_DGRAM, 0)
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_aton("192.168.11.31", &servaddr.sin_addr);
// Use port 0 on no args, port 9 on any arg
servaddr.sin_port = htons(argc > 1 ? 9 : 0 );
sendto(sockfd, "", 0, MSG_CONFIRM,
(const struct sockaddr *) &servaddr, sizeof(servaddr));
if(errno) {
int err = errno;
perror("Error during sendto");
printf("Errno: %d\n", err);
}
}
Таким образом, мы на правильном пути. Однако, если вас интересует сетевой стек Qt, взгляните на
Погружаясь в бездну
Теперь давайте полностью пропустим glibc и вместо этого перейдем прямо к ядру. Поскольку мы имеем дело с UDP в IPv4, нам нужно перейти на _ 24_. Поскольку мы уже знаем, что получаем EINVAL
, мы можем просто найдите ошибку и найдите:
// Note: if `usin` is valid than an destination was given to sendto.
// This is true for messages sent via QUdpSocket::sendTo.
if (usin) {
if (msg->msg_namelen < sizeof(*usin))
return -EINVAL;
if (usin->sin_family != AF_INET) {
if (usin->sin_family != AF_UNSPEC)
return -EAFNOSUPPORT;
}
daddr = usin->sin_addr.s_addr;
dport = usin->sin_port;
if (dport == 0)
return -EINVAL;
}
Ядро Linux распознает зарезервированный порт и отклоняет его как недопустимый в udp_sendmsg
. Хотя это может показаться неправильной функцией, системный вызов sendto
реализован в терминах socket_sendmsg
, который вызывает udp_sendmsg
на сокетах UDP.
Следовательно, мы не можем отправлять какие-либо пакеты UDP через QUdpSocket::sendTo
.
Альтернатива через QUdpSocket::connectToHost
Теперь есть альтернатива QUdpSocket::sendTo
. Если мы знаем, что собираемся отправлять все сообщения на один и тот же порт, тогда мы можем использовать connectToHost
, чтобы не повторяться:
QByteArray payload;
QUdpSocket socket;
socket.connectToHost(target_address, 0);
socket.write(payload);
Если мы попробуем этот вариант, то сразу получим правильный результат. Почему?
QUdpSocket::connectToHost
использует системный вызов connect
. Системный вызов connect
не возвращает EINVAL (по крайней мере, до 4.15, более высокие не проверял). Кроме того, он использует ipv4_datagram_connect
, который с радостью принимает любой порт.
Мы также можем снова проверить поведение на простом C:
int main(int argc, char* argv[]) {
int sockfd;
int not_ok = 0;
struct sockaddr_in servaddr;
sockfd = socket(AF_INET, SOCK_DGRAM, 0)
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_aton("192.168.11.31", &servaddr.sin_addr);
// Use port 0 on no args, port 9 on any arg
servaddr.sin_port = htons(argc > 1 ? 9 : 0 );
connect(sockfd, (const struct sockaddr *) &servaddr, sizeof(servaddr));
send(sockfd, "", 0, MSG_CONFIRM);
if(errno) {
int err = errno;
perror("Error during sendto");
printf("Errno: %d\n", err);
}
}
Так что насчет udp_sendmsg
, который используется QIODevice::write
или send
? Ну, помните if(usin)
в приведенном выше коде? Поскольку адрес хранится в текущем состоянии сокета, usin == NULL
. Проверка адреса назначения никогда не происходит. Это может быть ошибкой или полностью задумано. Для этих файлов нужно будет проверить git log
s.
Учитывая, что connect(...)
с нулевым портом назначения может быть обычным вариантом использования для UDP, это поведение может никогда не измениться, поскольку оно нарушит пространство пользователя, однако не следует слишком доверять зарезервированный порт, который не предназначен для использования в данном протоколе.
person
Zeta
schedule
03.02.2021