java - 为什么不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭?

标签 java sockets tcp network-programming

作为 recent question 的后续行动,我想知道为什么在 Java 中,如果不尝试在 TCP 套接字上读/写,就不可能检测到套接字已被对等方优雅地关闭?无论是使用 pre-NIO Socket 还是 NIO SocketChannel,似乎都是这种情况。

当对等端优雅地关闭 TCP 连接时,连接两端的 TCP 堆栈都知道这一事实。服务器端(启动关闭的那个)最终处于状态 FIN_WAIT2,而客户端(没有明确响应关闭的那个)最终处于状态 CLOSE_WAIT 。为什么 SocketSocketChannel 中没有可以查询 TCP 堆栈以查看底层 TCP 连接是否已终止的方法?是不是 TCP 栈没有提供这样的状态信息?还是避免对内核进行昂贵的调用是一项设计决策?

在已经发布了这个问题的一些答案的用户的帮助下,我想我知道问题可能来自哪里。没有明确关闭连接的一方最终处于 TCP 状态 CLOSE_WAIT 意味着连接正在关闭并等待一方发出自己的 CLOSE 操作。我想 isConnected 返回 trueisClosed 返回 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
...

最佳答案

我经常使用套接字,主要是与选择器一起使用,虽然不是网络 OSI 专家,但据我了解,调用 shutdownOutput() on a Socket 实际上在网络(FIN)上发送一些东西,唤醒我在另一端的选择器(在 C 语言中的行为相同)。这里你有检测:实际检测到一个读操作,当你尝试它时会失败。

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

如果你需要等待对方关闭它的socket,你需要等待它的FIN。为了实现这一点,您必须检测 Socket.getInputStream().read() < 0 ,这意味着你应该关闭你的套接字,因为它会关闭它的InputStream

从我在 C 中所做的,现在在 Java 中,实现这样的同步关闭应该这样完成:

  1. 关闭套接字输出(在另一端发送 FIN,这是此套接字将发送的最后一件事)。输入仍处于打开状态,因此您可以 read()并检测远程close()
  2. 读取套接字 InputStream直到我们收到来自另一端的回复 FIN(因为它会检测到 FIN,它会经历同样的优雅断开过程)。这在某些操作系统上很重要,因为只要其中一个缓冲区仍然包含数据,它们实际上就不会关闭套接字。它们被称为“幽灵”套接字,并在操作系统中耗尽了描述符编号(现代操作系统可能不再是问题)
  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:来自“Fundamental Networking in Java”:见图。 3.3 p.45,以及整个 §3.7,第 43-48 页

关于java - 为什么不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/155243/

相关文章:

java - 我可以使用 checkstyle、maven-checkstyle-plugin 和 google_checks.xml 启用抑制吗?

sockets - Rust 中 TcpListener 和 TcpAcceptor 的职责

sockets - 什么是 AF_INET,为什么需要它?

.net - 现有连接被远程主机强行关闭

java - 通过传入列标题对不同列进行 ORDER BY

java - 正则表达式精确匹配字符串?

java - 如何正确编码和解码Base64字符串?

python - 如何在 python 中创建 IPv6 套接字?为什么得到 socket.error : (22, 'Invalid argument' )?

PHP 通过 TCP 套接字发送文件

java - 无法使用ip地址访问tomcat