为什么在没有尝试I / O的情况下不可能检测到TCP套接字已被对等端正常关闭?

时间:2020-03-06 14:57:00  来源:igfitidea点击:

作为对最近问题的回应,我想知道为什么在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类的另一个原因。似乎一切都有些混蛋。