epoll 零 recv() 和 negative(EAGAIN) send()

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

前几天我一直在与 epoll 作斗争,现在我不知所措 ;)

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

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

event_template.events = EPOLLIN | EPOLLRDHUP | EPOLLET

尽管我可以看到成功的 send() 在我这边,但当我注意到 nginx 以 502 响应时,一切都变得很好奇。我 运行 wireshark 嗅探并意识到我的服务器将(尝试并获取 RST)数据发送到网络上的另一台机器。所以,我决定套接字描述符无效,这有点像 "undefined behaviour"。最后,我发现在第二个 recv() 上我得到了零字节,这意味着必须关闭连接并且我不能再发回数据。尽管如此,我从 epoll 得到的不仅仅是 EPOLLIN,还有 EPOLLRDHUP。

问题:在 EPOLLRDHUP 处理期间,当 recv() returns 为零并稍后关闭(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() 也可能 return -1 和 EAGAIN,这意味着我应该订阅 EPOLLOUT 并等待内核缓冲区足够空闲以接收一些数据从我的我。这是正确的吗?但是,如果客户不会等那么久怎么办?或者,我可以调用阻塞发送(无论如何,我在不同的线程上发送)并保证我发送给内核的所有内容都会因为 setsockopt(SO_LINGER) 而真正发送给对等方?我要求确认的 最终猜测 :我可以同时读取和写入,但 N>1 并发写入是一场数据竞赛,我必须处理的一切是一个互斥量。

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

Questions: Do I have to close socket just for reading when recv() returns zero and shutdown(SHUT_WR) later on during EPOLLRDHUP processing?

不,没有特别的理由来执行这一系列有些复杂的操作。

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

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

无论如何,在您执行 close() 套接字之前,它仍然有效,即使您无法通过它成功通信。在那之前,没有理由期望您尝试通过它发送的消息会去往除了可能到达原始远程端点之外的任何地方。尝试发送可能会成功,或者即使数据从未到达远端,它们也可能看起来成功,或者可能会因几种不同的错误之一而失败。

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

您应该对 return 值为 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() 不会 return 任何数据,如果您在 EPoll 中使用边缘触发模式,那么假设相反是危险的。在这种情况下,您应该继续 recv(),以非阻塞模式或 MSG_DONTWAIT,直到调用失败 EAGAINEWOULDBLOCK

As far as I now know, send() may also return -1 and EAGAIN which means that I'm supposed to subscribe on EPOLLOUT and wait when kernel buffer will be free enough to receive some data from my me. Is this right?

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

But what if client won't wait so long?

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

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

Or, may I call blocking send(anyway, I'm sending on a different thread) and guarantee the everything what I send to kernel will be really sent to peer because of setsockopt(SO_LINGER)?

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

不过,我不太明白 SO_LINGER 与它有什么关系,至少在本地方面是这样。内核将尽一切努力将您已经通过 send() 调用分派的数据发送到远程对等方,即使您 close() 在数据仍在缓冲时套接字,无论 [= 的值如何36=]。该选项的目的是在连接关闭后接收(并丢弃)与连接关联的散乱数据,这样它们就不会意外地传送到另一个套接字。

但是,

None可以保证数据成功传递到远程对端。没有什么能保证。

And a final guess which I ask to confirm: I'm allowed to read and write simultaneously, but N>1 concurrent writes is a data race and everything that I have to deal with it is a mutex.

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