为什么在没有尝试I / O的情况下不可能检测到TCP套接字已被对等端正常关闭?
作为对最近问题的回应,我想知道为什么在Java中如果不尝试在TCP套接字上进行读/写操作就无法检测到套接字已被对等端正常关闭了,这是为什么呢?无论使用的是NIO之前的Socket还是NIO的SocketChannel,似乎都是这种情况。
当对等方正常关闭TCP连接时,连接两端的TCP堆栈都会知道这一事实。服务器端(发起关闭操作的服务器端)以状态FIN_WAIT2结束,而客户端端(未明确响应关闭操作的服务器端)以状态CLOSE_WAIT结束。为什么在Socket或者SocketChannel中没有方法可以查询TCP堆栈以查看基础TCP连接是否已终止? TCP堆栈不提供这种状态信息吗?还是为了避免昂贵地调用内核而做出的设计决定?
在已经发布了该问题的一些答案的用户的帮助下,我想我知道问题可能来自何处。没有显式关闭连接的那一侧将以TCP状态" CLOSE_WAIT"结束,这意味着该连接正在关闭过程中,并等待该一侧发出自己的" CLOSE"操作。我认为公平的是," isConnected"返回" true",而" isClosed"返回" false",但是为什么没有诸如" isClosing"这样的东西呢?
下面是使用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 ...
解决方案
基础套接字API没有此类通知。
无论如何,发送TCP堆栈直到最后一个数据包都不会发送FIN位,因此从发送应用程序逻辑上关闭其套接字之前(甚至在发送该数据之前)开始,可能会有很多数据被缓冲。同样,由于网络比接收应用程序快(我不知道,也许我们正在通过较慢的连接中继数据)而被缓冲的数据对接收者来说很重要,并且我们不希望接收应用程序丢弃它只是因为FIN位已被堆栈接收。
这是一个有趣的话题。我刚刚浏览了Java代码以进行检查。根据我的发现,有两个不同的问题:第一个是TCP RFC本身,它允许远程关闭的套接字以半双工传输数据,因此远程关闭的套接字仍处于半开放状态。根据RFC,RST不会关闭连接,我们需要发送一个显式的ABORT命令。因此Java允许通过半封闭套接字发送数据
(有两种方法可以读取两个端点的关闭状态。)
另一个问题是实现表示此行为是可选的。随着Java努力实现可移植性,他们实现了最佳的通用功能。我猜,维护(操作系统,半双工的实现)的映射将是一个问题。
我认为这更多是一个套接字编程问题。 Java只是遵循套接字编程的传统。
从维基百科:
TCP provides reliable, ordered delivery of a stream of bytes from one program on one computer to another program on another computer.
握手完成后,TCP不会在两个端点(客户端和服务器)之间进行任何区分。术语"客户端"和"服务器"主要是为了方便起见。因此,"服务器"可能正在发送数据,而"客户端"可能正在同时发送其他一些数据。
术语"关闭"也具有误导性。只有FIN声明,这意味着"我将不再向我们发送任何东西"。但这并不意味着就没有飞行中的数据包,或者其他没有更多要说的。如果将蜗牛邮件实现为数据链路层,或者数据包经过不同的路由,则接收方接收到数据包的顺序可能有误。 TCP知道如何为我们解决此问题。
另外,作为一个程序,我们可能没有时间继续检查缓冲区中的内容。因此,在我们方便的时候,我们可以检查缓冲区中的内容。总而言之,当前的套接字实现还不错。如果实际上有isPeerClosed(),则每次调用read时都要进行额外的调用。
出现这种情况的原因(不是Java特定的)是因为我们没有从TCP堆栈中获取任何状态信息。毕竟,套接字只是另一个文件句柄,如果不实际尝试就无法发现是否有实际数据要从中读取(select(2)
在那里无济于事,它只是表明我们可以尝试不使用它)封锁)。
有关更多信息,请参见Unix套接字常见问题解答。
由于到目前为止,没有一个答案可以完全回答这个问题,因此,我在总结我对这个问题的当前理解。
建立TCP连接并且一个对等方在其套接字上调用" close()"或者" shutdownOutput()"时,连接另一端的套接字将转换为" CLOSE_WAIT"状态。原则上,可以从TCP堆栈中找出套接字是否处于" CLOSE_WAIT"状态,而无需调用" read / recv"(例如Linux上的" getsockopt()"):http://www.developerweb.net/forum /showthread.php?t=4395),但这不是可移植的。
Java的" Socket"类似乎旨在提供与BSD TCP套接字相当的抽象,可能是因为这是人们在编写TCP / IP应用程序时所习惯的抽象级别。 BSD套接字是一种通用的支持,而不仅仅是INET(例如TCP)套接字,因此它们没有提供一种可移植的方式来找出套接字的TCP状态。
没有像isCloseWait()这样的方法,因为习惯于在BSD套接字提供的抽象级别上对TCP应用程序进行编程的人们并不希望Java提供任何额外的方法。
仅写要求交换分组,这使得可以确定连接的丢失。常见的解决方法是使用KEEP ALIVE选项。
这是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 */ }
Java IO堆栈在突然崩溃时被破坏时肯定会发送FIN。根本无法检测到这一点毫无意义,因为大多数客户端仅在关闭连接时才发送FIN。
...我真正开始讨厌NIO Java类的另一个原因。似乎一切都有些混蛋。