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

时间:2017-02-01 17:19:18

标签: 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的套接字写入? Apple的建议对我来说似乎很可疑。如果我们从一开始就将套接字放入writefds然后再写一段时间,那么就不会因为套接字是可写的而立即唤醒select()(和那是因为我们还没有写到它?)

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

2 个答案:

答案 0 :(得分:3)

  

Apple是否建议使用writefds只是因为它是正确的   官方方式"或者还有其他方法可以处理   socket写没有writefds?

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

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

  

如果我们从一开始就将套接字放入writefds,那就不要了   写一段时间,不要选择()立刻叫醒   因为套接字是可写的(因为我们还没有写过   它呢?

是的,绝对 - 这就是为什么你只将套接字放入writefds set ,如果你当前有一些你想要写入套接字的数据。如果您当前没有要写入套接字的数据,则不要将套接字从writefds中删除,以便select()不会立即返回。

  

关于exceptfds - 我还没有看到任何与TCP一起使用它的例子   插座。我已经读过它用于带外数据。

通常除了fds之外没有使用太多(TCP的带外数据功能,AFAIK也没有)。我唯一看到它使用的另一个时间是在Windows下进行异步/非阻塞TCP连接 - 当异步/非阻塞TCP连接尝试失败时,Windows使用exceptfds唤醒select()。 / p>

  

然后,如果发送失败,我可以将数据排入某个内部   缓冲区并稍后发送(这意味着 - 下次select()   随着readfs醒来)。但这似乎很危险 - 如果下次读取会怎样   唤醒来得晚得多,要写的数据就在我们的   缓冲等待,理论上,永远?

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

当您的发送方生成数据的速度快于接收方可以接收数据时(或者换句话说,比网络传输速度更快),真正的问题就出现了 - 在这种情况下,如果您排队等待"多余"数据进入发送方的FIFO,FIFO可以无限制地增长,直到最终你的发送过程由于内存耗尽而崩溃 - 绝对不是理想的行为。

有几种方法可以解决这个问题;一种方法是简单地监视当前保存在FIFO中的字节数,以及何时达到某个阈值(例如,一兆字节或某种东西;什么构成一个"合理的"阈值将取决于您的应用程序是什么在这样做时,服务器可以决定客户端是否能够很好地执行并且自卫地关闭发送套接字(并且当然释放相关的FIFO队列)。这在很多情况下运行良好,但如果您的服务器瞬间生成/排队的数据量超过该数据量,则可能会出现误报,最终会不正确地断开实际运行良好的客户端。

另一种方法(我更喜欢,在可能的情况下)是设计服务器,以便当为当前没有为该套接字排队的输出数据时,为套接字生成更多输出数据。即当套接字选择为准备写入时,尽可能多地将现有数据从FIFO队列中排出到套接字中。当FIFO队列为空时,你想要从生成输出字节的数据,套接字是可以写的, 是唯一一次生成更多输出数据字节并将它们放入FIFO队列。永远重复这一点,无论客户端有多慢,您的FIFO队列大小都不会大于您在generate-more-data-bytes步骤的一次迭代中生成的数据量。

答案 1 :(得分:-1)

select返回时,readfdswritefds会被修改为显示select末尾的状态:

  

退出时,会对这些集进行修改,以指示哪些文件描述符实际上已更改状态。

检查每组中的每个fd并采取相应的行动:读取是否可以读取,写入是否可以写入。

这带来了一个问题:我们如何避免select()的{​​{1}}立即返回?在许多情况下,插槽大部分时间都可以写入。您必须管理writefds

来自@EJP的评论更新:

如果您遇到短写或writefds / writefds,只对任何单个套接字使用EAGAIN,告诉您套接字何时再次可写,并停止使用它作为发送完成后很快就会成功。