ECONNRESET in Send Linux C

ECONNRESET in Send Linux C

根据Unix Network Programming,当一个套接字向一个关闭的套接字写入两次(在一个FIN数据包之后),然后在第一次成功发送,但是从另一台主机收到一个RST数据包。由于主机收到 RST,因此套接字被销毁。于是第二次写,收到SIGPIPE信号,返回EPIPE错误。

但是在send man pages可以返回ECONNRESET,表示收到了一个RST包。当它 returns ECONNRESET - 没有信号返回。

ECONNRESET在哪些情况下可以返回?为什么在这种情况下没有 SIGPIPE 信号?

注意:我已经检查过类似的问题here。但是,当我 运行 在我的 linux 计算机上发送时,返回的是 EPIPE 错误,而不是 ECONNRESET。

如果对等方在套接字缓冲区中仍有未处理的数据时关闭连接,它将发回一个 RST 数据包。这将导致在套接字上设置一个标志,并且下一次发送将 return ECONNRESET 作为结果。如果连接被没有未完成数据的对等方关闭,则 EPIPE 在发送时被 returned(或 SIGPIPE 触发)。在这两种情况下,本地套接字仍处于打开状态(即文件描述符有效),但底层连接已关闭。

示例:想象一个服务器读取一个字节然后关闭连接:

  • EPIPE:客户端发送第一个字节。服务器读取字节并关闭连接后,客户端将发送更多数据,然后再发送一些数据。最新的发送调用将触发EPIPE/SIGPIPE.
  • ECONNRESET:客户端先发送多于一个字节。服务器将读取一个字节并关闭套接字接收缓冲区中更多字节的连接。这将触发来自服务器的连接 RST 数据包,在下一次发送时,客户端将收到 ECONNRESET。

TCP 连接可以看作是两个端点之间的两条数据管道。一条数据管道用于将数据从 A 发送到 B,一条数据管道用于将数据从 B 发送到 A。这两条管道属于一个连接,但它们不会相互影响。在一条管道上发送数据不会影响在另一条管道上发送的数据。如果一个管道上的数据是对先前在另一个管道上发送的数据的回复数据,那么只有您的应用程序知道这一点,TCP 对此一无所知。 TCP 的任务是确保数据可靠地从管道的一端传输到另一端,并且尽可能快,这就是 TCP 所关心的。

一旦一方完成发送数据,它就会通过发送一个设置了 FIN 标志的数据包来告诉另一方它已完成。发送 FIN 标志意味着“我已经发送了所有我想发送给你的数据,所以我的发送管道现在关闭了”。您可以通过调用 shutdown(socketfd, SHUT_WR) 在您的代码中有意触发它。如果另一方随后将在套接字上调用 recv(),它不会收到错误,但 receive 会说它读取了零字节,这意味着 "end of stream"。流结束不是一个错误,它只意味着没有更多的数据到达那里,无论你在那个套接字上调用 recv() 的频率如何。

当然,这并不影响其他管道,所以当A -> B关闭后,B -> A仍然可以使用。即使您关闭了发送管道,您仍然可以从该套接字接收。但是,在某些时候,B 也将完成发送数据并传输 FIN。一旦两个管道都关闭,整个连接就关闭了,这将是一个正常的关闭,因为双方已经能够发送他们想要发送的所有数据,并且没有数据应该丢失,因为只要有未经确认的数据正在传输中,另一方不会说它已完成,而是先等待该数据可靠传输。

或者有一个 RST 标志可以立即关闭整个连接,不管对方是否完成发送,也不管是否有未确认的数据在传输中,所以 RST 有一个导致数据丢失的可能性很高。由于这是一种可能需要特殊处理的异常情况,程序员知道是否是这种情况会很有用,这就是存在两个错误的原因:

EPIPE - 您不能通过该管道发送,因为该管道不再有效。但是,您在中断之前发送的所有数据仍然可靠地传送,您只是无法发送任何新数据。

ECONNRESET - 您的管道坏了,可能是您之前尝试发送的数据在传输过程中丢失了。如果这是一个问题,你最好以某种方式处理它。

但这两个错误并没有一对一地映射到 FINRST 标志。如果您在系统认为没有数据丢失风险的情况下收到 RST,则没有理由让您无所事事。因此,如果您之前发送的所有数据都被确认为正确接收,然后当您尝试发送新数据时连接被 RST 关闭,则不会丢失任何数据。这包括您尝试发送的当前数据,因为这些数据没有丢失,它从未在途中发送过,这是不同的,因为您仍然拥有它,而您之前发送的数据可能不再存在。如果您的汽车在旅途中抛锚了,那么这与您仍在家中时完全不同,因为您的汽车引擎甚至无法启动。所以最终由您的系统决定 RST 是否触发 ECONNRESETEPIPE

好的,但是对方为什么要先给你发一个RST呢?为什么不总是以 FIN 结束?嗯,有几个原因,但最突出的两个是:

  1. 一方只能向另一方发送信号,表示已完成发送,但表示已完成整个连接的唯一方法是发送 RST。所以如果一方想要关闭一个连接并且它想优雅地关闭它,它会首先发送一个 FIN 来表示它不会再发送新数据,然后给另一方一些时间来停止发送数据,允许 in-flight 数据通过并最终发送 FIN。但是,万一对方不想停下来,一直发啊发怎么办?这种行为是合法的,因为 FIN 并不意味着连接需要关闭,它只意味着一侧已经完成。结果是 FIN 后跟 RST 以最终关闭该连接。这可能会导致 in-flight 数据丢失,也可能不会,只有 RST 的接收者才能确定数据是否丢失,自从发送者以来,它一定是站在他这边的RSTFIN 之后肯定不再发送任何数据。对于 recv() 调用,此 RST 无效,因为在发出 "end of stream" 信号之前有一个 FIN,因此recv() 将报告已读取零字节。

  2. 一方要关闭连接,但仍有未发送的数据。理想情况下,它会等到所有未发送的数据都已发送,然后发送一个 FIN,但是,允许等待的时间是有限的,超过该时间后,仍然有未发送的数据。在那种情况下,它不能发送 FIN,因为 FIN 是一个谎言。它会告诉对方“嘿,我发送了所有我想发送的数据”,但事实并非如此。本来应该发送的数据,但是因为要求关闭是即时的,所以不得不丢弃这个数据,所以这边直接发送一个RST。此 RST 是否会触发 send() 调用的 ECONNRESET 再次取决于事实,即 RST 的接收者是否有未发送的数据正在传输中。但是,它肯定会在下一个 recv() 调用时触发 ECONNRESET 错误,以告诉程序“另一方实际上想向您发送更多数据,但它不能并且因此其中一些数据丢失了”,因为这可能又是一种以某种方式处理的情况,因为您收到的数据肯定是不完整的,这是您应该注意的事情。

如果你想强制一个套接字总是直接用 RST 关闭而不是用 FIN/FINFIN/RST,您可以将延迟时间设置为零。

struct linger l = { .l_onoff = 1, .l_linger = 0 };
setsockopt(socketfd, SOL_SOCKET, SO_LINGER, &l, sizeof(l));

现在套接字必须立即关闭,没有任何延迟,无论多么小,立即关闭 TCP 套接字的唯一方法是发送 RST。有些人认为“为什么启用它并将时间设置为零?为什么不直接禁用它呢?”但禁用具有不同的含义。

延迟时间是 close() 调用可能会阻塞以执行挂起的发送操作以正常关闭套接字的时间。如果启用 (.l_onoff != 0),对 close() 的调用最多可能会阻塞 .l_linger 秒。如果将时间设置为零,它可能根本不会阻塞并因此立即终止 (RST)。但是,如果您禁用它,那么 close() 将永远不会阻塞,但系统可能仍会在关闭时徘徊,但这种挥之不去的情况发生在后台,因此您的进程将不再注意到它,因此也无法知道当套接字真正关闭时,因为socketfd立即失效,即使内核中的底层套接字仍然存在。