作为recent question的后续内容,我想知道为什么在没有尝试在TCP套接字上读/写的情况下,Java不可能检测到套接字已被对等端正常关闭?无论是否使用前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
...
答案 0 :(得分:29)
我一直在使用套接字,主要是使用选择器,虽然不是网络OSI专家,但根据我的理解,在Socket上调用shutdownOutput()
实际上会在网络上发送一些内容(FIN)唤醒我的选择器另一方面(C语言中的行为相同)。在这里你有检测:实际检测到你尝试时会失败的读取操作。
在您提供的代码中,关闭套接字将关闭输入和输出流,而无法读取可能的数据,从而丢失它们。 Java Socket.close()
方法执行“正常”断开连接(与我最初的想法相反),因为输出流中剩余的数据将被发送,然后是FIN 以发出关闭信号。 FIN将由另一方确认,因为任何常规数据包都 1 。
如果您需要等待另一方关闭其套接字,则需要等待其FIN。要实现这一点,您必须检测Socket.getInputStream().read() < 0
,这意味着您不关闭套接字,因为关闭其{{1} } 强>
从我在C中所做的,现在在Java中,实现这样的同步关闭应该像这样:
InputStream
并检测远程read()
close()
,直到我们从另一端收到回复FIN(因为它将检测到FIN,它将经历相同的优雅的diconnection过程)。这在某些操作系统上很重要,因为只要其中一个缓冲区仍包含数据,它们就不会实际关闭套接字。它们被称为“幽灵”套接字并在操作系统中使用描述符编号(现代操作系统可能不再是问题)InputStream
或关闭其Socket.close()
或InputStream
)如以下Java代码段所示:
OutputStream
当然双方必须使用相同的关闭方式,或者发送部分可能始终发送足够的数据以保持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
}
循环忙(例如,如果发送部分是只发送数据而从不读取以检测连接终止。这是笨拙的,但你可能无法控制它。)
正如@WarrenDew在评论中指出的那样,丢弃程序(应用程序层)中的数据会导致应用程序层出现非正常的断开连接:尽管所有数据都是在TCP层(while
循环)接收的,他们被丢弃了。
1 :来自“Fundamental Networking in Java”:见图。 3.3 p.45,以及整个§3.7,pp 43-48
答案 1 :(得分:18)
我认为这更像是一个套接字编程问题。 Java只是遵循套接字编程的传统。
来自Wikipedia:
TCP提供可靠,有序 从一个字节传递字节流 将计算机上的程序转移到另一台 另一台计算机上的程序。
握手完成后,TCP不会对两个端点(客户端和服务器)进行任何区分。术语“客户端”和“服务器”主要是为了方便起见。因此,“服务器”可以发送数据,“客户端”可以同时发送一些其他数据。
“关闭”一词也具有误导性。只有FIN声明,这意味着“我不会再向你发送任何东西。”但这并不意味着没有飞行中的数据包,或者另一个没有更多的话要说。如果您将蜗牛邮件实现为数据链路层,或者您的数据包通过不同的路由,则接收器可能会以错误的顺序接收数据包。 TCP知道如何为您解决此问题。
另外,作为程序,您可能没有时间继续检查缓冲区中的内容。因此,在您方便时,您可以检查缓冲区中的内容。总而言之,当前的套接字实现并不是那么糟糕。如果确实存在isPeerClosed(),则每次要调用read时都必须进行额外调用。
答案 2 :(得分:11)
底层套接字API没有这样的通知。
发送TCP堆栈不会发送FIN位,直到最后一个数据包为止,因此当发送应用程序在发送数据之前逻辑上关闭其套接字时,可能会缓冲大量数据。同样,因为网络比接收应用程序更快而缓冲的数据(我不知道,也许你通过较慢的连接中继它)对接收者来说可能是重要的,你不希望接收应用程序丢弃它只是因为堆栈已经收到了FIN位。
答案 3 :(得分:8)
由于到目前为止没有一个答案完全回答这个问题,我总结了我目前对这个问题的理解。
当建立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提供任何额外的方法。
答案 4 :(得分:7)
检测(TCP)套接字连接的远程端是否已关闭可以使用java.net.Socket.sendUrgentData(int)方法完成,如果远程端关闭,则捕获它抛出的IOException。这已经在Java-Java和Java-C之间进行了测试。
这避免了设计通信协议以使用某种ping机制的问题。通过在套接字上禁用OOBInline(setOOBInline(false),接收的任何OOB数据都将被静默丢弃,但仍可以发送OOB数据。如果远程端关闭,则尝试连接重置,失败并导致抛出某些IOException
如果您在协议中实际使用OOB数据,那么您的里程可能会有所不同。
答案 5 :(得分:4)
这是一个有趣的话题。我刚刚通过java代码进行检查。根据我的发现,有两个不同的问题:第一个是TCP RFC本身,它允许远程关闭套接字以半双工方式传输数据,因此远程关闭的套接字仍然是半开放的。根据RFC,RST不会关闭连接,您需要发送一个显式的ABORT命令;所以Java允许通过半封闭的套接字发送数据
(有两种方法可以在两个端点读取关闭状态。)
另一个问题是实现说这种行为是可选的。随着Java努力实现可移植性,他们实现了最佳的通用功能。我想,维护一张(OS,实现半双工)的地图本来就是个问题。
答案 6 :(得分:4)
Java IO堆栈在突然拆除时会被强制发送FIN。没有意义的是你无法检测到这一点,b / c大多数客户只有在关闭连接时才发送FIN。
...我真的开始讨厌NIO Java类的另一个原因。似乎一切都有点半屁股。
答案 7 :(得分: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 */
}
答案 8 :(得分:2)
此行为(不是特定于Java)的原因是您没有从TCP堆栈获取任何状态信息。毕竟,套接字只是另一个文件句柄,你无法确定是否有实际数据要从中读取而没有实际尝试(select(2)
在那里没有帮助,它只表示你可以尝试不用阻塞)。
有关详细信息,请参阅Unix socket FAQ。
答案 9 :(得分:2)
这是一个蹩脚的解决方法。使用SSL;)和SSL在拆解时进行紧密握手,因此您会收到关闭套接字的通知(大多数实现似乎都在进行属性握手拆解)。
答案 10 :(得分:0)
只有写入才需要交换数据包,这样才能确定连接丢失。常见的解决方法是使用KEEP ALIVE选项。
答案 11 :(得分:-3)
在处理半开放Java套接字时,可能需要查看 isInputShutdown()和isOutputShutdown()。