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

Posted

技术标签:

【中文标题】为啥不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭?【英文标题】:Why is it impossible, without attempting I/O, to detect that TCP socket was gracefully closed by peer?为什么不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭? 【发布时间】:2010-09-14 09:22:27 【问题描述】:

作为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
...

【问题讨论】:

我想说:SCTP 协议没有这个“问题”。 SCTP 不像 TCP 那样有半关闭状态,换句话说,当另一端关闭其发送套接字时,一侧不能继续发送数据。这应该会让事情变得更容易。 我们有两个邮箱(套接字)...................... ............. 邮箱使用 RoyalMail (IP) 互相发送邮件,忘记 TCP ...... .............................一切都很好,邮箱可以互相发送/接收邮件(最近有很多延迟)发送同时接收没有问题。 ............. 如果一个邮箱被卡车撞倒并发生故障.... 另一个邮箱怎么知道?它必须由 Royal Mail 通知,而后者在下一次尝试从该失败的邮箱发送/接收邮件之前不会知道.. ......呃...... 如果你不打算从套接字读取或写入套接字,你为什么要关心?如果您要从套接字读取或写入套接字,为什么还要进行额外检查?用例是什么? Socket.close 不是优雅的结束。 @immibis 这肯定是一个优雅的关闭,除非套接字接收缓冲区中有未读数据或者你弄乱了 SO_LINGER。 【参考方案1】:

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

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

【讨论】:

在我的测试示例中(也许我应该在这里提供一个......)没有故意通过连接发送/接收数据。所以,我很确定堆栈会收到 FIN(优雅)或 RST(在某些非优雅场景中)。 netstat 也证实了这一点。 当然——如果没有缓冲,那么 FIN 将立即发送到一个空包(无负载)上。但是,在 FIN 之后,连接的那一端不再发送数据包(它仍然会 ACK 发送给它的任何内容)。 会发生什么情况是连接的双方最终在 CLOSE_WAITFIN_WAIT_2 并且它处于这种状态 isConcected() isClosed() 仍然看不到连接已经终止。 感谢您的建议!我想我现在更好地理解了这个问题。我把问题说得更具体了(见第三段):为什么没有“Socket.isClosing”来测试半关闭连接?【参考方案2】:

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

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

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

【讨论】:

我想你在谈论 RFC 793 (faqs.org/rfcs/rfc793.html) 第 3.5 节关闭连接。我不确定它是否解释了这个问题,因为双方都完成了连接的正常关闭并最终处于不应发送/接收任何数据的状态。 视情况而定。您在套接字上看到多少个 FIN?此外,可能是特定于平台的问题:可能 windows 用 FIN 回复每个 FIN,并且两端的连接都关闭,但其他操作系统可能不会这样做,这就是问题 2 出现的地方 不,很遗憾,情况并非如此。 isOutputShutdown 和 isInputShutdown 是每个人在遇到这种“发现”时首先尝试的事情,但这两种方法都返回 false。我刚刚在 Windows XP 和 Linux 2.6 上对其进行了测试。即使在尝试读取之后,所有 4 个方法的返回值都保持不变 郑重声明,这不是半双工的。半双工是指一次只能发送一侧;双方仍然可以发送。 isInputShutdown 和 isOutputShutdown 测试连接的 local 端 - 它们是用于确定您是否在此 Socket 上调用了 shutdownInput 或 shutdownOutput 的测试。他们没有告诉你任何关于远程连接的信息。【参考方案3】:

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

来自Wikipedia:

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

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

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

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

【讨论】:

我不这么认为,你可以在windows和linux上测试C代码中的状态!!!由于某种原因,Java 可能不会公开一些东西,就像公开 windows 和 linux 上的 getsockopt 函数一样。事实上,下面的答案有一些 linux 端的 linux C 代码。 我不认为拥有 'isPeerClosed()' 方法会以某种方式让您在每次读取尝试之前调用它。只有当您明确需要它时,您才可以简单地调用它。我同意当前的套接字实现并不是那么糟糕,即使如果你想知道套接字的远程部分是否关闭,它也需要你写入输出流。因为如果不是,你还要在另一边处理你的书面数据,简直就像坐在垂直的钉子上一样大快人心;) 确实的意思是'没有更多的数据包在飞行'。 FIN 是在 任何传输中的数据之后收到的。然而它的意思是对等端已经关闭了套接字用于输入。你必须**send一些东西*并得到一个'连接重置'来检测。 FIN 可能只是意味着关闭输出。【参考方案4】:

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

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

【讨论】:

REALbasic 套接字(在 Mac OS X 和 Linux 上)基于 BSD 套接字,但是当连接被另一端断开时,RB 设法给你一个很好的错误 102。所以我同意原始海报,这应该是可能的,Java(和 Cocoa)不提供它是蹩脚的。 @JoeStrout RB 只能在您执行某些 I/O 时执行此操作。没有 API 可以在不执行 I/O 的情况下为您提供连接状态。时期。这不是 Java 的缺陷。这实际上是由于 TCP 中缺少“拨号音”,这是一个经过深思熟虑的设计特性。 select() 告诉您是否有数据或 EOS 可供读取而不会阻塞。 “你可以在没有阻塞的情况下尝试的信号”是没有意义的。如果您处于非阻塞模式,您可以总是尝试不阻塞。 select() 由套接字接收缓冲区中的数据或套接字发送缓冲区中的待处理 FIN 或空间驱动。 @EJP getsockopt(SO_ERROR) 怎么样?事实上,即使getpeername 也会告诉你套接字是否仍然连接。【参考方案5】:

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

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

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

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

【讨论】:

Java 也不能提供任何额外的可移植方法。也许他们可以创建一个 isCloseWait() 方法,如果平台不支持它会返回 false,但是如果他们只在支持的平台上进行测试,有多少程序员会被这个陷阱所困扰? 看起来它可以移植给我......windows有这个msdn.microsoft.com/en-us/library/windows/desktop/…和linux这个pubs.opengroup.org/onlinepubs/009695399/functions/… 不是程序员习惯了;就是套接字接口对程序员有用。请记住,套接字抽象不仅仅用于 TCP 协议。 Java 中没有像isCloseWait() 这样的方法,因为并非所有平台都支持它。 ident (RFC 1413) 协议允许服务器在发送响应后保持连接打开,或关闭连接而不发送更多数据。 Java ident 客户端可能会选择保持连接打开以避免下次查找时的 3 次握手,但它如何知道连接仍然打开?它应该尝试通过重新打开连接来响应任何错误吗?还是协议设计错误?【参考方案6】:

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

【讨论】:

我认为允许端点通过发送设置了 FIN 的数据包来启动正常连接关闭,而无需写入任何有效负载。 @Alexander 当然可以,但这与这个答案无关,这是关于检测较少的连接。【参考方案7】:

这是 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, ...)做同样的事情。 错误文件描述符集不会指示关闭连接。请阅读选择手册:'exceptfds - 此设置用于“异常情况”。在实践中,只有一个这样的异常情况是常见的:从 TCP 套接字读取的带外 (OOB) 数据的可用性。 FIN 不是 OOB 数据。 您可以使用 'Selector' 类来访问 'select()' 系统调用。不过它使用 NIO。 连接被对方​​关闭并没有什么异常。 许多平台,包括 Java,do提供对 select() 系统调用的访问。【参考方案8】:

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

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

【讨论】:

另外,当存在 FIN 时,我似乎只在读取时获得和结束流(-1 返回)。所以这是我能看到的在读取端检测到关闭的唯一方法。 你可以检测到。阅读时获得EOS。 Java 不发送 FIN。 TCP 就是这样做的。 Java 没有实现 TCP/IP,它只是使用平台实现。【参考方案9】:

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

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

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

【讨论】:

【参考方案10】:

当谈到处理半开的 Java 套接字时,你可能想看看 isInputShutdown() 和 isOutputShutdown()。

【讨论】:

没有。这只会告诉你你调用了什么,而不是对等体调用了什么。 愿意分享您的声明来源吗? 愿意分享您的相反来源吗?是你的说法。如果你有证据,就让我们来吧。我断言你是不正确的。做实验,证明我错了。 三年后没有实验。 QED【参考方案11】:

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

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

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

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

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

如下Java sn -p所示:

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 确实执行了优雅的关闭。这不是“残酷的”。 @EJP,“优雅断开连接”是发生在 TCP 级别的特定交换,客户端应该向服务器发出断开连接的信号,而服务器又会在关闭其端之前发送剩余数据. “发送剩余数据”部分必须由程序处理(尽管大多数时候人们不会发送任何东西)。调用socket.close() 是“残酷的”,因为它不尊重此客户端/服务器信号。只有当自己的套接字输出缓冲区已满时,服务器才会收到客户端断开连接的通知(因为对方没有确认数据,而对方已关闭)。 更多信息请见MSDN。 @Matthieu 如果您的应用程序没有读取所有可用数据,这在应用程序层可能是不正常的,但在 TCP 传输层,数据仍然被接收并且连接正常终止。如果您的应用程序从输入流中读取所有数据并仅将其丢弃,情况也是如此。 @LeonidUsov 这根本不正确。 Java read() 在流结束时返回 -1,并且无论您调用多少次都会继续这样做。 A C read()recv() 在流结束时返回零,并且无论您调用多少次都会继续这样做。【参考方案12】:

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

【讨论】:

在 java 中使用 SSL 时如何“通知”套接字被关闭?

以上是关于为啥不尝试 I/O 就不可能检测到 TCP 套接字已被对等方优雅地关闭?的主要内容,如果未能解决你的问题,请参考以下文章

在windows系统中对套接字数据进行io时为啥不可以直接使用文件io相关函数?

通过 TCP 套接字进行 i/o 调用的问题

为什么网络I/O会被阻塞?

I/O复用 - 各种不同的IO模型

为什么网络I/O会被阻塞?

为什么网络I/O会被阻塞?