ChatGPT解决这个技术问题 Extra ChatGPT

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

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

M
Matthieu

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

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

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

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

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


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

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

Wikipedia

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

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

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

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


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

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

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


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

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

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


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 次握手,但它如何知道连接仍然打开?它应该尝试通过重新打开连接来响应任何错误吗?还是协议设计错误?
U
Uncle Per

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

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

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


佚名

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

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


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

这是一个有趣的话题。我刚刚挖掘了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 测试连接的本地端 - 它们是用于确定您是否在此 Socket 上调用了 shutdownInput 或 shutdownOutput 的测试。他们不会告诉您任何有关连接远程端的信息。
B
Blazor

这是 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 数据。
您可以使用“选择器”类来访问“选择()”系统调用。虽然它使用 NIO。
被对方关闭的连接没有什么异常。
许多平台,包括 Java,都提供对 select() 系统调用的访问。
D
Dean Hiller

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


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

这种行为(不是 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 也会告诉你套接字是否仍然连接。
R
Ray

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


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

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


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

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅