注意:此答案仅考虑 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);
这是由于sendmsg 的Linux 内核实现所致。但是,考虑到sendmsg 和connect+send(或connectToHost 和write)的行为可能没有区别,你不应该指望connectToHost 和“写”永远工作。毕竟,WoL 是一个以太网框架。
为什么QUdpSocket::sendTo 会失败?
沿着网络栈走
IANA assigns ports to both UDP and TCP。我们的目标端口0 在 IANA 的注册中列为保留。这是很自然的,因为源端口 0 在 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 的网络堆栈感兴趣,请查看
深入深渊
现在让我们完全跳过 glibc,直接进入内核。由于我们正在处理 IPv4 中的 UDP,我们需要进入/net/ipv4/udp.c。我们已经知道我们得到EINVAL,我们可以简单地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 系统调用不返回 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 logs 是否有这些文件。
鉴于目标端口为零的connect(...)可能是 UDP 的常见用例,这种行为可能永远不会改变,因为它会破坏用户空间,但是,不应该对不打算在给定协议中使用的保留端口过于信任。