c-使用管道、tee()和splice()将数据发送到多个套接字



我用tee()复制一个"master"管道,用splice()写入多个套接字。当然,这些管道会以不同的速率清空,这取决于我能将()连接到目标套接字的数量。因此,当我下一次将数据添加到"主"管道,然后再次tee()时,我可能会遇到这样的情况:我可以向管道写入64KB,但只能向其中一个"从"管道写入4KB。我猜,如果我把所有的"主"管道()都连接到套接字上,我将永远无法把剩下的60KB()连接到那个从管道上。这是真的吗?我想我可以跟踪tee_offset(从0开始),我将其设置为"未测试"数据的开头,然后不拼接()。所以在这种情况下,我会将tee_offset设置为4096,并且在我能够将其全部拼接到其他管道之前,不拼接更多。我在这里走对了吗?有什么提示/警告吗?

如果我理解正确,您已经获得了一些实时数据源,您希望将其多路传输到多个套接字。您有一个连接到产生数据的任何东西的"源"管道,并且您为每个要发送数据的套接字都有一个"目的地"管道。您要做的是使用tee()将数据从源管道复制到每个目标管道,使用splice()将数据从目标管道复制到套接字本身。

你在这里要遇到的根本问题是,如果其中一个套接字根本跟不上——如果你生成数据的速度快于发送数据的速度,那么你就会遇到问题。这与管道的使用无关,这只是一个根本问题。因此,在这种情况下,你需要选择一种策略来应对——我建议你处理这种情况,即使你不认为它很常见,因为这些事情后来经常会咬到你。您的基本选择是关闭有问题的套接字,或者跳过数据,直到清除其输出缓冲区——例如,后一种选择可能更适合音频/视频流。

然而,与管道的使用有关的问题是,在Linux上,管道缓冲区的大小有些不灵活。自Linux 2.6.11(在2.6.17中添加了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. 将每个目标管道的read_counter增加tee()传输的量。跟踪最低的结果值
  6. 查找read_counter的最低结果值-如果该值为非零,则从源管道中丢弃该数据量(例如,使用在/dev/null上打开目的地的splice()调用)。丢弃数据后,从所有管道上的read_counter中减去丢弃的量(由于这是最低值,因此这不会导致任何管道变为负值)
  7. 从步骤3开始重复

注意:过去有一件事让我很困惑,那就是SPLICE_F_NONBLOCK会影响管道上的tee()splice()操作是否是非阻塞的,而用fnctl()设置的O_NONBLOCK会影响与其他调用(例如read()write())的交互是否是非阻止的。如果希望所有内容都是非阻塞的,请将两者都设置。还要记住,要使套接字不阻塞,否则向其传输数据的splice()调用可能会阻塞(如果使用线程方法,除非这是您想要的)。

正如你所看到的,这种策略有一个主要问题——只要一个套接字阻塞,所有东西都会停止——该套接字的目标管道就会充满,然后源管道就会停滞。因此,如果您在步骤4中达到tee()返回EAGAIN的阶段,那么您将希望关闭该套接字,或者至少"断开"它(即将它从循环中取出),这样您就不会向它写入任何其他内容,直到它的输出缓冲区为空。您的选择取决于您的数据流是否可以从跳过的部分中恢复。

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

如果您最终在任何时候从用户空间插入数据,有一件事值得注意,那就是vmsplice()调用,它可以以类似于writev()调用的方式从用户空间向管道中执行"收集输出"。如果您进行了足够的缓冲,从而在多个不同分配的缓冲区中分割数据,这可能会很有用(例如,如果您使用池分配器方法)。

最后,您可以想象在使用tee()splice()的"快速"方案之间交换套接字,如果它们无法跟上,则将它们转移到较慢的用户空间缓冲中。这会使您的实现复杂化,但如果您正在处理大量连接,而其中只有很小一部分连接速度较慢,那么您仍然会在一定程度上减少向用户空间的复制量。然而,这只是处理瞬态网络问题的短期措施——正如我最初所说,如果套接字比源慢,就会出现根本问题。您最终会达到缓冲限制,需要跳过数据或关闭连接。

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

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

让我知道,如果有什么我已经报道了,你想了解更多细节。

最新更新