作为最近一个问题的后续内容,我想知道为什么在没有尝试在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 ...
Matthieu.. 29
我一直在使用套接字,主要是使用选择器,虽然不是网络OSI专家,但根据我的理解,调用shutdownOutput()
Socket实际上会在网络上发送一些东西(FIN)唤醒另一边的Selector(C中的相同行为)语言).在这里你有检测:实际检测到你尝试时会失败的读操作.
在您提供的代码中,关闭套接字将关闭输入和输出流,而无法读取可能的数据,从而丢失它们.Java Socket.close()
方法执行"正常"断开连接(与我最初的想法相反),因为输出流中剩余的数据将被发送,然后是FIN以发出关闭信号.FIN将由另一方确认,因为任何常规数据包都是1.
如果你需要等待另一方关闭它的套接字,你需要等待它的FIN.而要达到这一点,你必须检测Socket.getInputStream().read() < 0
,这意味着你应该不会关闭套接字,因为它会关闭其InputStream
.
从我在C中所做的,现在在Java中,实现这样的同步关闭应该像这样:
关闭套接字输出(在另一端发送FIN,这是此套接字将发送的最后一件事).输入仍处于打开状态,因此您可以read()
检测遥控器close()
读取套接字InputStream
直到我们从另一端收到reply-FIN(因为它将检测FIN,它将经历相同的优雅的diconnection过程).这在某些操作系统上很重要,因为只要其中一个缓冲区仍包含数据,它们就不会实际关闭套接字.它们被称为"ghost"套接字,并在操作系统中使用描述符编号(现代操作系统可能不再是问题)
关闭套接字(通过调用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:来自" Java中的基础网络 ":见图.3.3 p.45,以及整个§3.7,第43-48页
我一直在使用套接字,主要是使用选择器,虽然不是网络OSI专家,但根据我的理解,调用shutdownOutput()
Socket实际上会在网络上发送一些东西(FIN)唤醒另一边的Selector(C中的相同行为)语言).在这里你有检测:实际检测到你尝试时会失败的读操作.
在您提供的代码中,关闭套接字将关闭输入和输出流,而无法读取可能的数据,从而丢失它们.Java Socket.close()
方法执行"正常"断开连接(与我最初的想法相反),因为输出流中剩余的数据将被发送,然后是FIN以发出关闭信号.FIN将由另一方确认,因为任何常规数据包都是1.
如果你需要等待另一方关闭它的套接字,你需要等待它的FIN.而要达到这一点,你必须检测Socket.getInputStream().read() < 0
,这意味着你应该不会关闭套接字,因为它会关闭其InputStream
.
从我在C中所做的,现在在Java中,实现这样的同步关闭应该像这样:
关闭套接字输出(在另一端发送FIN,这是此套接字将发送的最后一件事).输入仍处于打开状态,因此您可以read()
检测遥控器close()
读取套接字InputStream
直到我们从另一端收到reply-FIN(因为它将检测FIN,它将经历相同的优雅的diconnection过程).这在某些操作系统上很重要,因为只要其中一个缓冲区仍包含数据,它们就不会实际关闭套接字.它们被称为"ghost"套接字,并在操作系统中使用描述符编号(现代操作系统可能不再是问题)
关闭套接字(通过调用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:来自" Java中的基础网络 ":见图.3.3 p.45,以及整个§3.7,第43-48页
我认为这更像是一个套接字编程问题.Java只是遵循套接字编程传统.
来自维基百科:
TCP提供从一台计算机上的一个程序到另一台计算机上的另一个程序的可靠,有序的字节流传送.
握手完成后,TCP不会对两个端点(客户端和服务器)进行任何区分.术语"客户端"和"服务器"主要是为了方便起见.因此,"服务器"可以发送数据,"客户端"可以同时向对方发送一些其他数据.
"关闭"一词也具有误导性.只有FIN声明,这意味着"我不会再向你发送任何东西." 但这并不意味着没有飞行中的数据包,或者另一个没有更多的话要说.如果您将蜗牛邮件实现为数据链路层,或者您的数据包通过不同的路由,则接收器可能会以错误的顺序接收数据包.TCP知道如何为您解决此问题.
另外,作为程序,您可能没有时间继续检查缓冲区中的内容.因此,在您方便时,您可以检查缓冲区中的内容.总而言之,当前的套接字实现并不是那么糟糕.如果确实存在isPeerClosed(),则每次要调用read时都必须进行额外调用.
底层套接字API没有这样的通知.
发送TCP堆栈不会发送FIN位,直到最后一个数据包为止,因此在发送应用程序在发送数据之前逻辑上关闭其套接字时可能会缓冲大量数据.同样,因为网络比接收应用程序更快而缓冲的数据(我不知道,也许你通过较慢的连接中继它)对接收者来说可能是重要的,你不希望接收应用程序丢弃它只是因为堆栈已经收到了FIN位.
由于到目前为止没有一个答案完全回答这个问题,我总结了我目前对这个问题的理解.
当建立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提供任何额外的方法.
检测(TCP)套接字连接的远程端是否已关闭可以使用java.net.Socket.sendUrgentData(int)方法完成,如果远程端关闭,则捕获它抛出的IOException.这已经在Java-Java和Java-C之间进行了测试.
这避免了设计通信协议以使用某种ping机制的问题.通过在套接字上禁用OOBInline(setOOBInline(false),接收的任何OOB数据都将被静默丢弃,但仍可以发送OOB数据.如果远程端关闭,则尝试连接重置,失败并导致抛出某些IOException .
如果您在协议中实际使用OOB数据,那么您的里程可能会有所不同.