如何在新到达时使UDP套接字替换旧消息(还没有recv()'d)?

时间:2010-08-11 12:49:38

标签: c sockets udp

首先,一点上下文来解释我为什么选择“UDP采样”路线:
我想在一段未知的时间内快速生成数据。我想要采样的数据是在另一台机器上,而不是消耗数据的机器。我在两者之间有专用的以太网连接,因此带宽不是问题。我遇到的问题是消耗数据的机器比生成数据的机器慢得多。一个额外的限制是,虽然我没有得到所有的样本(它们只是样本),但我必须得到最后一个

我的第一个解决方案是让数据生成器为每个生成的样本发送一个UDP数据报,让数据使用者尝试获取它可能的样本,并在UDP套接字已满时让其他人被套接字层丢弃。此解决方案的问题在于,当新UDP数据报到达并且套接字已满时,数据报将被丢弃而不是旧数据报。因此,我不能保证拥有最后一个!

我的问题是:有没有办法让UDP套接字在新到达时替换旧数据报?

接收器目前是一台Linux机器,但是未来可能会改变另一种类似unix的操作系统(Windows可能会实现BSD插槽,但不太可能) 理想的解决方案是使用广泛的机制(如setsockopt()s)来工作。

PS:我想到了其他解决方案,但它们更复杂(涉及对发送者的大量修改),因此我首先要对我所要求的可行性有一个明确的地位! :)

更新 - 我知道接收机器上的操作系统可以处理网络负载+重新组装发送方生成的流量。它的默认行为是在套接字缓冲区已满时丢弃新的数据报。而且由于接收过程中的处理时间,我知道无论我做什么它都会变满(浪费一半的内存在套接字缓冲区上不是一个选项:))。
- 我真的希望避免让一个辅助进程执行操作系统在数据包分派时可能完成的操作,并浪费资源只是在SHM中复制消息。
- 我看到修改发件人的问题是我可以访问的代码只是一个PleaseSendThisData()函数,它不知道它可能是最后一次在很长一段时间之前被调用,所以我不知道看到任何可行的伎俩...但我愿意接受建议! :)

如果真的没有办法改变BSD套接字中的UDP接收行为,那么......告诉我,我准备接受这个可怕的事实,并且当我开始研究“帮助程序”解决方案时回到它:)

6 个答案:

答案 0 :(得分:5)

只需将套接字设置为非阻塞,然后循环recv(),直到它返回< 0与errno == EAGAIN。然后处理你得到的最后一个包,冲洗并重复。

答案 1 :(得分:4)

我同意“caf”。 将套接字设置为非阻塞模式。

每当你在套接字上收到一些东西时 - 读一个循环,直到没有剩下的东西。然后处理最后读取的数据报。

只有一个注意事项:您应该为套接字设置一个大型系统接收缓冲区

int nRcvBufSize = 5*1024*1024; // or whatever you think is ok
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*) &nRcvBufSize, sizeof(nRcvBufSize));

答案 2 :(得分:2)

这很难在听众方面完全正确,因为它实际上可能会错过网络接口芯片中的最后一个数据包,这将使您的程序不会有机会看到它。

操作系统的UDP代码将是尝试处理此问题的最佳位置,因为它会获得新数据包,即使它决定丢弃它们,因为它已经排队太多了。然后它可以决定丢弃一个旧的或丢弃一个旧的,但我不知道如何告诉它这是你想要它做的。

您可以尝试在接收器上处理此问题,方法是让一个程序或线程始终尝试读入最新的数据包,另一个程序或线程始终尝试获取最新的数据包。如果你把它作为两个独立的程序或两个线程来做,将如何做到这一点。

作为线程,您需要一个互斥锁(信号量或类似的东西)来保护指针(或引用)到一个用于保存1个UDP有效负载的结构以及您想要的任何其他内容(大小,发送方IP,发送方端口,时间戳等)。

实际从套接字读取数据包的线程会将数据包的数据存储在结构中,获取保护该指针的互斥锁,将当前指针交换为指向其刚刚生成的结构的指针,释放互斥锁,向处理器发出信号线程,它有事情要做,然后清除它刚刚得到一个指针的结构,并用它来保存下一个数据包。

实际处理数据包有效负载的线程应该等待来自另一个线程的信号和/或定期(500 ms左右可能是一个很好的起点,但你决定了)并获取互斥锁,将其指针交换到一个UDP有效载荷结构,其中有一个,释放互斥锁,然后如果结构有任何数据包数据,它应该处理它,然后等待下一个信号。如果它没有任何数据,它应该继续等待下一个信号。

处理器线程应该以比UDP侦听器更低的优先级运行,以便侦听器不太可能错过数据包。当处理最后一个数据包(你真正关心的数据包)时,处理器不会被中断,因为听众没有新的数据包可以听到。

您可以通过使用队列而不仅仅使用单个指针作为两个线程的交换位置来扩展它。单指针只是一个长度为1的队列,很容易处理。

您还可以通过尝试让侦听器线程检测是否有多个数据包等待并且实际上只将最后一个数据包放入处理器线程的队列中来扩展它。你如何做到这一点因平台而异,但如果你使用的是* nix,那么对于没有等待的套接字,这应该返回0:

while (keep_doing_this()) {
    ssize_t len = read(udp_socket_fd, my_udp_packet->buf, my_udp_packet->buf_len); 
    // this could have been recv or recvfrom
    if (len < 0) {
        error();
    }
    int sz;
    int rc = ioctl(udp_socket_fd, FIONREAD, &sz);
    if (rc < 0) {
        error();
    }
    if (!sz) {
        // There aren't any more packets ready, so queue up the one we got
        my_udp_packet->current_len = len;

        my_udp_packet = swap_udp_packet(my_ucp_packet);
        /* swap_udp_packet is code you would have to write to implement what I talked
           about above. */

        tgkill(this_group, procesor_thread_tid, SIGUSR1);
    } else if (sz > my_udp_packet->buf_len) {
        /* You could resize the buffer for the packet payload here if it is too small.*/
    }
}

必须为每个线程分配udp_packet,并为交换指针分配1。如果使用队列进行交换,则必须为队列中的每个位置提供足够的udp_packets - 因为指针只是长度为1的队列,所以只需要1。

如果您使用的是POSIX系统,请考虑不使用实时信号进行信号传输,因为它们会排队。使用常规信号将允许您处理信号的次数相同,只需信号一次,直到信号被处理,而实时信号排队。定期唤醒以检查队列还允许您在检查是否有任何新数据包之后但在致电pause等待信号之前,处理最后一个信号到达的可能性。

答案 3 :(得分:1)

另一个想法是拥有一个专用的读取器进程,它只对套接字进行循环,并将传入的数据包读入共享内存中的循环缓冲区(您必须担心正确的写入顺序)。像kfifo这样的东西。非阻塞在这里也很好。新数据会覆盖旧数据。然后,其他进程将始终可以访问队列的最新块和尚未覆盖的所有先前块。

对于简单的单向阅读器来说可能太复杂了,只是一种选择。

答案 4 :(得分:1)

我很确定这是一个与Two Army Problem密切相关的可证明不可解决的问题。

我可以想到一个肮脏的解决方案:建立一个TCP“控制”边带连接,它携带最后一个数据包,这也是一个“结束传输”指示。否则,您需要使用Engineering Approaches中提到的一种更通用的实用方法。

答案 5 :(得分:0)

这是一个古老的问题,但是您基本上想将套接字队列(FIFO)变成堆栈(LIFO)。除非您想弄弄内核,否则不可能。

您需要将数据报从内核空间移至用户空间,然后进行处理。最简单的方法是像这样的循环...

  1. 阻塞直到套接字上有数据(请参阅选择,轮询,epoll)
  2. 排空套接字,根据您自己的选择策略存储数据报
  3. 处理存储的数据报
  4. 重复