如何使用 QUdpSocket 写入端口 0?
How to write to port 0 with QUdpSocket?
我想使用 Qt 实现 WoL Magic Packet 以便跨 GNU/Linux 和 Microsoft Windows 进行移植。维基百科说:“ 通常作为 UDP 数据报发送到端口 0(保留端口号)、7(回显协议)或 9(丢弃协议)”,但我不能写任何QUdpSocket 端口 0 上的数据,为什么?
问题示例
QUdpSocket socket;
auto const writtenSize = socket.writeDatagram(toSend, magicPacketLength,
QHostAddress(ip), defaultPort);
if (writtenSize != magicPacketLength)
{
result = { false, "writtenSize(" + QString::number(writtenSize)
+ ") != magicPacketLength(" + QString::number(magicPacketLength) + "): "
+ socket.errorString() };
}
输出为:
writtenSize(-1) != magicPacketLength(102): Unable to send a message
其他端口(7和9)没问题,但为什么我无法向端口0写入数据?
注意:此答案仅考虑 Linux,但同样适用于 任何 其他根据 IETF RFC 实现 UDP 的系统。
TL;DR:使用 connectToHost
和 write
您必须 QUdpSocket::connectToHost
然后 QIODevice::write
,例如
QUdpSocket socket;
socket.connectToHost(target_address, 0);
socket.write(magic_datagram, magic_datagram_size);
这是由于 Linux 内核实现了 sendmsg
。但是,鉴于 sendmsg
和 connect
+send
(或 connectToHost
和 write
)的行为可能没有差异,您不应该计算 connectToHost
和 `write' 永远工作。毕竟,WoL 是一个以太网框架。
为什么 QUdpSocket::sendTo
失败?
沿着网络堆栈走
IANA assigns ports to both UDP and TCP. Our destination port 0
is listed in the IANA's registration as reserved. This is only natural, as the source port zero is well-defined in the UDP specification为“未使用”。
然而,保留值很少会阻止我们直接输入它,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 的网络堆栈感兴趣,请查看
QUdpSocket::writeDatagram
QNativeSocketEngine::writeDatagram
QNativeSocketEnginePrivate::sendDatagram
(which sets the error)
深入深渊
现在让我们完全跳过 glibc,直接进入内核。由于我们在 IPv4 中处理 UDP,我们需要进入 /net/ipv4/udp.c
. As we already know that we get EINVAL
, we can simply search for the error and find:
// 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 套接字上调用 udp_sendmsg
。
因此,我们无法通过QUdpSocket::sendTo
发送任何UDP数据包。
通过 QUdpSocket::connectToHost
的替代方案
现在,QUdpSocket::sendTo
有了替代方案。如果我们知道我们要将所有消息发送到同一个端口,那么我们可以使用 connectToHost
来避免重复:
QByteArray payload;
QUdpSocket socket;
socket.connectToHost(target_address, 0);
socket.write(payload);
如果我们尝试这个变体,我们会立即得到正确的结果。为什么?
QUdpSocket::connectToHost
使用 connect
系统调用。 connect
系统调用不会 return 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);
}
}
那么被 QIODevice::write
或 send
使用的 udp_sendmsg
呢?好吧,还记得上面代码中的 if(usin)
吗?由于地址存储在套接字的当前状态中,usin == NULL
。目标地址检查永远不会发生。这 可能 是一个错误,或者完全是故意的。需要检查这些文件的 git log
s。
鉴于目标端口为零的 connect(...)
可能 是 UDP 的常见用例,此行为可能永远不会改变,因为它会破坏用户-space,然而,人们不应该对不打算在给定协议中使用的保留端口给予太多信任。
我想使用 Qt 实现 WoL Magic Packet 以便跨 GNU/Linux 和 Microsoft Windows 进行移植。维基百科说:“ 通常作为 UDP 数据报发送到端口 0(保留端口号)、7(回显协议)或 9(丢弃协议)”,但我不能写任何QUdpSocket 端口 0 上的数据,为什么?
问题示例
QUdpSocket socket;
auto const writtenSize = socket.writeDatagram(toSend, magicPacketLength,
QHostAddress(ip), defaultPort);
if (writtenSize != magicPacketLength)
{
result = { false, "writtenSize(" + QString::number(writtenSize)
+ ") != magicPacketLength(" + QString::number(magicPacketLength) + "): "
+ socket.errorString() };
}
输出为:
writtenSize(-1) != magicPacketLength(102): Unable to send a message
其他端口(7和9)没问题,但为什么我无法向端口0写入数据?
注意:此答案仅考虑 Linux,但同样适用于 任何 其他根据 IETF RFC 实现 UDP 的系统。
TL;DR:使用 connectToHost
和 write
您必须 QUdpSocket::connectToHost
然后 QIODevice::write
,例如
QUdpSocket socket;
socket.connectToHost(target_address, 0);
socket.write(magic_datagram, magic_datagram_size);
这是由于 Linux 内核实现了 sendmsg
。但是,鉴于 sendmsg
和 connect
+send
(或 connectToHost
和 write
)的行为可能没有差异,您不应该计算 connectToHost
和 `write' 永远工作。毕竟,WoL 是一个以太网框架。
为什么 QUdpSocket::sendTo
失败?
沿着网络堆栈走
IANA assigns ports to both UDP and TCP. Our destination port 0
is listed in the IANA's registration as reserved. This is only natural, as the source port zero is well-defined in the UDP specification为“未使用”。
然而,保留值很少会阻止我们直接输入它,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 的网络堆栈感兴趣,请查看
QUdpSocket::writeDatagram
QNativeSocketEngine::writeDatagram
QNativeSocketEnginePrivate::sendDatagram
(which sets the error)
深入深渊
现在让我们完全跳过 glibc,直接进入内核。由于我们在 IPv4 中处理 UDP,我们需要进入 /net/ipv4/udp.c
. As we already know that we get EINVAL
, we can simply search for the error and find:
// 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 套接字上调用 udp_sendmsg
。
因此,我们无法通过QUdpSocket::sendTo
发送任何UDP数据包。
通过 QUdpSocket::connectToHost
的替代方案
现在,QUdpSocket::sendTo
有了替代方案。如果我们知道我们要将所有消息发送到同一个端口,那么我们可以使用 connectToHost
来避免重复:
QByteArray payload;
QUdpSocket socket;
socket.connectToHost(target_address, 0);
socket.write(payload);
如果我们尝试这个变体,我们会立即得到正确的结果。为什么?
QUdpSocket::connectToHost
使用 connect
系统调用。 connect
系统调用不会 return 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);
}
}
那么被 QIODevice::write
或 send
使用的 udp_sendmsg
呢?好吧,还记得上面代码中的 if(usin)
吗?由于地址存储在套接字的当前状态中,usin == NULL
。目标地址检查永远不会发生。这 可能 是一个错误,或者完全是故意的。需要检查这些文件的 git log
s。
鉴于目标端口为零的 connect(...)
可能 是 UDP 的常见用例,此行为可能永远不会改变,因为它会破坏用户-space,然而,人们不应该对不打算在给定协议中使用的保留端口给予太多信任。