为什么在不尝试I / O的情况下,不可能检测到TCP套接字已被对方正常closures?

作为最近一个问题的后续,我想知道为什么在Java中不可能读取/写入TCP套接字来检测套接字是否被对方正常closures? 无论是使用pre-NIO Socket还是使用NIO SocketChannel似乎都是如此。

当对端正常closuresTCP连接时,连接两端的TCP协议栈都知道这个事实。 服务器端(启动closures的那个)以FIN_WAIT2状态结束,而客户端(没有明确响应closures的那一端)以状态CLOSE_WAIT结束。 为什么在SocketSocketChannel没有可以查询TCP堆栈的方法来查看底层的TCP连接是否已经终止? TCP堆栈是不是提供这样的状态信息? 还是这是一个devise决定,以避免昂贵的内核调用?

在已经发布了这个问题的一些答案的用户的帮助下,我想我会看到问题可能来自哪里。 未明确closures连接的一端以TCP状态CLOSE_WAIT结束,意味着连接处于closures状态,并等待该端发出自己的CLOSE操作。 我想这是公平的isConnected返回trueisClosed返回false ,但为什么不是有像isClosing

以下是使用pre-NIO套接字的testing类。 但是使用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); } } 

当testing客户端连接到testing服务器时,即使服务器启动closures连接,输出也保持不变:

 connected: true, closed: false connected: true, closed: false ... 

我一直在使用套接字,通常与select器,而不是一个networkingOSI专家,根据我的理解,调用一个套接字上的shutdownOutput()实际上发送的东西在networking上(FIN)唤醒我的select器在另一边在C语言中的行为)。 在这里你有检测 :实际上检测一个读取操作,当你尝试时会失败。

在你给出的代码中,closures套接字将closuresinput和输出stream,而不能读取可能的数据,因此丢失它们。 Java Socket.close()方法执行“优雅的”断开(与我最初的想法相反),在输出stream中留下的数据将被发送, 接着发送一个FIN来表示closures。 FIN将由另一方确认,因为任何常规的数据包都会1

如果您需要等待另一端closures其sockets,则需要等待其FIN。 为了实现这一点,你必须检测到Socket.getInputStream().read() < 0 ,这意味着你不应该closures你的套接字,因为它会closures它的InputStream

从我在C,现在在Java中所做的,实现这样一个同步closures应该这样做:

  1. closures套接字输出(在另一端发送FIN,这是通过此套接字发送的最后一个东西)。 input仍然是打开的,所以你可以read()和检测远程close()
  2. 读取套接字InputStream直到我们收到来自另一端的答复FIN(因为它将检测FIN,它将经历相同的优美的连接过程)。 这对于某些操作系统很重要,因为只要其中一个缓冲区仍然包含数据,它们实际上并不closures套接字。 他们被称为“幽灵”套接字,并在操作系统中使用描述符号码(这可能不再是现代操作系统的问题)
  3. closures套接字(通过调用Socket.close()或closures它的InputStreamOutputStream

如以下Java代码片段所示:

 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 } 

当然,双方都必须使用相同的closures方式,或者发送部分可能总是发送足够的数据来保持while循环的忙碌(例如,如果发送部分只是发送数据,从不读取以检测连接终止。 ,但你可能无法控制)。

正如@WarrenDew在他的评论中指出的那样,丢弃程序(应用程序层)中的数据会引起应用程序层的非正常断开:虽然所有数据都是在TCP层( while循环)接收到的,但它们将被丢弃。

1 :从“ Java基础networking ”:见图。 3.3 p.45,和整个§3.7,第43-48页

我认为这是更多的套接字编程问题。 Java只是遵循套接字编程的传统。

维基百科 :

TCP提供从一台计算机上的一个程序到另一台计算机上的另一个程序的可靠的,有序的字节stream交付。

一旦握手完成,TCP就不会在两个端点(客户端和服务器)之间进行区分。 术语“客户端”和“服务器”主要是为了方便。 所以,“服务器”可以发送数据,“客户端”可以同时发送一些其他数据。

“closures”一词也是误导。 只有FIN的声明,意思是“我不会再寄给你任何东西”。 但是,这并不意味着没有包裹在飞行中,或者没有其他的说。 如果您将snail邮件作为数据链路层实施,或者您的数据包走过了不同的路线,那么接收方可能会收到错误顺序的数据包。 TCP知道如何解决这个问题。

另外,作为一个程序,你可能没有时间去检查缓冲区中的内容。 所以,在您方便的时候,您可以检查缓冲区中的内容。 总而言之,当前的socket实现并不是那么糟糕。 如果实际上有isPeerClosed(),这是额外的电话,你必须使每次你想要读取。

底层套接字API没有这样的通知。

发送TCP堆栈将不会发送FIN位直到最后一个数据包,所以在发送数据之前发送应用程序在逻辑上closures它的套接字时可能会有大量的数据被caching。 同样,由于networking比接收应用程序更快(我不知道,也许你是通过一个较慢的连接来中继它),因此缓冲的数据对于接收方来说可能是重要的,并且您不希望接收方应用程序丢弃它只是因为FIN位已经被堆栈接收。

迄今为止,没有一个答案能够完全回答这个问题,所以我总结了我对这个问题的现有理解。

当TCP连接build立并且一个对端在其套接字上调用close()shutdownOutput()时,连接另一端的套接字将转换为CLOSE_WAIT状态。 原则上,可以从TCP堆栈中找出一个套接字是否处于CLOSE_WAIT状态,而不调用read/recv (例如,Linux上的getsockopt() : http : //www.developerweb.net/forum/showthread.php? getsockopt() = 4395 ),但这不是便携式的。

Java的Socket类似乎被devise为提供一个可与BSD TCP套接字相媲美的抽象,可能是因为这是人们在编程TCP / IP应用程序时习惯的抽象级别。 BSD套接字是支持除INET(例如TCP)之外的其他通用套接字,所以它们不提供找出套接字的TCP状态的便携方式。

没有像isCloseWait()这样的方法,因为人们习惯于在BSD套接字提供的抽象层次上对TCP应用程序进行编程,不希望Java提供任何额外的方法。

通过java.net.Socket.sendUrgentData(int)方法检测(TCP)套接字连接的远程端是否已closures,如果远程端closures,则捕获抛出的IOException。 这已经在Java-Java和Java-C之间进行了testing。

这就避免了devise通信协议使用某种ping机制的问题。 通过在套接字上禁用OOBInline(setOOBInline(false)),任何接收到的OOB数据都被静静地丢弃,但是OOB数据仍然可以被发送,如果远程端closures,则尝试连接复位,失败并导致抛出一些IOException 。

如果您在协议中使用了OOB数据,那么您的里程可能会有所不同。

这是一个有趣的话题。 我刚才通过java代码来检查。 从我的发现来看,存在两个不同的问题:第一个是TCP RFC本身,它允许远程closures的套接字以半双工的方式传输数据,所以远程closures的套接字仍然是半开放的。 根据RFC,RST不会closures连接,您需要发送一个明确的ABORT命令; 所以Java允许通过半封闭的套接字发送数据

(有两种方法可以读取两个端点的closures状态。)

另一个问题是实现说这个行为是可选的。 随着Java努力成为便携式,他们实现了最好的通用function。 我猜,维护(操作系统,半双工实现)的地图会是一个问题。

Java IO堆栈在发生突然拆卸时被肯定发送FIN。 这是没有道理的,你无法检测到这一点,大部分客户端只发送FIN如果他们正在closures连接。

另一个原因是我真的开始讨厌NIO的Java类。 似乎一切都是一点半屁股。

这是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 */ } 

这是一个蹩脚的解决方法。 使用SSL;)SSL会在拆卸时进行紧密的握手,所以你会被通知被closures的套接字(大多数的实现似乎是在进行一次性握手拆卸)。

这种行为(不是Java特定的)的原因是,您不能从TCP堆栈获取任何状态信息。 毕竟,一个套接字只是另一个文件句柄,你不能确定是否有实际的数据读取,而没有真正尝试( select(2)不会有帮助,它只是表示你可以尝试没有阻止) 。

有关更多信息,请参阅Unix套接字FAQ 。

只有写入要求交换数据包,以确定连接丢失。 常见的解决方法是使用KEEP ALIVE选项。

在处理半开放的Java套接字时,可能需要查看isInputShutdown()和isOutputShutdown() 。