如果我理解正确,您有一些实时数据源,想要将其复用到多个套接字。您已经连接了一个“源”管道到任何产生数据的地方,并且对于每个您希望发送数据的套接字都有一个“目标”管道。您正在使用
tee()
从源管道复制数据到每个目标管道,并使用
splice()
将其从目标管道复制到套接字本身。
您将遇到的根本问题是,如果其中一个套接字无法跟上 - 如果您产生的数据比您发送的速度更快,那么您将会遇到问题。这与您使用管道无关,这只是一个根本性的问题。因此,您需要选择一种策略来应对这种情况 - 即使您不希望它很常见,我建议您处理这个问题,因为这些问题通常会在以后困扰您。您的基本选择是关闭有问题的套接字,或者跳过数据直到它清除其输出缓冲区 - 后者可能更适合音频/视频流等情况。
然而,与你使用管道相关的问题是,在Linux上管道缓冲区的大小有些不太灵活。从Linux 2.6.11开始,默认为64K(自Linux 2.6.17中添加了tee()
调用)-请参见pipe manpage。自2.6.35以来,可以通过将F_SETPIPE_SZ
选项传递给fcntl()
(请参见fcntl manpage),将此值更改到由/proc/sys/fs/pipe-size-max
指定的限制,但缓冲仍比在用户空间中动态分配的方案更难以按需更改。这意味着你应对慢速套接字的能力会受到一定限制,能否接受这种情况取决于你期望接收和发送数据的速率。
假设这个缓冲策略是可以接受的,你在正确地假设需要跟踪每个目标管道从源头消耗了多少数据,只有所有目标管道都消耗了数据,才能安全地丢弃数据。这有点复杂,因为tee()没有偏移量的概念——你只能从管道的开头复制。这意味着你只能以最慢的套接字的速度进行复制,因为在一些数据被从源头消耗之前,你不能使用tee()将数据复制到目标管道中,而在所有套接字都拥有即将消耗的数据之前,你也不能这样做。
如何处理这个问题取决于你的数据的重要性。如果你真的需要tee()和splice()的速度,并且你相信慢速套接字是极为罕见的事件,你可以像这样做(我假设你正在使用非阻塞IO和单个线程,但类似的方法也适用于多个线程):
- 确保所有管道都是非阻塞的(使用
fcntl(d, F_SETFL, O_NONBLOCK)
使每个文件描述符都变为非阻塞)。
- 为每个目标管道初始化一个
read_counter
变量,初始值为零。
- 使用类似epoll()的方法等待源管道中有数据。
- 循环遍历所有
read_counter
为零的目标管道,调用tee()
函数将数据传输到每个管道。确保在flags参数中传递SPLICE_F_NONBLOCK
。
- 通过
tee()
传输的数据量增加到每个目标管道的read_counter
变量中,并跟踪最小值。
- 找到
read_counter
的最小值 - 如果这个值不为零,则从源管道中丢弃相应数量的数据(例如使用一个打开在/dev/null
上的目标进行splice()
调用)。在丢弃数据之后,从所有管道的read_counter
中减去已丢弃的数量(因为这是最小值,所以这不会导致任何一个管道的计数器变为负数)。
- 返回第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()
更受限制,但可能更具可移植性。如果你真的希望代码具有可移植性,请坚持使用用户空间缓冲。
如果有任何我涉及的内容需要更详细的解释,请告诉我。
F_SETPIPE_SZ
,谢谢!我已经编辑了我的回答。请记住,2.6.35 仍然有点新(例如,Ubuntu 10.04 LTS 是 2.6.32 AFAIK)。只要您不介意针对 Linux,这种方法似乎是没问题的。我建议尽可能限制代码范围,以免出现针对 Linux 特定的情况。还要记住的是,这只是解决方案的一个方面——如果性能很重要,建议尝试使用非阻塞 IO vs. 线程 vs. 进程来看哪个最适合您。管道的好处之一是,如果需要,它们在fork()
跨进程时可正常工作。 - Cartroo