作为 recent question 的后续,我想知道为什么在 Java 中,如果不尝试在 TCP 套接字上读取/写入,就不可能检测到套接字已被对等方优雅地关闭?无论使用 pre-NIO Socket
还是 NIO SocketChannel
,情况似乎都是如此。
当对等方优雅地关闭 TCP 连接时,连接两端的 TCP 堆栈都知道这一事实。服务器端(启动关闭的那个)最终处于状态 FIN_WAIT2
,而客户端(没有明确响应关闭的那个)最终处于状态 CLOSE_WAIT
。为什么 Socket
或 SocketChannel
中没有可以查询 TCP 堆栈以查看底层 TCP 连接是否已终止的方法?是不是 TCP 栈没有提供这样的状态信息?或者是为了避免对内核进行昂贵的调用而做出的设计决定?
在已经发布了这个问题的一些答案的用户的帮助下,我想我知道问题可能来自哪里。未明确关闭连接的一方最终处于 TCP 状态 CLOSE_WAIT
,这意味着连接正在关闭并等待该方发出自己的 CLOSE
操作。我认为 isConnected
返回 true
而 isClosed
返回 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
...
Socket.close
不是优雅的结束。
我经常使用套接字,主要是与选择器一起使用,虽然不是网络 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 只是遵循套接字编程的传统。
从 Wikipedia:
TCP 提供从一台计算机上的一个程序到另一台计算机上的另一个程序的可靠、有序的字节流传递。
握手完成后,TCP 不会在两个端点(客户端和服务器)之间进行任何区分。术语“客户端”和“服务器”主要是为了方便。因此,“服务器”可能正在发送数据,而“客户端”可能正在同时向对方发送一些其他数据。
“关闭”一词也具有误导性。只有FIN声明,意思是“我不会再给你发东西了”。但这并不意味着飞行中没有数据包,或者对方无话可说。如果您将蜗牛邮件实现为数据链路层,或者如果您的数据包经过不同的路由,则接收方可能会以错误的顺序接收数据包。 TCP 知道如何为您解决这个问题。
此外,作为一个程序,您可能没有时间继续检查缓冲区中的内容。因此,在您方便时,您可以检查缓冲区中的内容。总而言之,当前的套接字实现还不错。如果确实存在 isPeerClosed(),那么每次您想调用 read 时都必须进行额外调用。
底层的套接字 API 没有这样的通知。
无论如何,发送 TCP 堆栈直到最后一个数据包才会发送 FIN 位,因此当发送应用程序在发送数据之前逻辑关闭其套接字时,可能会缓冲大量数据。同样,由于网络比接收应用程序更快而缓冲的数据(我不知道,也许您正在通过较慢的连接中继它)对接收器可能很重要,并且您不希望接收应用程序丢弃它只是因为堆栈已收到 FIN 位。
CLOSE_WAIT
和 FIN_WAIT_2
并且它处于这种状态 isConceted()
和 isClosed()
仍然看不到连接已终止。
由于到目前为止没有一个答案完全回答了这个问题,我总结了我目前对这个问题的理解。
当建立 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 提供任何额外的方法。
isCloseWait()
这样的方法,因为并非所有平台都支持它。
可以使用 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 类的另一个原因。似乎一切都有些半途而废。
这是一个有趣的话题。我刚刚挖掘了java代码来检查。根据我的发现,有两个明显的问题:第一个是 TCP RFC 本身,它允许远程关闭的套接字以半双工方式传输数据,因此远程关闭的套接字仍然是半开的。根据 RFC,RST 不会关闭连接,您需要发送显式 ABORT 命令;所以Java允许通过半封闭套接字发送数据
(有两种方法可以读取两个端点的关闭状态。)
另一个问题是实现说这种行为是可选的。由于 Java 力求可移植,它们实现了最好的通用特性。我猜,维护(操作系统,半双工的实现)的地图将是一个问题。
这是 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, ...)
做同样的事情。
这是一个蹩脚的解决方法。使用 SSL ;) 并且 SSL 在拆卸时会进行关闭握手,因此您会收到套接字被关闭的通知(大多数实现似乎都会进行属性握手拆卸)。
这种行为(不是 Java 特定的)的原因是您没有从 TCP 堆栈获得任何状态信息。毕竟,套接字只是另一个文件句柄,如果没有实际尝试,您将无法确定是否有实际数据要从中读取(select(2)
对此无济于事,它仅表示您可以尝试不阻塞)。
有关详细信息,请参阅 Unix socket FAQ。
select()
告诉您是否有数据或 EOS 可供读取而不会阻塞。 “你可以在没有阻塞的情况下尝试的信号”是没有意义的。如果您处于非阻塞模式,您可以总是尝试不阻塞。 select()
由套接字接收缓冲区中的数据或未决 FIN 或套接字发送缓冲区中的空间驱动。
getsockopt(SO_ERROR)
呢?事实上,即使是 getpeername
也会告诉你套接字是否仍然连接。
只有写入需要交换数据包才能确定连接丢失。一个常见的解决方法是使用 KEEP ALIVE 选项。
在处理半开 Java 套接字时,可能需要查看 isInputShutdown() 和 isOutputShutdown()。
socket.close()
是“残酷的”,因为它不尊重此客户端/服务器信号。仅当其自己的套接字输出缓冲区已满时,服务器才会收到客户端断开连接的通知(因为对方没有确认数据,而对方已关闭)。read()
在流结束时返回 -1,并且无论您调用多少次都会继续这样做。 ACread()
或recv()
在流结束时返回零,并且无论您调用多少次都会继续这样做。