为什么不尝试 I/O,就不可能检测到 TCP 套接字已被对等方正常关闭?

2022-08-31 10:53:13

作为对最近一个问题的跟进,我想知道为什么在Java中,如果不尝试在TCP套接字上读取/写入,就不可能检测到套接字已被对等体优雅地关闭?无论使用前NIO还是NIO,情况似乎都是如此。SocketSocketChannel

当对等方优雅地关闭 TCP 连接时,连接两端的 TCP 堆栈都知道这一事实。服务器端(启动关机的服务器端)最终处于 状态 ,而客户端(未显式响应关机的服务器端)最终处于状态。为什么在 或 中没有可以查询 TCP 堆栈以查看底层 TCP 连接是否已终止的方法?是 TCP 堆栈不提供此类状态信息吗?还是为了避免对内核进行代价高昂的调用而做出的设计决策?FIN_WAIT2CLOSE_WAITSocketSocketChannel

在已经发布了这个问题的一些答案的用户的帮助下,我想我看到了问题可能来自哪里。未显式关闭连接的一方最终处于 TCP 状态,这意味着连接正在关闭并等待该端发出自己的操作。我想返回和返回是足够公平的,但是为什么没有这样的东西呢?CLOSE_WAITCLOSEisConnectedtrueisClosedfalseisClosing

以下是使用 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
...

答案 1

我经常使用Sockets,主要是与Selectors一起使用,虽然不是网络OSI专家,但根据我的理解,调用Socket实际上会在网络上发送一些东西(FIN),这会唤醒另一端的选择器(C语言中的行为相同)。在这里,您有检测:实际检测在您尝试时将失败的读取操作。shutdownOutput()

在您提供的代码中,关闭套接字将关闭输入流和输出流,而无法读取可能可用的数据,因此会丢失它们。Java方法执行“优雅”断开连接(与我最初的想法相反),因为输出流中剩余的数据将被发送,然后发送FIN以发出其关闭信号。FIN将由另一方确认,就像任何常规数据包将1一样。Socket.close()

如果您需要等待另一端关闭其插槽,则需要等待其 FIN。要实现这一点,您必须检测 ,这意味着您不应该关闭套接字,因为它会关闭其输入流Socket.getInputStream().read() < 0

从我在C中所做的以及现在在Java中所做的工作来看,实现这种同步关闭应该是这样的:

  1. 关闭套接字输出(在另一端发送 FIN,这是此套接字发送的最后一件事)。输入仍处于打开状态,因此您可以检测遥控器read()close()
  2. 读取套接字,直到我们从另一端收到回复 FIN(因为它将检测到 FIN,它将经历相同的优雅断开连接过程)。这在某些操作系统上很重要,因为只要其中一个缓冲区仍然包含数据,它们实际上就不会关闭套接字。它们被称为“ghost”套接字,并在操作系统中用尽了描述符编号(这在现代操作系统中可能不再是问题)InputStream
  3. 关闭套接字(通过调用或关闭其或Socket.close()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
}

当然,双方必须使用相同的闭合方式,否则发送部分可能总是发送足够的数据来保持循环繁忙(例如,如果发送部分仅发送数据并且从不读取以检测连接终止。这很笨拙,但你可能无法控制)。while

正如@WarrenDew在他的评论中指出的那样,丢弃程序(应用层)中的数据会导致应用层的非正常断开连接:尽管所有数据都是在TCP层(循环)上接收的,但它们被丢弃了。while

1:摘自“Java 中的基础网络”:参见图 3.3 p.45,以及整个 §3.7,第 43-48 页


答案 2

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

来自维基百科

TCP 提供从一台计算机上的一个程序到另一台计算机上的另一个程序的可靠、有序的字节流传递。

握手完成后,TCP不会对两个端点(客户端和服务器)进行任何区分。术语“客户端”和“服务器”主要是为了方便。因此,“服务器”可能正在发送数据,而“客户端”可能同时向彼此发送一些其他数据。

“关闭”一词也具有误导性。只有FIN声明,这意味着“我不会再给你发送任何东西了。但这并不意味着没有包在飞行中,或者另一个没有更多的话要说。如果将蜗牛邮件实现为数据链路层,或者数据包通过不同的路由,则接收方可能以错误的顺序接收数据包。TCP知道如何为您解决此问题。

此外,作为一个程序,您可能没有时间继续检查缓冲区中的内容。因此,在您方便的时候,您可以检查缓冲区中的内容。总而言之,当前的套接字实现并没有那么糟糕。如果真的有 isPeerClosed(),那就是你每次想调用 read 时必须进行的额外调用。


推荐