Epoll zero recv() and negative(EAGAIN) send()

Epoll zero recv() and negative(EAGAIN) send()

本文关键字:EAGAIN send negative and zero Epoll recv      更新时间:2023-10-16

我最近几天一直在与epoll作斗争,我现在在偏僻的地方;)

互联网上有很多信息,显然在系统人员中,但我可能服用过量并且有点困惑。

在我的服务器应用程序(nginx后端)中,我正在等待来自ET模式的客户端的数据:

event_template.events = EPOLLIN | EPOLLRDHUP | EPOLLET

当我注意到nginx以502响应时,一切都变得奇怪,尽管我可以看到成功的send()在我这边。我跑铁丝鲨嗅探并意识到我的服务器将(尝试并获取RST)数据发送到网络上的另一台机器。因此,我决定套接字描述符无效,这是一种"未定义的行为"。最后,我发现在第二个 recv() 上我得到零字节,这意味着必须关闭连接,并且不允许我再发送数据。尽管如此,我从epoll得到的不仅仅是EPOLLIN,而是EPOLLRDHUP。

问题:当 recv() 返回零并在 EPOLLRDHUP 处理期间稍后返回 shutdown(SHUT_WR) 时,我是否必须关闭套接字只是为了读取?

简而言之,从套接字读取:

std::array<char, BatchSize> batch;
ssize_t total_count = 0, count = 0;
do {
count = recv(_handle, batch.begin(), batch.size(), MSG_DONTWAIT);
if (0 == count && 0 == total_count) {
/// @??? Do I need to wait zero just on first iteration?
close();
return total_count;
} else if (count < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/// @??? Will be back with next EPOLLIN?!
break ;
}
_last_error = errno;
/// @brief just log the error               
return 0;
}
if (count > 0) {
total_count += count;
/// DATA!
if (count < batch.size()) {
/// @??? Received less than requested - no sense to repeat recv, otherwise I need one more turn?! 
return total_count;
}
}           
} while (count > 0);

可能,我的一般错误是尝试在无效的套接字描述符上发送数据,以后发生的一切只是结果。但是,我继续挖掘;)我的问题的第二部分也是关于以MSG_DONTWAIT模式写入套接字。

据我现在所知,send() 也可能返回 -1 和 EAGAIN,这意味着我应该订阅 EPOLLOUT 并等待内核缓冲区足够空闲以从我的我那里接收一些数据。这是对的吗?但是,如果客户不会等这么久怎么办?或者,我可以调用阻塞发送(无论如何,我在不同的线程上发送)并保证我发送到内核的所有内容都将由于 setsockopt(SO_LINGER) 而真正发送到对等体?我要求确认的最后一个猜测:我被允许同时读取和写入,但 N>1 并发写入是一场数据竞赛,我必须处理的所有事情都是互斥锁。

感谢所有至少读到最后的人:)

问题:当 recv() 时,我是否必须关闭套接字才能读取 稍后在 EPOLLRDHUP 期间返回零和关机(SHUT_WR) 加工?

不,没有特别的理由来执行这种有点复杂的操作序列。

recv()接收到0返回值后,您知道连接在网络层至少是半闭合的。 您不会从中得到任何进一步的信息,我不希望在边缘触发模式下运行的 EPoll 进一步宣传其阅读准备情况,但这本身并不需要任何特定的操作。如果写入端保持打开状态(从本地角度来看),那么您可以继续write()send()它,尽管您将没有确认收到您发送的内容的机制。

实际应该执行的操作取决于您所采用的应用程序级协议或消息交换模式。 如果您希望远程对等方在等待您的数据时关闭其端点的写入端(连接到本地端点的读取端),请务必发送它预期的数据。 否则,您可能应该关闭整个连接并在recv()通过返回0发出文件结束信号时停止使用它。 请注意,close()描述符将自动将其从注册的任何 Epoll 兴趣集中删除,但前提是没有其他打开文件描述符引用相同的打开文件描述符。

无论如何,在您close()套接字之前,即使您无法成功通过它进行通信,它仍然有效。 在此之前,没有理由期望您尝试通过它发送的消息会发送到原始远程终结点以外的任何位置。 尝试发送可能会成功,或者即使数据从未到达远端,它们也可能看起来成功,或者可能会失败并出现几个不同的错误之一。

/// @??? Do I need to wait zero just on first iteration?

无论是否已收到任何数据,都应对返回值 0 执行操作。 不一定是相同的操作,但无论哪种方式,您都应该安排一种或另一种方式将其从 EPoll 兴趣集中移出,很可能是通过关闭它。

/// @??? Will be back with next EPOLLIN?!

如果recv()失败并EAGAINEWOULDBLOCK那么 EPoll 很可能会在将来的调用中发出读取准备信号。 不过,不一定是下一个。

/// @??? Received less than requested - no sense to repeat recv, otherwise I need one more turn?! 

收到少于您要求的情况是您应该始终做好准备的可能性。 这并不一定意味着另一个recv()不会返回任何数据,如果您在 EPoll 中使用边缘触发模式,那么假设相反是危险的。 在这种情况下,您应该继续recv(),在非阻塞模式下或MSG_DONTWAIT,直到调用失败并出现EAGAINEWOULDBLOCK

据我现在所知,send() 也可能返回 -1 和 EAGAIN,这意味着我应该订阅 EPOLLOUT 并等待内核缓冲区足够空闲以从我的我那里接收一些数据。这是对的吗?

send()当然会因EAGAINEWOULDBLOCK而失败. 它也可以成功,但发送的字节数少于您请求的字节数,您应该为此做好准备。 无论哪种方式,通过订阅文件描述符上的 EPOLLOUT 事件来响应都是合理的,以便稍后继续发送。

但是,如果客户不会等这么久怎么办?

这取决于客户端在这种情况下会做什么。 如果它关闭了连接,则以后尝试send()它将失败并显示不同的错误。 如果您只在描述符上注册了 EPOLLOUT 事件,那么我怀疑有可能(尽管不太可能)陷入该尝试永远不会发生的状态,因为没有发出进一步的事件信号。 通过注册和正确处理EPOLLRDHUP事件也可以进一步降低这种可能性,即使您的主要兴趣是写作。

如果客户端在没有关闭连接的情况下放弃,那么EPOLLRDHUP可能没有用,并且您更有可能将陈旧的连接无限期地卡在 EPoll 中。 使用每个 FD 超时来解决这种可能性可能是值得的。

或者,我可以调用阻止发送(无论如何,我正在发送不同的thread)并保证我发送到内核的所有内容都将是 真的因为 setsockopt(SO_LINGER)而发送到 peer?

如果你有一个单独的线程完全致力于发送该特定的文件描述符,那么你当然可以考虑阻止send()s。 唯一的缺点是您无法在此基础上实现超时,但除此之外,如果这样的线程阻止发送数据或接收更多要发送的数据,它会做什么?

不过,我看不出SO_LINGER与它有什么关系,至少在当地是这样。 内核将尽一切努力将您已经通过send()调用调度的数据发送到远程对等方,即使您在数据仍处于缓冲状态时close()套接字,无论SO_LINGER的值如何。 该选项的目的是在连接关闭后接收(和丢弃)与连接关联的散乱数据,以便它们不会意外传递到另一个套接字。

但是,这些都不能保证数据成功传递到远程对等方。 没有什么可以保证这一点。

我要求确认的最后一个猜测:我可以阅读和 同时写入,但 N>1 并发写入是数据竞赛,并且 我必须处理的一切都是互斥锁。

插座是全双工的,是的。 此外,POSIX要求大多数功能,包括send()recv(),都是线程安全的。 然而,多个线程写入同一个套接字会带来麻烦,因为单个调用的线程安全性并不能保证多个调用之间的一致性。