select() - writefds 和 exceptfds 与非阻塞 TCP 套接字的实际使用?

Jus*_*tin 2 c sockets networking tcp

根据Linux man pagesselect支持三种唤醒事件:

  • readfds 将被监视以查看字符是否可供阅读
  • writefds 将被监视以查看是否有空间可用于写入
  • exceptfds 将被监视异常

在网上和网络书籍中寻找 TCP 套接字的实际使用示例时,我大多只看到readfds被使用,即使代码稍后尝试写入套接字。

但是套接字可能还没有准备好写入,因为我们可能只在readfs集合中接收到它而不是在writefds集合中。为了避免写阻塞,我通常将套接字的 fd 设置为非阻塞模式。然后,如果send失败了,我可能只是队列中的数据到一些内部缓冲区,稍后发送出去(这意味着-未来当时间select()readfs唤醒)。但这似乎很危险——如果下一次readfs唤醒来得更晚并且要写入的数据只是坐在我们的缓冲区中等待,理论上,永远会怎样?

Apple 的文档还建议使用writefdsUsing Sockets and Socket Streams,请参阅“使用纯 POSIX 代码处理事件”部分,引用:

在循环中调用 select,为读取和写入 描述符集传递该文件描述符集(通过调用 FD_COPY 创建)的两个单独副本

所以问题是:

  1. Apple 是否writefds仅仅因为它是“正确的官方方式”而建议使用它,或者也许还有其他方法可以在没有writefds. 苹果的建议对我来说似乎很可疑。如果我们writefds从一开始就将socket放入,然后一段时间不写入,会不会select()因为socket可写而立即唤醒(那是因为我们还没有写入)?

  2. 关于exceptfds- 我还没有看到任何使用 TCP 套接字的例子。我读过它用于带外数据。这是否意味着exceptfds如果我只处理主流 Internet 流量(例如 HTTP、音频/视频流、游戏服务器等),我可以忽略TCP 套接字?

Jer*_*ner 5

Apple 是否建议使用 writefds 只是因为它是“正确的官方方式”,或者也许还有其他方法可以在没有 writefds 的情况下处理套接字写入?

另一种方法(你在你看过的教程中看到的)是假设写缓冲区总是足够大,可以立即保存你想发送给它的任何数据,只要你需要就盲目地调用 send()到。

它简化了代码,但它不是一个很好的方法——也许它对于玩具/示例程序来说已经足够好了,但我不想在生产质量的代码中做出这样的假设,因为这意味着如果/当您的程序一次生成足够的数据来填充套接字的输出缓冲区时。根据您(错误)如何处理对 send() 的调用,您的程序将进入自旋循环(调用 send() 并获得 EWOULDBLOCK,一遍又一遍,直到最终有足够的空间来放置所有数据) ,或错误输出(如果您将 EWOULDBLOCK/short-send() 视为致命错误条件),或删除一些传出数据字节(如果您只是完全忽略了 send() 的返回值)。这些都不是处理全输出缓冲区情况的优雅方式。

如果我们从一开始就将套接字放入 writefds 然后一段时间不写入它,则 select() 不会因为套接字可写而立即唤醒(那是因为我们还没有写入它) )?

是的,绝对 - 这就是为什么如果您当前有一些要写入 socket 的数据,您只会将 socket 放入 writefds 集。如果您当前没有要写入套接字的数据,则可以将套接字排除在 writefds 之外,以便 select() 不会立即返回。

关于exceptfds -我还没有看到任何使用它与TCP套接字的例子。我读过它用于带外数据。

通常,exceptfds 的用途并不多(TCP 的带外数据功能也不是,AFAIK)。我见过它的唯一一次使用是在 Windows 下进行异步/非阻塞 TCP 连接时——当异步/非阻塞 TCP 连接尝试失败时,Windows 使用 exceptfds 唤醒 select()。

然后,如果发送失败,我可以将数据排入某个内部缓冲区并稍后将其发送出去(这意味着 - 下次带有 readfs 的 select() 唤醒时)。但这似乎很危险——如果下一次 readfs 唤醒来得晚得多并且要写入的数据只是坐在我们的缓冲区中等待,理论上,永远?

由于 TCP 会自动减慢发送方的传输速度,以大致以接收方接收的速率进行传输,因此接收程序可能会简单地停止调用 recv(),最终将发送方的传输速率降低到零。或者,发送方和接收方之间的网络可能会开始丢弃如此多的数据包,以至于传输速率实际上变为零,即使接收方像预期的那样调用 recv() 也是如此。在任何一种情况下,这都意味着您的排队数据很可能会在您的传出数据缓冲区中停留很长时间——在后一种情况下可能不会永远存在,因为完全陷入困境的 TCP 连接最终会出错;在前一种情况下,您需要调试接收方而不是发送方。

真正的问题是当您的发送方生成数据的速度比接收方接收数据的速度快(或者,换句话说,比网络传输数据的速度快)——在这种情况下,如果您正在排队“多余”的数据进入发送方的 FIFO,该 FIFO 可以无限增长,直到最终您的发送进程因内存耗尽而崩溃——这绝对不是理想的行为。

有几种方法可以处理;一种方法是简单地监控当前保存在 FIFO 中的字节数,当它达到某个阈值(例如一兆字节或其他什么;什么构成“合理”阈值将取决于您的应用程序正在做什么)时,服务器可以决定客户端根本不能很好地执行并关闭发送套接字以进行自卫(当然并释放相关联的 FIFO 队列)。这在很多情况下都很有效,尽管如果您的服务器瞬间生成/排队的数据量超过该数量,它可能会出现误报,并最终以不当方式断开实际运行良好的客户端。

另一种方法(我喜欢,如果可能)是设计服务器,使得其为一个插座产生更多的输出数据时,目前还没有输出数据排队用于该套接字。即当套接字选择为准备写入时,尽可能多地将现有数据从 FIFO 队列中排出到套接字中。当 FIFO 队列为空并且您有要从中生成传出字节的数据并且套接字准备好写入时,是生成更多输出数据字节并将它们放入 FIFO 队列的唯一时间。永远重复这个过程,无论客户端有多慢,您的 FIFO 队列的大小永远不会大于您在一次生成更多数据字节步骤迭代中生成的数据量。

  • 选择可写性只能在 EAGAIN/EWOULDBLOCK 之后使用。否则当你有东西要写的时候,就马上写下来,然后检查。通过 `select()` 这样做只是浪费时间。您的最后一段似乎有些循环:只有在有空间时才生成输出,只有在有输出时才测试空间。 (2认同)