为啥不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭?
Posted
技术标签:
【中文标题】为啥不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭?【英文标题】:Why is it impossible, without attempting I/O, to detect that TCP socket was gracefully closed by peer?为什么不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭? 【发布时间】:2010-09-14 09:22:27 【问题描述】:作为recent question 的后续行动,我想知道为什么在 Java 中,如果不尝试在 TCP 套接字上读/写,就不可能检测到套接字已被对等方优雅地关闭?无论使用 pre-NIO Socket
还是 NIO SocketChannel
,似乎都是这种情况。
当对等端优雅地关闭 TCP 连接时,连接两端的 TCP 堆栈都知道这一事实。服务器端(启动关闭的那个)最终处于状态FIN_WAIT2
,而客户端(没有明确响应关闭的那个)最终处于状态CLOSE_WAIT
。为什么Socket
或SocketChannel
中没有可以查询TCP 堆栈以查看底层TCP 连接是否已终止的方法?是不是 TCP 栈没有提供这样的状态信息?还是避免对内核进行昂贵的调用是一项设计决策?
在已经发布了这个问题的一些答案的用户的帮助下,我想我知道问题可能来自哪里。没有明确关闭连接的一方最终处于 TCP 状态CLOSE_WAIT
,这意味着连接正在关闭并等待该方发出自己的CLOSE
操作。我想isConnected
返回true
和isClosed
返回false
是公平的,但为什么没有类似isClosing
的东西?
以下是使用 pre-NIO 套接字的测试类。但是使用 NIO 可以获得相同的结果。
import java.net.ServerSocket;
import java.net.Socket;
public class MyServer
public static void main(String[] args) throws Exception
final ServerSocket ss = new ServerSocket(12345);
final Socket cs = ss.accept();
System.out.println("Accepted connection");
Thread.sleep(5000);
cs.close();
System.out.println("Closed connection");
ss.close();
Thread.sleep(100000);
import java.net.Socket;
public class MyClient
public static void main(String[] args) throws Exception
final Socket s = new Socket("localhost", 12345);
for (int i = 0; i < 10; i++)
System.out.println("connected: " + s.isConnected() +
", closed: " + s.isClosed());
Thread.sleep(1000);
Thread.sleep(100000);
当测试客户端连接到测试服务器时,即使服务器开始关闭连接,输出仍然保持不变:
connected: true, closed: false
connected: true, closed: false
...
【问题讨论】:
我想说:SCTP 协议没有这个“问题”。 SCTP 不像 TCP 那样有半关闭状态,换句话说,当另一端关闭其发送套接字时,一侧不能继续发送数据。这应该会让事情变得更容易。 我们有两个邮箱(套接字)...................... ............. 邮箱使用 RoyalMail (IP) 互相发送邮件,忘记 TCP ...... .............................一切都很好,邮箱可以互相发送/接收邮件(最近有很多延迟)发送同时接收没有问题。 ............. 如果一个邮箱被卡车撞倒并发生故障.... 另一个邮箱怎么知道?它必须由 Royal Mail 通知,而后者在下一次尝试从该失败的邮箱发送/接收邮件之前不会知道.. ......呃...... 如果你不打算从套接字读取或写入套接字,你为什么要关心?如果您要从套接字读取或写入套接字,为什么还要进行额外检查?用例是什么?Socket.close
不是优雅的结束。
@immibis 这肯定是一个优雅的关闭,除非套接字接收缓冲区中有未读数据或者你弄乱了 SO_LINGER。
【参考方案1】:
底层的套接字 API 没有这样的通知。
发送 TCP 堆栈在最后一个数据包之前不会发送 FIN 位,因此当发送应用程序在发送数据之前逻辑关闭其套接字时,可能会缓冲大量数据。同样,由于网络比接收应用程序更快而缓冲的数据(我不知道,也许您正在通过较慢的连接中继它)对接收器可能很重要,并且您不希望接收应用程序丢弃它只是因为堆栈已经收到了 FIN 位。
【讨论】:
在我的测试示例中(也许我应该在这里提供一个......)没有故意通过连接发送/接收数据。所以,我很确定堆栈会收到 FIN(优雅)或 RST(在某些非优雅场景中)。 netstat 也证实了这一点。 当然——如果没有缓冲,那么 FIN 将立即发送到一个空包(无负载)上。但是,在 FIN 之后,连接的那一端不再发送数据包(它仍然会 ACK 发送给它的任何内容)。 会发生什么情况是连接的双方最终在CLOSE_WAIT
和 FIN_WAIT_2
并且它处于这种状态 isConcected()
和 isClosed()
仍然看不到连接已经终止。
感谢您的建议!我想我现在更好地理解了这个问题。我把问题说得更具体了(见第三段):为什么没有“Socket.isClosing”来测试半关闭连接?【参考方案2】:
这是一个有趣的话题。我刚刚挖掘了java代码来检查。根据我的发现,有两个明显的问题:第一个是 TCP RFC 本身,它允许远程关闭的套接字以半双工方式传输数据,因此远程关闭的套接字仍然是半开的。根据 RFC,RST 不会关闭连接,您需要发送显式 ABORT 命令;所以Java允许通过半封闭套接字发送数据
(有两种方法可以读取两个端点的关闭状态。)
另一个问题是实现说这种行为是可选的。由于 Java 力求可移植,它们实现了最好的通用特性。我猜,维护(操作系统,半双工的实现)的映射会是个问题。
【讨论】:
我想你在谈论 RFC 793 (faqs.org/rfcs/rfc793.html) 第 3.5 节关闭连接。我不确定它是否解释了这个问题,因为双方都完成了连接的正常关闭并最终处于不应发送/接收任何数据的状态。 视情况而定。您在套接字上看到多少个 FIN?此外,可能是特定于平台的问题:可能 windows 用 FIN 回复每个 FIN,并且两端的连接都关闭,但其他操作系统可能不会这样做,这就是问题 2 出现的地方 不,很遗憾,情况并非如此。 isOutputShutdown 和 isInputShutdown 是每个人在遇到这种“发现”时首先尝试的事情,但这两种方法都返回 false。我刚刚在 Windows XP 和 Linux 2.6 上对其进行了测试。即使在尝试读取之后,所有 4 个方法的返回值都保持不变 郑重声明,这不是半双工的。半双工是指一次只能发送一侧;双方仍然可以发送。 isInputShutdown 和 isOutputShutdown 测试连接的 local 端 - 它们是用于确定您是否在此 Socket 上调用了 shutdownInput 或 shutdownOutput 的测试。他们没有告诉你任何关于远程连接的信息。【参考方案3】:我认为这更像是一个套接字编程问题。 Java 只是遵循套接字编程的传统。
来自Wikipedia:
TCP 提供可靠的、有序的 从一个传送字节流 将一台计算机上的程序传输到另一台计算机 在另一台计算机上程序。
一旦握手完成,TCP 就不再区分两个端点(客户端和服务器)。术语“客户端”和“服务器”主要是为了方便。因此,“服务器”可能正在发送数据,而“客户端”可能正在同时发送一些其他数据。
“关闭”一词也具有误导性。只有 FIN 声明,意思是“我不会再给你发东西了”。但这并不意味着飞行中没有数据包,或者对方无话可说。如果您将蜗牛邮件实现为数据链路层,或者您的数据包经过不同的路由,则接收方可能会以错误的顺序接收数据包。 TCP 知道如何为您解决此问题。
另外,作为一个程序,您可能没有时间继续检查缓冲区中的内容。因此,在您方便时,您可以检查缓冲区中的内容。总而言之,当前的套接字实现还不错。如果真的有 isPeerClosed(),那是你每次想调用 read 时都要额外调用的。
【讨论】:
我不这么认为,你可以在windows和linux上测试C代码中的状态!!!由于某种原因,Java 可能不会公开一些东西,就像公开 windows 和 linux 上的 getsockopt 函数一样。事实上,下面的答案有一些 linux 端的 linux C 代码。 我不认为拥有 'isPeerClosed()' 方法会以某种方式让您在每次读取尝试之前调用它。只有当您明确需要它时,您才可以简单地调用它。我同意当前的套接字实现并不是那么糟糕,即使如果你想知道套接字的远程部分是否关闭,它也需要你写入输出流。因为如果不是,你还要在另一边处理你的书面数据,简直就像坐在垂直的钉子上一样大快人心;) 它确实的意思是'没有更多的数据包在飞行'。 FIN 是在 任何传输中的数据之后收到的。然而它不的意思是对等端已经关闭了套接字用于输入。你必须**send一些东西*并得到一个'连接重置'来检测。 FIN 可能只是意味着关闭输出。【参考方案4】:这种行为(不是 Java 特定的)的原因是您没有从 TCP 堆栈获得任何状态信息。毕竟,套接字只是另一个文件句柄,如果没有实际尝试,您将无法确定是否有实际数据要从中读取(select(2)
对此无济于事,它仅表示您可以尝试不阻塞)。
有关详细信息,请参阅Unix socket FAQ。
【讨论】:
REALbasic 套接字(在 Mac OS X 和 Linux 上)基于 BSD 套接字,但是当连接被另一端断开时,RB 设法给你一个很好的错误 102。所以我同意原始海报,这应该是可能的,Java(和 Cocoa)不提供它是蹩脚的。 @JoeStrout RB 只能在您执行某些 I/O 时执行此操作。没有 API 可以在不执行 I/O 的情况下为您提供连接状态。时期。这不是 Java 的缺陷。这实际上是由于 TCP 中缺少“拨号音”,这是一个经过深思熟虑的设计特性。select()
告诉您是否有数据或 EOS 可供读取而不会阻塞。 “你可以在没有阻塞的情况下尝试的信号”是没有意义的。如果您处于非阻塞模式,您可以总是尝试不阻塞。 select()
由套接字接收缓冲区中的数据或套接字发送缓冲区中的待处理 FIN 或空间驱动。
@EJP getsockopt(SO_ERROR)
怎么样?事实上,即使getpeername
也会告诉你套接字是否仍然连接。【参考方案5】:
由于到目前为止没有一个答案完全回答了这个问题,我总结一下我目前对这个问题的理解。
当建立 TCP 连接并且一个对等方在其套接字上调用 close()
或 shutdownOutput()
时,连接另一侧的套接字将转换为 CLOSE_WAIT
状态。原则上,无需调用read/recv
(例如,Linux 上的getsockopt()
:http://www.developerweb.net/forum/showthread.php?t=4395),就可以从 TCP 堆栈中找出套接字是否处于CLOSE_WAIT
状态,但这不是可移植的。
Java 的Socket
类似乎旨在提供与BSD TCP 套接字相当的抽象,可能是因为这是人们在编写TCP/IP 应用程序时习惯的抽象级别。 BSD 套接字是支持 INET(例如 TCP)之外的套接字的泛化,因此它们不提供查找套接字 TCP 状态的可移植方式。
没有像 isCloseWait()
这样的方法,因为人们习惯于在 BSD 套接字提供的抽象级别上对 TCP 应用程序进行编程,并不期望 Java 提供任何额外的方法。
【讨论】:
Java 也不能提供任何额外的可移植方法。也许他们可以创建一个 isCloseWait() 方法,如果平台不支持它会返回 false,但是如果他们只在支持的平台上进行测试,有多少程序员会被这个陷阱所困扰? 看起来它可以移植给我......windows有这个msdn.microsoft.com/en-us/library/windows/desktop/…和linux这个pubs.opengroup.org/onlinepubs/009695399/functions/… 不是程序员习惯了;就是套接字接口对程序员有用。请记住,套接字抽象不仅仅用于 TCP 协议。 Java 中没有像isCloseWait()
这样的方法,因为并非所有平台都支持它。
ident (RFC 1413) 协议允许服务器在发送响应后保持连接打开,或关闭连接而不发送更多数据。 Java ident 客户端可能会选择保持连接打开以避免下次查找时的 3 次握手,但它如何知道连接仍然打开?它应该尝试通过重新打开连接来响应任何错误吗?还是协议设计错误?【参考方案6】:
只有写入需要交换数据包才能确定连接丢失。一个常见的解决方法是使用 KEEP ALIVE 选项。
【讨论】:
我认为允许端点通过发送设置了 FIN 的数据包来启动正常连接关闭,而无需写入任何有效负载。 @Alexander 当然可以,但这与这个答案无关,这是关于检测较少的连接。【参考方案7】:这是 Java(以及我看过的所有其他)OO 套接字类的一个缺陷——无法访问 select 系统调用。
C 中的正确答案:
struct timeval tp;
fd_set in;
fd_set out;
fd_set err;
FD_ZERO (in);
FD_ZERO (out);
FD_ZERO (err);
FD_SET(socket_handle, err);
tp.tv_sec = 0; /* or however long you want to wait */
tp.tv_usec = 0;
select(socket_handle + 1, in, out, err, &tp);
if (FD_ISSET(socket_handle, err)
/* handle closed socket */
【讨论】:
你可以用getsocketop(... SOL_SOCKET, SO_ERROR, ...)
做同样的事情。
错误文件描述符集不会指示关闭连接。请阅读选择手册:'exceptfds - 此设置用于“异常情况”。在实践中,只有一个这样的异常情况是常见的:从 TCP 套接字读取的带外 (OOB) 数据的可用性。 FIN 不是 OOB 数据。
您可以使用 'Selector' 类来访问 'select()' 系统调用。不过它使用 NIO。
连接被对方关闭并没有什么异常。
许多平台,包括 Java,do提供对 select() 系统调用的访问。【参考方案8】:
Java IO 堆栈在突然拆除时被破坏时肯定会发送 FIN。您无法检测到这一点是没有意义的,b/c 大多数客户端仅在关闭连接时才发送 FIN。
...我真正开始讨厌 NIO Java 类的另一个原因。似乎一切都有些半途而废。
【讨论】:
另外,当存在 FIN 时,我似乎只在读取时获得和结束流(-1 返回)。所以这是我能看到的在读取端检测到关闭的唯一方法。 你可以检测到。阅读时获得EOS。 Java 不发送 FIN。 TCP 就是这样做的。 Java 没有实现 TCP/IP,它只是使用平台实现。【参考方案9】:检测 (TCP) 套接字连接的远程端是否已关闭可以使用 java.net.Socket.sendUrgentData(int) 方法完成,并在远程端关闭时捕获它抛出的 IOException。这已经在 Java-Java 和 Java-C 之间进行了测试。
这避免了将通信协议设计为使用某种 ping 机制的问题。通过在套接字上禁用 OOBInline (setOOBInline(false),任何接收到的 OOB 数据都会被静默丢弃,但 OOB 数据仍然可以发送。如果远程端关闭,则尝试重置连接,失败,并导致抛出一些 IOException .
如果您在协议中实际使用 OOB 数据,那么您的里程可能会有所不同。
【讨论】:
【参考方案10】:当谈到处理半开的 Java 套接字时,你可能想看看 isInputShutdown() 和 isOutputShutdown()。
【讨论】:
没有。这只会告诉你你调用了什么,而不是对等体调用了什么。 愿意分享您的声明来源吗? 愿意分享您的相反来源吗?是你的说法。如果你有证据,就让我们来吧。我断言你是不正确的。做实验,证明我错了。 三年后没有实验。 QED【参考方案11】:我经常使用套接字,主要是与选择器一起使用,虽然不是网络 OSI 专家,但据我了解,在套接字上调用 shutdownOutput()
实际上会在网络 (FIN) 上发送一些东西,从而唤醒我的选择器侧(在 C 语言中的行为相同)。这里你有检测:实际检测到一个读操作,当你尝试它时会失败。
在您提供的代码中,关闭套接字将关闭输入和输出流,无法读取可能可用的数据,因此会丢失它们。 Java Socket.close()
方法执行“优雅”断开连接(与我最初的想法相反),因为输出流中留下的数据将被发送随后是 FIN 以表示其关闭。 FIN 将被对方确认,就像任何常规数据包都会1。
如果你需要等待对方关闭它的socket,你需要等待它的FIN。为了实现这一点,您必须检测Socket.getInputStream().read() < 0
,这意味着您应该不关闭您的套接字,因为它会关闭它的InputStream
.
从我在 C 中所做的,现在在 Java 中,实现这样的同步关闭应该这样完成:
-
关闭套接字输出(在另一端发送 FIN,这是此套接字将发送的最后一件事)。输入仍处于打开状态,因此您可以
read()
并检测远程close()
读取套接字InputStream
,直到我们收到来自另一端的回复-FIN(因为它会检测到FIN,它会经历同样的优雅断开过程)。这在某些操作系统上很重要,因为只要其中一个缓冲区仍然包含数据,它们实际上就不会关闭套接字。它们被称为“幽灵”套接字,并在操作系统中用完描述符编号(现代操作系统可能不再是问题)
关闭套接字(通过调用Socket.close()
或关闭其InputStream
或OutputStream
)
如下Java sn -p所示:
public void synchronizedClose(Socket sok)
InputStream is = sok.getInputStream();
sok.shutdownOutput(); // Sends the 'FIN' on the network
while (is.read() > 0) ; // "read()" returns '-1' when the 'FIN' is reached
sok.close(); // or is.close(); Now we can close the Socket
当然双方必须使用相同的关闭方式,否则发送部分可能总是发送足够的数据以保持while
循环繁忙(例如,如果发送部分只发送数据并且从不读取以检测连接终止。这很笨拙,但您可能无法控制)。
正如@WarrenDew 在他的评论中指出的那样,丢弃程序(应用层)中的数据会导致应用层的非正常断开连接:尽管所有数据都是在 TCP 层(while
循环)接收的,但它们是丢弃。
1:来自“Fundamental Networking in Java”:见图。 3.3 p.45,以及整个 §3.7,第 43-48 页
【讨论】:
Java 确实执行了优雅的关闭。这不是“残酷的”。 @EJP,“优雅断开连接”是发生在 TCP 级别的特定交换,客户端应该向服务器发出断开连接的信号,而服务器又会在关闭其端之前发送剩余数据. “发送剩余数据”部分必须由程序处理(尽管大多数时候人们不会发送任何东西)。调用socket.close()
是“残酷的”,因为它不尊重此客户端/服务器信号。只有当自己的套接字输出缓冲区已满时,服务器才会收到客户端断开连接的通知(因为对方没有确认数据,而对方已关闭)。
更多信息请见MSDN。
@Matthieu 如果您的应用程序没有读取所有可用数据,这在应用程序层可能是不正常的,但在 TCP 传输层,数据仍然被接收并且连接正常终止。如果您的应用程序从输入流中读取所有数据并仅将其丢弃,情况也是如此。
@LeonidUsov 这根本不正确。 Java read()
在流结束时返回 -1,并且无论您调用多少次都会继续这样做。 A C read()
或 recv()
在流结束时返回零,并且无论您调用多少次都会继续这样做。【参考方案12】:
这是一个蹩脚的解决方法。使用 SSL ;) 并且 SSL 在拆解时会进行关闭握手,因此您会收到有关套接字被关闭的通知(大多数实现似乎都会进行属性握手拆解)。
【讨论】:
在 java 中使用 SSL 时如何“通知”套接字被关闭?以上是关于为啥不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭?的主要内容,如果未能解决你的问题,请参考以下文章