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


92

作为对最近一个问题的回应,我想知道为什么在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
...

我要提一提的是:SCTP协议没有这个“问题”。SCTP不像TCP那样具有半关闭状态,换句话说,一侧不能继续发送数据,而另一端已经关闭了其发送套接字。那应该使事情变得容易。
TarnayKálmán09年

2
我们有两个邮箱(套接字).................................................... ............邮箱使用RoyalMail(IP)向彼此发送邮件,而不必担心TCP.....................................。 ......一切都很好,花哨的,邮箱可以互相发送/接收邮件(最近有很多滞后),而没有任何问题。.............如果一个邮箱被卡车撞到而失败....另一个邮箱怎么知道?它必须由Royal Mail通知,Royal Mail直到下一次
尝试

如果您不打算从套接字读取或写入套接字,为什么要关心呢?而且,如果您要从套接字读取或写入套接字,为什么还要额外检查?用例是什么?
David Schwartz

2
Socket.close不是一个优雅的关闭。
user253751 2014年

@immibis除非套接字接收缓冲区中有未读的数据或者您对SO_LINGER感到困惑,否则肯定是一个正常的关闭。
罗恩侯爵

Answers:


29

我经常使用Sockets,大多数情况下是使用Selectors,尽管不是网络OSI专家,但据我了解,调用shutdownOutput()Socket实际上会在网络(FIN)上发送某些内容,从而唤醒另一端的Selector(C语言中的相同行为)语言)。在这里,您可以进行检测:实际检测到尝试读取操作将失败的读取操作。

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

如果需要等待另一侧关闭其插槽,则需要等待其FIN。为了实现这一点,您必须检测Socket.getInputStream().read() < 0,这意味着您不应该关闭它的那样关闭InputStream套接字。

从我在C语言以及现在在Java语言中所做的工作来看,应该这样完成同步关闭:

  1. 关闭套接字输出(在另一端发送FIN,这是此套接字将发送的最后一件事)。输入仍处于打开状态,因此您可以read()检测到遥控器close()
  2. 读取套接字,InputStream直到我们从另一端收到Reply-FIN(因为它将检测到FIN,它将通过相同的正常双连接过程)。这在某些操作系统上很重要,因为只要它们的缓冲区之一仍包含数据,它们就不会真正关闭套接字。它们被称为“ ghost”套接字,并在操作系统中用完描述符号(对于现代操作系统,这可能不再是问题)
  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第45页,以及整个3.7节,第43-48页


Java确实执行正常关闭。它不是“残酷的”。
罗恩侯爵

2
@EJP,“正常断开连接”是在TCP级别进行的特定交换,在此客户端应向服务器发出断开连接的信号,客户端将在断开服务器连接之前发送剩余数据。“发送剩余数据”部分必须由程序处理(尽管人们通常不会发送任何东西)。调用socket.close()是不符合此客户端/服务器信令的方式,是“残酷的”。仅当其自己的套接字输出缓冲区已满时(因为没有数据被另一方关闭的另一方确认),服务器才会收到客户端断开连接的通知。
Matthieu 2013年

有关更多信息,请参见MSDN
Matthieu 2013年

Java不会强制应用程序首先调用Socket.close()而不是“发送剩余数据”,并且不会阻止应用程序首先使用关机。Socket.close()就像close(sd)一样。在这方面,Java与Berkeley套接字API本身没有什么不同。的确,在大多数方面。
罗恩侯爵

2
@LeonidUsov那是完全不正确的。Java read()在流的末尾返回-1,并且无论您多次调用它都继续这样做。AC read()recv()在流结束时返回零,并且无论您多次调用它都将继续这样做。
洛恩侯爵,

18

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

维基百科

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

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

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

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


1
我不这么认为,您可以在Windows和Linux上的C代码中测试状态!!!Java由于某种原因可能不会公开某些内容,例如公开Windows和linux上的getsockopt函数会很好。实际上,下面的答案在linux方面有一些linux C代码。
迪恩·希勒

我不认为拥有“ isPeerClosed()”方法会以某种方式使您在每次读取尝试之前调用它。仅在明确需要时才可以调用它。我同意当前的套接字实现不是很糟糕,即使您想了解套接字的远程部分是否关闭,也需要写输出流。因为如果不是这样,那么您也必须在另一侧处理您的书面数据,这和坐在垂直钉子上一样令人愉悦;)
Jan Hruby

1
确实意味着“不再有飞行中的包裹”。传输任何数据之后,将接收到FIN 。但是,这并不意味着对等方已关闭套接字进行输入。您必须“ 发送一些东西”并获得“连接重置”才能检测到。FIN可能只是意味着关闭输出。
罗恩侯爵,

11

基础套接字API没有此类通知。

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


在我的测试示例中(也许我应该在这里提供一个...),没有故意通过连接发送/接收数据。因此,我很确定堆栈会收到FIN(优美)或RST(在某些非优美情况下)。netstat也确认了这一点。
亚历山大

1
当然-如果没有缓冲,则FIN将立即发送,否则将发送一个空的数据包(没有有效负载)。但是,在FIN之后,连接的那端不再发送任何数据包(它仍将对发送给它的任何内容进行ACK)。
Mike Dimmick

1
发生的情况是,连接的侧面最终以<code> CLOSE_WAIT </ code>和<code> FIN_WAIT_2 </ code>表示,并且在这种状态下,<code> isConcected()</ code>和<code> isClosed()</ code>仍然看不到连接已终止。
亚历山大

感谢您的建议!我想我现在对这个问题有了更好的了解。我提出了更具体的问题(请参见第三段):为什么没有“ Socket.isClosing”来测试半封闭连接?
亚历山大

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


3
Java 也无法提供任何额外的方法。也许他们可以创建一个isCloseWait()方法,如果平台不支持该方法,则该方法将返回false,但是如果仅在受支持的平台上进行测试,那么有多少程序员会被该陷阱咬住?
迅速


1
并不是程序员习惯了它。正是套接字接口对程序员有用。请记住,套接字抽象不仅仅用于TCP协议。
沃伦·露

Java中没有类似的方法,isCloseWait()因为并非所有平台都支持该方法。
罗恩侯爵,

ident(RFC 1413)协议允许服务器在发送响应后保持连接打开,或在不发送任何其他数据的情况下关闭连接。Java ident客户端可能选择保持连接打开,以避免在下一次查找时进行三向握手,但是如何知道连接仍然打开?它应该尝试通过重新打开连接来响应任何错误吗?还是协议设计错误?
大卫

7

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

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

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


4

Java IO堆栈在突然崩溃时被破坏时肯定会发送FIN。根本无法检测到这一点毫无意义,因为大多数客户端仅在关闭连接时才发送FIN。

...我真正开始讨厌NIO Java类的另一个原因。似乎一切都有些混蛋。


1
同样,看起来只有当存在FIN时,我只会在读取时获得并结束流(返回-1)。因此,这是我可以看到的在读取侧检测到关机的唯一方法。

3
您可以检测到它。阅读时会得到EOS。而且Java不发送FIN。TCP做到了。Java不实现TCP / IP,它仅使用平台实现。
2013年

4

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

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

另一个问题是实现表示此行为是可选的。随着Java努力实现可移植性,他们实现了最佳的通用功能。我猜,维护(操作系统,半双工的实现)的映射将是一个问题。


1
我想您正在谈论RFC 793(faqs.org/rfcs/rfc793.html)第3.5节“关闭连接”。我不确定它能解释问题,因为双方都完成了正常关闭连接并最终处于不应该发送/接收任何数据的状态。
亚历山大

要看。您在插座上看到多少FIN?另外,可能是特定于平台的问题:也许Windows用FIN答复每个FIN,并且两端的连接都关闭了,但是其他操作系统可能没有这种行为,这就是问题2出现的地方
Lorenzo Boccaccia

1
不,不幸的是事实并非如此。当遇到此“发现”时,isOutputShutdown和isInputShutdown是每个人都尝试的第一件事,但是两种方法都返回false。我刚刚在Windows XP和Linux 2.6上进行了测试。即使经过一次读取尝试,所有4种方法的返回值仍保持不变
Alexander

1
作为记录,这不是半双工的。半双工意味着在s时间内只有一侧可以发送;双方仍然可以发送。
rbp

1
isInputShutdown和isOutputShutdown测试连接的本地端-它们是测试以确定您是否在此Socket上调用了shutdownInput或shutdownOutput。他们没有告诉您有关连接远程端的任何信息。
Mike Dimmick

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

您可以使用进行相同的操作getsocketop(... SOL_SOCKET, SO_ERROR, ...)
大卫·史瓦兹

1
错误文件描述符集不会指示连接已关闭。请阅读选择手册:'exceptfds-注意此设置是否存在“异常情况”。在实践中,只有一种这样的例外情况很常见:从TCP套接字读取的带外(OOB)数据的可用性。FIN不是OOB数据。
Jan Wrobel 2012年

1
您可以使用“选择器”类访问“ select()”系统调用。它虽然使用NIO。
Matthieu

1
另一端已关闭的连接没有什么例外。
David Schwartz

1
许多平台,包括 Java,提供对select()系统调用的访问。
洛恩侯爵,

2

这是一个me脚的解决方法。使用SSL;),SSL会在拆卸时进行紧密握手,因此会通知您套接字已关闭(大多数实现似乎是在进行适当的握手拆卸)。


2
在Java中使用SSL时,如何“通知”套接字已关闭?
Dave B

2

出现这种情况的原因(不是Java特定的)是因为您没有从TCP堆栈中获取任何状态信息。毕竟,套接字只是另一个文件句柄,您无法找出是否有实际数据可以在不实际尝试的情况下从中读取(select(2)这对您没有帮助,它只是表明您可以尝试而不会阻塞)。

有关更多信息,请参见Unix套接字常见问题解答


2
REALbasic套接字(在Mac OS X和Linux上)基于BSD套接字,但是当另一端断开连接时,RB会给您一个很好的错误102。因此,我同意原始的发布者的观点,这应该是可能的,而Java(和Cocoa)没有提供它是很la脚的。
Joe Strout 2010年

1
@JoeStrout RB仅在执行一些I / O时才能这样做。没有API,无需执行I / O即可让您获得连接状态。期。这不是Java的缺陷。实际上,这是由于TCP中缺少“拨号音”,这是故意的设计功能。
洛恩侯爵,

select() 告诉您是否有数据或EOS可供读取而不会阻塞。“您可以尝试而不会阻塞的信号”毫无意义。如果您处于非阻止模式,则可以始终尝试不阻止。select()由套接字接收缓冲区中的数据或套接字发送缓冲区中的未决FIN或空间驱动。
罗恩侯爵

@EJP getsockopt(SO_ERROR)呢?实际上,甚至getpeername还会告诉您套接字是否仍处于连接状态。
大卫·史瓦兹

0

仅写要求交换分组,这允许确定连接丢失。常见的解决方法是使用KEEP ALIVE选项。


我认为端点可以通过发送设置了FIN的数据包来发起正常的连接关闭,而无需写入任何有效负载。
亚历山大

@Alexander当然可以,但是与这个答案无关,这是关于减少连接的检测
罗恩侯爵,

-3

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


不。那只会告诉您您所呼叫的内容,而不是对等方所呼叫的内容。
罗恩侯爵

愿意分享您的陈述来源吗?
JimmyB

3
想要分享您的消息来源吗?这是你的声明。如果您有证明,那就让我们拥有。我断言你不正确。做实验,证明我错了。
洛恩侯爵,

三年后,没有实验。QED
罗恩侯爵,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.