【问题标题】:How to write to port 0 with QUdpSocket?如何使用 QUdpSocket 写入端口 0?
【发布时间】:2021-02-03 08:08:58
【问题描述】:

我想使用 Qt 实现 WoL Magic Packet 以实现跨 GNU/Linux 和 Microsoft Windows 的可移植性。维基百科说:“通常作为 UDP 数据报发送到端口 0(保留端口号)、7(回声协议)或 9(丢弃协议)”,但我无法在端口上写入任何数据0 与 QUdpSocket,为什么?

问题示例

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 写入数据?

【问题讨论】:

  • 该死的好问题。不幸的是,我刚刚注意到您对 Windows 和 Linux 都感兴趣。如果 connectToHost+write 在 Windows 上不起作用,请添加评论。

标签: c++ qt5 qudpsocket


【解决方案1】:

注意:此答案仅考虑 Linux,但对于根据 IETF RFC 实现 UDP 的任何其他系统也应如此。

TL;DR:使用 connectToHostwrite

你必须先QUdpSocket::connectToHost 然后QIODevice::write,例如

QUdpSocket socket;
socket.connectToHost(target_address, 0);

socket.write(magic_datagram, magic_datagram_size);

这是由于sendmsg 的Linux 内核实现所致。但是,考虑到sendmsgconnect+send(或connectToHostwrite)的行为可能没有区别,你不应该指望connectToHost 和“写”永远工作。毕竟,WoL 是一个以太网框架。

为什么QUdpSocket::sendTo 会失败?

沿着网络栈走

IANA assigns ports to both UDP and TCP。我们的目标端口0 在 IANA 的注册中列为保留。这是很自然的,因为源端口 0 在 the UDP specification 中被明确定义为“未使用”。

然而,保留值很少会阻止我们直接输入它,Qt 很乐意接受它。所以在此过程中必须有一些东西阻止我们实际发送数据报。

我们的数据报在最终进入网络之前要遍历好几层:

  1. Qt 的网络栈
  2. GNU C 库的 (glibc) 套接字(通常只是内核周围的一小层)
  3. Linux 内核
  4. 网卡(此时真的不应该关心)

Qt 的错误管理和 C 风格的错误

在深入研究这个问题之前,我们应该首先通过errnoperror()检查第二层是否有更多信息:

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_sendmsgQIODevice::writesend 使用呢?好吧,还记得上面代码中的if(usin) 吗?由于地址存储在套接字的当前状态中,usin == NULL。目标地址检查永远不会发生。这可能是一个错误,或者完全是故意的。需要检查git logs 是否有这些文件。

鉴于目标端口为零的connect(...)可能是 UDP 的常见用例,这种行为可能永远不会改变,因为它会破坏用户空间,但是,不应该对不打算在给定协议中使用的保留端口过于信任。

【讨论】:

  • 真的谢谢你:D。目前,我无法在 Windows 上进行测试。我会尽快提供反馈。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多