简体   繁体   English

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

[英]select() - practical use of writefds and exceptfds with non-blocking TCP sockets?

According to Linux man pages , select supports three kinds of events for waking up:根据Linux man pagesselect支持三种唤醒事件:

  • readfds will be watched to see if characters become available for reading readfds将被监视以查看字符是否可供阅读
  • writefds will be watched to see if space is available for write将监视writefds以查看是否有空间可用于写入
  • exceptfds will be watched for exceptions将监视exceptfds是否有异常

While looking for practical use examples with TCP sockets online and in networking books, I mostly see only readfds being used, even if the code tries to write to the socket later.在网上和网络书籍中寻找 TCP 套接字的实际使用示例时,我大多只看到使用readfds ,即使代码稍后尝试写入套接字。

But the socket might be not ready for writing because we might have received it only in readfs set but not in writefds set.但是套接字可能还没有准备好写入,因为我们可能只在readfs集中而不是在writefds集中接收到它。 To avoid blocking on writes, I usually set the fd of the socket in non-block mode.为了避免写阻塞,我通常将套接字的 fd 设置为非阻塞模式。 Then if send fails, I could just queue the data into some internal buffer and send it out later (which means - next time when select() with readfs wakes up).然后,如果send失败,我可以将数据排入某个内部缓冲区并稍后将其发送出去(这意味着 - 下次带有readfs select()唤醒时)。 But this seems dangerous - what if next readfs wakeup comes much later and the data to be written just sits in our buffer waiting, theoretically, forever?但这似乎很危险——如果下一次readfs唤醒来得晚得多并且要写入的数据只是坐在我们的缓冲区中等待,理论上,永远?

Apple's documentation also recommends to use writefds : Using Sockets and Socket Streams , see section "Handling Events with Pure POSIX Code", quoting: Apple 的文档还建议使用writefdsUsing Sockets and Socket Streams ,请参阅“使用纯 POSIX 代码处理事件”部分,引用:

Call select in a loop, passing two separate copies of that file descriptor set (created by calling FD_COPY) for the read and write descriptor sets.在循环中调用 select,为读取和写入描述符集传递该文件描述符集(通过调用 FD_COPY 创建)的两个单独副本

So the questions are:所以问题是:

  1. Is Apple recommending to use writefds just because it's "the right official way" or maybe there are other approaches how to deal with socket writes without writefds ? Apple 是否建议使用writefds只是因为它是“正确的官方方式”,或者也许还有其他方法可以在没有writefds情况下处理套接字写入? And Apple's recommendation seems suspicious to me.苹果的建议对我来说似乎很可疑。 If we put the socket into writefds from the very start and then don't write to it for some time, won't select() wake up immediately just because the socket is writable (and that's because we haven't written to it yet)?如果我们从一开始就将套接字放入writefds然后一段时间不写入它,则select()不会因为套接字可写而立即唤醒(那是因为我们还没有写入它) )?

  2. About exceptfds -I haven't yet seen any examples using it with TCP sockets.关于exceptfds -我还没有看到任何使用它与TCP套接字的例子。 I have read that it is used for out-of-band data.我读过它用于带外数据。 Does that mean that I can ignore exceptfds for TCP sockets if I deal only with mainstream Internet traffic, such as HTTP, audio/video streaming, game servers etc.?这是否意味着如果我只处理主流 Internet 流量,例如 HTTP、音频/视频流、游戏服务器等,我可以忽略 TCP 套接字的exceptfds

Is Apple recommending to use writefds just because it's "the right official way" or maybe there are other approaches how to deal with socket writes without writefds? Apple 是否建议使用 writefds 只是因为它是“正确的官方方式”,或者也许还有其他方法可以在没有 writefds 的情况下处理套接字写入?

The other approach (that you saw in the tutorials you looked at) is to assume that the write-buffer will always be large enough to immediately hold whatever data you want to send out to it, and just blindly call send() whenever you need to.另一种方法(你在你看过的教程中看到的)是假设写缓冲区总是足够大,可以立即保存你想发送给它的任何数据,只要你需要就盲目地调用 send()到。

It simplifies the code, but it's not a very good approach -- maybe it's good enough for a toy/example program, but I wouldn't want to make that assumption in production-quality code, because it means something bad will happen if/when your program generates enough data at once to fill the socket's output buffer.它简化了代码,但它不是一个很好的方法——也许它对于玩具/示例程序来说已经足够好了,但我不想在生产质量的代码中做出这样的假设,因为这意味着如果/当您的程序一次生成足够的数据来填充套接字的输出缓冲区时。 Depending on how you (mis)handled the call to send(), either your program would go into a spin loop (calling send() and getting EWOULDBLOCK, over and over again, until there was finally enough room to place all the data), or error out (if you treated EWOULDBLOCK/short-send() as a fatal error condition), or drop some of the outgoing data bytes (if you were just ignoring send()'s return value completely).根据您(错误)如何处理对 send() 的调用,您的程序将进入自旋循环(调用 send() 并获得 EWOULDBLOCK,一遍又一遍,直到最终有足够的空间来放置所有数据) ,或错误输出(如果您将 EWOULDBLOCK/short-send() 视为致命错误条件),或删除一些传出数据字节(如果您只是完全忽略了 send() 的返回值)。 None of these is a graceful way to handle the full-output-buffer situation.这些都不是处理全输出缓冲区情况的优雅方式。

If we put the socket into writefds from the very start and then don't write to it for some time, won't select() wake up immediately just because the socket is writable (and that's because we haven't written to it yet)?如果我们从一开始就将套接字放入 writefds 然后一段时间不写入它,则 select() 不会因为套接字可写而立即唤醒(那是因为我们还没有写入它) )?

Yes, absolutely -- which is why you would only place the socket into the writefds set if you currently have some data that you want to write to the socket .是的,绝对 - 这就是为什么如果您当前有一些要写入 socket 的数据,您只会将 socket 放入 writefds 集。 In the case where you currently have no data that you want to write to the socket, you'd leave the socket out of writefds so that select() wouldn't immediately return.如果您当前没有要写入套接字的数据,则可以将套接字排除在 writefds 之外,以便 select() 不会立即返回。

About exceptfds -I haven't yet seen any examples using it with TCP sockets.关于exceptfds -我还没有看到任何使用它与TCP套接字的例子。 I have read that it is used for out-of-band data.我读过它用于带外数据。

Generally exceptfds isn't used for much (neither is TCP's out-of-band data feature, AFAIK).通常,exceptfds 的用途并不多(TCP 的带外数据功能也不是,AFAIK)。 The only other time I've seen it used is when doing asynchronous/non-blocking TCP connects under Windows -- Windows uses the exceptfds to wake up select() when an asynchronous/non-blocking TCP connect attempt has failed.我见过它的唯一一次使用是在 Windows 下进行异步/非阻塞 TCP 连接时——当异步/非阻塞 TCP 连接尝试失败时,Windows 使用 exceptfds 唤醒 select()。

Then if send fails, I could just queue the data into some internal buffer and send it out later (which means - next time when select() with readfs wakes up).然后,如果发送失败,我可以将数据排入某个内部缓冲区并稍后将其发送出去(这意味着 - 下次带有 readfs 的 select() 唤醒时)。 But this seems dangerous - what if next readfs wakeup comes much later and the data to be written just sits in our buffer waiting, theoretically, forever?但这似乎很危险——如果下一次 readfs 唤醒来得晚得多并且要写入的数据只是坐在我们的缓冲区中等待,理论上,永远?

Since TCP automatically slows the sender down to transmit at roughly the rate the receiver receives it at, it certainly is possible that the receiving program could simply stop calling recv(), eventually reducing the sender's transmission rate to zero.由于 TCP 会自动减慢发送方的传输速度,以大致以接收方接收的速率进行传输,因此接收程序可能会简单地停止调用 recv(),最终将发送方的传输速率降低到零。 Or, alternatively, the network in between the sender and the receiver could start dropping so many packets that the transmission rate becomes effectively zero, even though the receiver is calling recv() like it is supposed to.或者,发送方和接收方之间的网络可能会开始丢弃如此多的数据包,以至于传输速率实际上为零,即使接收方像预期的那样调用 recv() 也是如此。 In either case, that would mean that your queued data could very well sit in your outgoing-data buffer for a long time -- probably not forever in the latter case, since a completely bogged down TCP connection will eventually error out;在任何一种情况下,这都意味着您的排队数据很可能会在您的传出数据缓冲区中停留很长时间——在后一种情况下可能不会永远存在,因为完全陷入困境的 TCP 连接最终会出错; and in the former case you need to debug the receiving side more than the sending side.在前一种情况下,您需要调试接收方而不是发送方。

The real problem comes when your sender is generating data faster than your receiver can receive it (or, to put it another way, faster than the network can transport it) -- in that case, if you're queueing the "excess" data into a FIFO on the sender's side, that FIFO could grow without bound until eventually your sending process crashes due to memory exhaustion -- definitely not desirable behavior.真正的问题是当您的发送方生成数据的速度比接收方接收数据的速度快(或者,换句话说,比网络传输数据的速度快)——在这种情况下,如果您正在排队“多余”的数据进入发送方的 FIFO,该 FIFO 可以无限增长,直到最终您的发送进程因内存耗尽而崩溃——这绝对不是理想的行为。

There are several ways to handle that;有几种方法可以处理; one way would be to simply monitor the number of bytes currently held in the FIFO, and when it reaches a certain threshold (eg one megabyte or something; what constitutes a "reasonable" threshold would depend on what your app is doing), the server could decide that the client simply can't perform well enough and close the sending socket in self-defense (and free up the associated FIFO queue of course).一种方法是简单地监控当前保存在 FIFO 中的字节数,当它达到某个阈值(例如一兆字节或其他什么;什么构成“合理”阈值将取决于您的应用程序正在做什么)时,服务器可以决定客户端根本不能很好地执行并关闭发送套接字以进行自卫(当然并释放相关联的 FIFO 队列)。 That works well in a lot of cases, although if your server ever generated/enqueued more than that amount of data instantaneously, it might suffer from false positives, and end up inappropriately disconnecting clients that were actually performing well.这在很多情况下都很有效,尽管如果您的服务器瞬间生成/排队的数据量超过该数量,它可能会出现误报,并最终以不当方式断开实际运行良好的客户端。

Another approach (which I prefer, when possible) is to design the server so that it only generates more output data for a socket when there is currently no output-data queued up for that socket.另一种方法(我喜欢,如果可能)是设计服务器,使得其在插座产生更多的输出数据时,目前还没有输出数据排队用于该套接字。 ie when the socket selects as ready-for-write, drain as much existing data as you can from the FIFO queue into the socket.即当套接字选择为准备写入时,尽可能多地将现有数据从 FIFO 队列中排出到套接字中。 When the FIFO queue is empty and you have data you want to generate outgoing-bytes from and the socket is ready-for-write, that is the only time to generate some more output data bytes and place them into the FIFO queue.当 FIFO 队列为空并且您有要从中生成传出字节的数据并且套接字准备好写入时,是生成更多输出数据字节并将它们放入 FIFO 队列的唯一时间。 Repeat that forever, and your FIFO queue's size will never be greater than the amount of data you generated in one iteration of your generate-more-data-bytes step, no matter how slow the client is.永远重复这个过程,无论客户端有多慢,您的 FIFO 队列的大小永远不会大于您在一次生成更多数据字节步骤迭代中生成的数据量。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM