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

时间:2008-09-30 21:49:07

标签: java sockets tcp network-programming

作为recent question的后续内容,我想知道为什么在没有尝试在TCP套接字上读/写的情况下,Java不可能检测到套接字已被对等端正常关闭?无论是否使用前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
...

12 个答案:

答案 0 :(得分:29)

我一直在使用套接字,主要是使用选择器,虽然不是网络OSI专家,但根据我的理解,在Socket上调用shutdownOutput()实际上会在网络上发送一些内容(FIN)唤醒我的选择器另一方面(C语言中的行为相同)。在这里你有检测:实际检测到你尝试时会失败的读取操作。

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

如果您需要等待另一方关闭其套接字,则需要等待其FIN。要实现这一点,您必须检测Socket.getInputStream().read() < 0,这意味着您关闭套接字,因为关闭其{{1} }

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

  1. 关闭套接字输出(在另一端发送FIN,这是此套接字将发送的最后一件事)。输入仍处于打开状态,因此您可以InputStream并检测远程read()
  2. 读取套接字close(),直到我们从另一端收到回复FIN(因为它将检测到FIN,它将经历相同的优雅的diconnection过程)。这在某些操作系统上很重要,因为只要其中一个缓冲区仍包含数据,它们就不会实际关闭套接字。它们被称为“幽灵”套接字并在操作系统中使用描述符编号(现代操作系统可能不再是问题)
  3. 关闭套接字(通过调用InputStream或关闭其Socket.close()InputStream
  4. 如以下Java代码段所示:

    OutputStream

    当然双方必须使用相同的关闭方式,或者发送部分可能始终发送足够的数据以保持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 } 循环忙(例如,如果发送部分是只发送数据而从不读取以检测连接终止。这是笨拙的,但你可能无法控制它。)

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

    1 :来自“Fundamental Networking in Java”:见图。 3.3 p.45,以及整个§3.7,pp 43-48

答案 1 :(得分:18)

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

来自Wikipedia

  

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

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

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

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

答案 2 :(得分:11)

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

发送TCP堆栈不会发送FIN位,直到最后一个数据包为止,因此当发送应用程序在发送数据之前逻辑上关闭其套接字时,可能会缓冲大量数据。同样,因为网络比接收应用程序更快而缓冲的数据(我不知道,也许你通过较慢的连接中继它)对接收者来说可能是重要的,你不希望接收应用程序丢弃它只是因为堆栈已经收到了FIN位。

答案 3 :(得分:8)

由于到目前为止没有一个答案完全回答这个问题,我总结了我目前对这个问题的理解。

当建立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提供任何额外的方法。

答案 4 :(得分:7)

检测(TCP)套接字连接的远程端是否已关闭可以使用java.net.Socket.sendUrgentData(int)方法完成,如果远程端关闭,则捕获它抛出的IOException。这已经在Java-Java和Java-C之间进行了测试。

这避免了设计通信协议以使用某种ping机制的问题。通过在套接字上禁用OOBInline(setOOBInline(false),接收的任何OOB数据都将被静默丢弃,但仍可以发送OOB数据。如果远程端关闭,则尝试连接重置,失败并导致抛出某些IOException

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

答案 5 :(得分:4)

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

(有两种方法可以在两个端点读取关闭状态。)

另一个问题是实现说这种行为是可选的。随着Java努力实现可移植性,他们实现了最佳的通用功能。我想,维护一张(OS,实现半双工)的地图本来就是个问题。

答案 6 :(得分:4)

Java IO堆栈在突然拆除时会被强制发送FIN。没有意义的是你无法检测到这一点,b / c大多数客户只有在关闭连接时才发送FIN。

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

答案 7 :(得分:3)

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

答案 8 :(得分:2)

此行为(不是特定于Java)的原因是您没有从TCP堆栈获取任何状态信息。毕竟,套接字只是另一个文件句柄,你无法确定是否有实际数据要从中读取而没有实际尝试(select(2)在那里没有帮助,它只表示你可以尝试不用阻塞)。

有关详细信息,请参阅Unix socket FAQ

答案 9 :(得分:2)

这是一个蹩脚的解决方法。使用SSL;)和SSL在拆解时进行紧密握手,因此您会收到关闭套接字的通知(大多数实现似乎都在进行属性握手拆解)。

答案 10 :(得分:0)

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

答案 11 :(得分:-3)

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