95

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

当对等方优雅地关闭 TCP 连接时,连接两端的 TCP 堆栈都知道这一事实。服务器端(启动关闭的那个)以 state 结束FIN_WAIT2,而客户端(没有明确响应关闭的那个)以 state 结束CLOSE_WAIT。为什么没有方法Socket可以SocketChannel查询 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
...
4

12 回答 12

29

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

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

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

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

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

如以下 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 页

于 2012-02-22T17:12:34.437 回答
19

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

来自维基百科

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

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

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

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

于 2008-10-01T06:06:54.953 回答
11

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

无论如何,发送 TCP 堆栈直到最后一个数据包才会发送 FIN 位,因此当发送应用程序在发送数据之前逻辑关闭其套接字时,可能会缓冲大量数据。同样,由于网络比接收应用程序更快而缓冲的数据(我不知道,也许您正在通过较慢的连接中继它)对接收器可能很重要,并且您不希望接收应用程序丢弃它只是因为堆栈已收到 FIN 位。

于 2008-09-30T22:11:15.457 回答
8

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

当建立 TCP 连接并且一个对等方调用close()shutdownOutput()套接字时,连接另一侧的套接字将转换为CLOSE_WAIT状态。原则上,可以从 TCP 堆栈中找出套接字是否处于CLOSE_WAIT状态而无需调用read/recv(例如,getsockopt()在 Linux 上: http: //www.developerweb.net/forum/showthread.php? t=4395 ),但事实并非如此便携的。

Java 的Socket类似乎旨在提供与 BSD TCP 套接字相当的抽象,可能是因为这是人们在编写 TCP/IP 应用程序时习惯的抽象级别。BSD 套接字是支持 INET(例如 TCP)之外的套接字的泛化,因此它们不提供查找套接字 TCP 状态的可移植方式。

没有这样的方法,isCloseWait()因为人们习惯于在 BSD 套接字提供的抽象级别上对 TCP 应用程序进行编程,并不期望 Java 提供任何额外的方法。

于 2008-10-01T13:17:37.040 回答
7

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

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

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

于 2011-08-10T12:28:09.317 回答
4

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

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

另一个问题是实现说这种行为是可选的。由于 Java 力求可移植,它们实现了最好的通用特性。我猜,维护(操作系统,半双工的实现)的地图将是一个问题。

于 2008-09-30T22:15:35.900 回答
4

当 Java IO 堆栈在突然拆除时被破坏时,它肯定会发送 FIN。您无法检测到这一点是没有意义的,b/c 大多数客户端仅在关闭连接时才发送 FIN。

...我真的开始讨厌 NIO Java 类的另一个原因。似乎一切都有些半途而废。

于 2009-07-06T03:44:06.653 回答
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 */  
}  
于 2009-01-24T21:23:08.730 回答
2

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

于 2012-08-24T15:47:59.617 回答
2

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

有关详细信息,请参阅Unix 套接字常见问题解答

于 2008-10-01T11:09:11.187 回答
0

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

于 2008-10-10T17:35:44.047 回答
-3

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

于 2011-10-27T10:34:26.283 回答