使用pipe道将数据发送到多个套接字,tee()和splice()

我正在用tee()复制一个“master”pipe道,使用splice()写入多个套接字。 自然,这些pipe道将以不同的速率清空,具体取决于我可以拼接()到目标套接字的多less。 所以,当我下一步将数据添加到“主”pipe道,然后再次发球时,我可能会遇到这样的情况:我可以向pipe道写入64KB,但只有4KB到“从”pipe道。 我在猜测,如果我拼接()所有的“主”pipe道的套接字,我将永远不能tee()剩余的60KB到奴隶pipe道。 真的吗? 我想我可以跟踪一个tee_offset(从0开始),我将其设置为“不确定”数据的开始,然后不拼接()。 所以在这种情况下,我会设置tee_offset为4096,而不是拼接更多,直到我能够开球到所有其他pipe道。 我在这里的正确轨道? 任何提示/警告给我?

如果我理解正确,那么您已经获得了一些您想要复用到多个套接字的实时数据源。 你有一个单一的“源”管道连接到任何产生你的数据,你有发送数据的每个套接字的“目标”管道。 你正在做的是使用tee()将数据从源管道复制到每个目标管道,并将splice()从目标管道复制到套接字本身。

你打算在这里遇到的根本问题是,如果其中一个套接字根本无法跟上 – 如果你生成的数据比你发送数据的速度快,那么你将会遇到问题。 这与你使用管道无关,这只是一个根本性的问题。 所以,在这种情况下,你应该选择一个策略来应对 – 即使你不希望这种情况很普遍,我也建议你处理这个问题,因为这些事情往往会在以后出现。 您的基本选择是关闭冒犯的套接字,或者在清除输出缓冲区之前跳过数据 – 例如,后一种选择可能更适合音频/视频流。

但是,与使用管道有关的问题是,在Linux上,管道缓冲区的大小有些不灵活。 由于Linux 2.6.11(在2.6.17添加了tee()调用tee()因此它默认为64K – 请参阅管道联机帮助页 。 从2.6.35开始,这个值可以通过F_SETPIPE_SZ选项改变为fcntl() (参见fcntl手册页 ),直到由/proc/sys/fs/pipe-size-max指定的限制,但是缓冲对于在用户空间的动态分配方案将按需更改。 这意味着你处理缓慢套接字的能力将受到一定的限制 – 这是否可以接受取决于你期望接收和发送数据的速率。

假设这个缓冲策略是可以接受的,那么假设你需要跟踪每个目标管道从源端消耗了多少数据,并且放弃所有目标管道消耗的数据是安全的。 这是因为tee()没有偏移的概念 – 你只能从管道的开头复制。 这样做的结果是,你只能以最慢的套接字的速度复制,因为你不能使用tee()复制到目标管道,直到某些数据已经从源消耗完,做到这一点,直到所有的套接字都有你要使用的数据。

你如何处理这取决于你的数据的重要性。 如果你真的需要tee()splice()的速度,并且你确信一个缓慢的套接字将是一个非常罕见的事件,你可以做这样的事情(我假设你正在使用非阻塞IO和一个单一的线程,但类似的东西也适用于多个线程):

  1. 确保所有管道都是非阻塞的(使用fcntl(d, F_SETFL, O_NONBLOCK)使每个文件描述符都是非阻塞的)。
  2. 初始化每个目标管道的read_counter变量为零。
  3. 使用像epoll()等东西,直到源管道中有东西。
  4. 循环read_counter为零的所有目标管道,调用tee()将数据传输到每个管道。 确保在标志中传递SPLICE_F_NONBLOCK
  5. 通过tee()传输的数量为每个目标管道增加read_counter 。 跟踪最低的结果值。
  6. 找到read_counter的最小结果值 – 如果这个值不是零,则从源管道中丢弃该数据量(例如,使用在/dev/null上打开目的地的splice()调用)。 丢弃数据后,从所有管道上减去从read_counter丢弃的数量(因为这是最低的值,那么这不会导致它们中的任何一个变为负值)。
  7. 从第3步开始重复。

注意:过去的一件事情是, SPLICE_F_NONBLOCK影响管道上的tee()splice()操作是否为非阻塞操作,而用fnctl()设置的O_NONBLOCK会影响与其他调用例如read()write() )是非阻塞的。 如果你想要一切都是非阻塞的,请同时设置。 还要记住要使你的套接字非阻塞,或者splice()调用将数据传递给它们可能会阻塞(除非这是你想要的,如果你使用的是线程方法)。

正如你所看到的,这个策略有一个主要的问题 – 只要一个套接字阻塞,一切都停止 – 那个套接字的目标管道将被填满,然后源管道将停滞。 所以,如果你到达tee()在第4步中返回EAGAIN的阶段,那么你将要关闭这个套接字,或者至少“断开”它(即把它从你的循环中取出),这样你就不会写直到它的输出缓冲区是空的。 你选择哪一个取决于你的数据流是否可以从跳过的位中恢复。

如果你想要更好地处理网络延迟,那么你将需要做更多的缓冲,这将涉及用户空间缓冲区(这相当于否定tee()splice() )的优点,或者可能基于磁盘的缓冲区。 基于磁盘的缓冲几乎肯定会比用户空间缓冲慢得多,因此不适合,因为大概你想要很多速度,因为你首先选择了tee()splice() ,但是我提到为了完整性。

有一点值得注意的是,如果最终从用户空间插入数据的任何一点是vmsplice()调用,可以执行vmsplice()调用类似的方式从用户空间“管道输出”到管道。 这可能是有用的,如果你做了足够的缓冲,你已经分割你的数据在多个不同的分配缓冲区(例如,如果你使用池分配器的方法)。

最后,你可以想象在使用tee()splice()的“快速”方案之间交换套接字,如果它们跟不上,就把它们移到较慢的用户空间缓冲区。 这将使你的实现复杂化,但是如果你处理大量的连接,并且只有很小的一部分连接速度很慢,那么你仍然会减少复制到用户空间的数量。 然而,这只是一个短暂的措施来应对短暂的网络问题 – 正如我原先所说,如果你的套接字比你的源头慢,你就有一个基本的问题。 你最终会达到一些缓冲限制,需要跳过数据或关闭连接。

总的来说,我会仔细考虑为什么你需要tee()splice()的速度,以及对于你的用例来说,在内存还是磁盘上的简单的用户空间缓冲更合适。 如果你确信速度总是很高,而且有限的缓冲是可以接受的,那么我上面所述的方法应该是可行的。

另外,我应该提到的一件事是,这将使您的代码非常特定于Linux – 我不知道这些调用在其他Unix变体中得到支持。 sendfile()调用比splice()更受限制,但可能相当便于携带。 如果你真的想要东西是可移植的,坚持用户空间缓冲。

让我知道是否有什么我已经涵盖了你想要更多的细节。