当前位置:  开发笔记 > 编程语言 > 正文

为什么在没有尝试I/O的情况下检测TCP套接字是否被对等端正常关闭是不可能的?

如何解决《为什么在没有尝试I/O的情况下检测TCP套接字是否被对等端正常关闭是不可能的?》经验,为你挑选了5个好方法。

作为最近一个问题的后续内容,我想知道为什么在没有尝试在TCP套接字上进行读/写操作的情况下,为什么Java不可能检测到套接字已被对等端正常关闭?无论是使用前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
...

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()或关闭它InputStreamOutputStream)

如以下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页



1> Matthieu..:

我一直在使用套接字,主要是使用选择器,虽然不是网络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()或关闭它InputStreamOutputStream)

如以下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页


@EJP,"优雅断开连接"是在TCP级别发生的特定交换,客户端应发出信号断开与服务器的连接,然后服务器在关闭其端之前发送剩余数据."发送剩余数据"部分必须由程序处理(尽管人们不会在大多数时间发送任何内容).调用`socket.close()`的方式是"残酷的",因为它不尊重这个客户机/服务器信令.仅当客户端的套接字输出缓冲区已满时,才会通知服务器客户端断开连接(因为没有数据被另一端关闭,而该数据已被关闭).
@LeonidUsov这简直是错误的.Java`read()`在流结束时返回-1,并且继续执行此操作,但是多次调用它.AC`read()`或`recv()`在流结束时返回零,并且继续这样做,但是多次调用它.

2> Eugene Yokot..:

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

来自维基百科:

TCP提供从一台计算机上的一个程序到另一台计算机上的另一个程序的可靠,有序的字节流传送.

握手完成后,TCP不会对两个端点(客户端和服务器)进行任何区分.术语"客户端"和"服务器"主要是为了方便起见.因此,"服务器"可以发送数据,"客户端"可以同时向对方发送一些其他数据.

"关闭"一词也具有误导性.只有FIN声明,这意味着"我不会再向你发送任何东西." 但这并不意味着没有飞行中的数据包,或者另一个没有更多的话要说.如果您将蜗牛邮件实现为数据链路层,或者您的数据包通过不同的路由,则接收器可能会以错误的顺序接收数据包.TCP知道如何为您解决此问题.

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



3> Mike Dimmick..:

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

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



4> Alexander..:

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

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


Nor*can*Java可以提供任何额外的方法.也许他们可以创建一个isCloseWait()方法,如果平台不支持它会返回false,但如果他们只在受支持的平台上测试过多少程序员会被这个问题所困扰?

5> 小智..:

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

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

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

推荐阅读
mobiledu2402851203
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有