unix SO_LINGER可以在套接字上设置的“延迟时间”到底是什么?

41zrol4v  于 8个月前  发布在  Unix
关注(0)|答案(1)|浏览(114)

手册页几乎没有解释这个选项,虽然在网络上和StackOverflow上的答案中有大量的信息,但我发现那里提供的许多信息甚至自相矛盾。那么这个设置到底有什么用,为什么我需要设置或修改它?

dwbf0jvd

dwbf0jvd1#

当TCP套接字断开连接时,系统必须考虑三件事:
1.套接字的send-buffer中可能还有未发送的数据,如果套接字立即关闭,这些数据将丢失。
1.可能仍有数据在传输中,即,数据已经被发送到另一端,但另一端尚未确认已正确接收到该数据,并且该数据可能必须被重新发送或以其他方式丢失。
1.关闭TCP套接字是一个三次握手,没有确认第三个数据包。由于发送方不知道第三个数据包是否已经到达,它必须等待一段时间,看看第二个数据包是否被重新发送。如果是这样,第三个已经丢失,必须重新发送。
当您使用close()调用关闭套接字时,系统通常不会立即销毁套接字,但会首先尝试解决上述所有三个问题,以防止数据丢失并确保干净的断开连接。所有这些都发生在后台(通常在操作系统内核中),因此尽管close()调用立即返回,套接字可能仍然活动一段时间,甚至发送剩余的数据。有一个特定于系统的时间上限,即系统在最终给予并销毁套接字(即使这意味着数据丢失)之前将尝试获得干净断开的时间。请注意,此时间限制可以在分钟范围内!
有一个名为SO_LINGER的套接字选项,用于控制系统如何关闭套接字。您可以使用该选项打开或关闭延迟,如果打开,则设置超时(如果关闭,则也可以设置超时,但超时没有效果)。
默认情况下,延迟是关闭的,这意味着close()立即返回,套接字关闭过程的细节留给系统,系统通常会按照上面的描述处理它。
如果您打开延迟并设置了一个非零的超时,close()将不会立即返回。它只会在问题(1)和(2)已经解决(所有数据都已发送,没有数据正在传输)或超时时返回。两者孰是孰非,可以从这场势均力敌的比赛结果中看出。如果成功,则所有剩余数据都已发送并得到确认,如果失败且errno设置为EWOULDBLOCK,则超时已达到,某些数据可能已丢失。
在非阻塞套接字的情况下,close()不会阻塞,即使延迟时间不是零。在这种情况下,没有办法得到关闭操作的结果,因为你不能在同一个套接字上调用close()两次。即使套接字是延迟的,一旦close返回,套接字文件描述符应该已经无效,再次使用该描述符调用close应该会导致errno设置为EBADF(“bad file descriptor”)失败。
然而,即使你设置延迟时间很短,比如一秒,套接字不会延迟超过一秒,它仍然会在延迟处理上面的问题(3)后停留一段时间。为了确保一个干净的断开连接,实现必须确保另一端也断开了该连接,否则剩余的数据仍然可能到达那个已经死了的连接。因此,套接字将进入大多数系统调用TIME_WAIT的状态,并在系统特定的时间内保持该状态,而不管延迟是否打开,也不管延迟时间设置为多少。
除了一个特殊情况:如果启用延迟但将延迟时间设置为零,这将改变几乎所有内容。在这种情况下,对close()的调用实际上会立即关闭套接字。这意味着无论套接字是阻塞还是非阻塞,close()都会立即返回。任何仍在发送缓冲区中的数据都将被丢弃。任何传输中的数据都将被忽略,并且可能会或可能不会正确到达另一端。套接字也不是使用正常的TCP关闭握手(FIN-ACK)关闭的,它是使用重置(RST)立即杀死的。因此,如果另一端在重置后尝试通过套接字发送某些内容,此操作将失败并返回ECONNRESET(“A connection wasforced closedby the peer.”),而正常关闭将导致EPIPE(“The socket is no longer connected.”)。虽然大多数程序将EPIPE视为无害事件,但如果它们不希望发生这种情况,它们往往会将ECONNRESET视为硬错误。
请注意,这描述了原始BSD套接字实现中的套接字行为(原始意味着这甚至可能不匹配现代BSD实现,如FreeBSD,OpenBSD,NetBSD等)。虽然BSD套接字API已经被今天几乎所有其他主要操作系统(Linux,Android,Windows,macOS,iOS等)复制,但这些系统上的行为有时会有所不同,许多other aspects of that API也是如此。

例如,如果在BSD上关闭了一个在发送缓冲区中有数据的非阻塞套接字,linger打开并且linger时间不为零,close调用将立即返回,但它将指示失败,错误将是EWOULDBLOCK(就像在linger超时后阻塞套接字的情况一样)。Windows也是如此。在macOS上,情况并非如此,close()将始终立即返回并指示成功,无论发送缓冲区中是否有数据。在Linux的情况下,close()调用实际上会阻塞到延迟超时,尽管套接字是非阻塞的。
要了解更多关于不同系统如何实际处理不同延迟设置的信息,请查看以下链接:
https://www.nybek.com/blog/2015/04/29/so_linger-on-non-blocking-sockets/
还有一个页面有阻塞套接字的结果,但不幸的是,互联网档案馆没有捕捉到它,原来的博客已经永远消失了。测试代码仍然可用,但我无法访问所有平台来重新创建测试结果:
https://github.com/nybek/linger-tools
正如你所看到的,行为也可能会改变,这取决于shutdown()是否在close()之前被调用,以及其他系统特定的方面,包括设置延迟超时等,尽管延迟被完全关闭,也会产生影响。
另一个特定于系统的行为是,如果进程没有先关闭套接字就死亡了,会发生什么。在这种情况下,系统将代表您关闭套接字,而某些系统在必须这样做时往往会忽略任何延迟设置,而只是回退到系统的默认行为。在这种情况下,它们不能在套接字关闭时“阻塞”,但有些系统甚至会忽略零超时,并在这种情况下执行FIN-ACK
因此,将延迟超时设置为零并不能阻止套接字进入TIME_WAIT状态。这取决于套接字是如何关闭的(shutdown()close()),是谁关闭的(你自己的代码还是系统),是阻塞的还是非阻塞的,最终取决于你的代码运行的系统。唯一正确的说法是:
如果你手动关闭一个阻塞的套接字(至少在你关闭它的那一刻,它可能是非阻塞的),并且这个套接字已经启用了延迟,超时为零,这是你避免这个套接字进入TIME_WAIT状态的最好机会。不能保证它不会发生,但如果这不能阻止它发生,你就没有别的办法阻止它发生,除非你有办法确保另一边的对等体会为你发起关闭;因为只有启动关闭操作的一侧可能最终处于TIME_WAIT状态。
所以我个人的专业建议是:如果你设计了一个服务器-客户端协议,设计它的方式通常是客户端先关闭连接,因为服务器套接字通常以TIME_WAIT状态结束是非常不可取的,但更不可取的是连接被RST关闭,因为这可能导致之前发送给客户端的数据丢失。

相关问题