Java socket API:如何判断连接是否已关闭?

116

我在使用Java套接字API时遇到了一些问题。我正在尝试显示当前连接到我的游戏的玩家数。当一个玩家连接时,很容易确定。然而,使用套接字API确定玩家何时断开连接似乎是不必要的困难。

调用已经从远程断开连接的套接字上的isConnected()总是返回true。同样,调用已经关闭远程套接字上的isClosed()总是返回false。我已经了解到实际确定套接字是否已关闭,必须向输出流写入数据并捕获异常。这似乎是一种非常不干净的处理方式。我们只需不断地向网络发送垃圾消息,才能知道套接字何时已关闭。

还有其他解决方案吗?

9个回答

216

没有TCP API可以告诉你连接的当前状态。 isConnected()isClosed()告诉您当前套接字的状态,而不是同一件事。

  1. isConnected()告诉您是否已连接此套接字。您已经连接,因此返回true。

  2. isClosed()告诉您是否已关闭此套接字。除非您关闭了它,否则它将返回false。

  3. 如果对等方以有序方式关闭了连接

    • read()返回-1
    • readLine()返回null
    • readXXX()会为任何其他XXX抛出EOFException

    • 写入将抛出IOException:“对等方重置了连接”,最终取决于缓冲延迟。

  4. 如果由于任何其他原因导致连接中断,则写入最终将像上述那样抛出IOException,读取可能会发生相同的情况。

  5. 如果对等方仍然连接但未使用连接,则可以使用读取超时。

  6. 与您可能在其他地方阅读的相反,ClosedChannelException不会告诉您这一点。[SocketException:socket closed.也不是如此]它只告诉您关闭了通道,然后继续使用它。换句话说,这是您的编程错误。它不表示已关闭的连接。

  7. 由于在Windows XP上进行了一些针对Java 7的实验,因此还出现了以下情况:

    • 您正在选择OP_READ
    • select()返回大于零的值
    • 相关的SelectionKey已经失效 (key.isValid() == false)

    这意味着对等方已经重置了连接。但是这可能与JRE版本或平台有关。


26
很难相信,面向连接的TCP协议甚至都不能知道它的连接状态...那些设计这些协议的人是不是闭着眼睛开车呢? - PedroD
41
相反,这是有意为之的。以前的协议套件(如SNA)有“拨号音”。TCP的设计旨在在核战争以及路由器的起落(更琐碎的是)中存活下来,因此完全没有任何类似于拨号音、连接状态等的东西;这也是为什么RFC中将TCP keepalive描述为有争议的特性,并且为什么它总是默认关闭。TCP仍然存在。SNA?IPX?ISO?不存在了。他们做对了。 - user207421
5
我不认为这是隐瞒这些信息的好借口。知道连接丢失并不一定意味着协议的容错能力更差,它始终取决于我们如何处理这些知识...... 对我来说,Java中的isBound和isConnected方法是纯粹的模拟方法,没有任何用处,但表达了对连接事件监听器的需求...... 但我要重申:知道连接已经丢失并不会使协议变得更糟。现在,如果你所说的那些协议一旦检测到连接丢失就断开连接,那就是另一回事了。 - PedroD
29
@Pedro,你没有理解。这不是“借口”。没有信息可以隐瞒。没有拨号音。TCP无法知道连接是否已经失败,直到你尝试对其进行操作。那是基本的设计标准。 - user207421
2
@user963241 当然应该。它是可关闭的:关闭它。 - user207421
显示剩余15条评论

13

在各种消息协议中,通常都会保持心跳(发送ping数据包),数据包不需要很大。探测机制将使您能够在TCP通常还没有发现时检测到已断开的客户端(TCP超时时间更长)。发送一个探测并等待5秒钟以获取回复,如果您连续2-3次未看到回复,则您的播放器已断开连接。

此外,相关问题


6
某些协议中通常会这样做。我注意到 HTTP,地球上使用最广泛的应用层协议,并没有PING操作。 - user207421
3
因为HTTP/1是一次性协议。你发送请求,接收响应。然后就完成了,套接字关闭了。Websocket扩展有PING操作,HTTP/2也有。 - Michał Zabielski
如果可能的话,我建议使用单个字节进行心跳检测 :) - Stefan Reich
@MichałZabielski 这是因为HTTP的设计者没有指定它。你不知道为什么。HTTP/1.1不是一次性协议。套接字没有关闭:HTTP连接默认是持久的。FTP、SMTP、POP3、IMAP、TLS等协议都没有心跳。 - user207421
2
心跳和ping不是两回事吗?当你发送ping时,你期望得到一个pong,但是对于心跳,你并不期望得到任何东西。 - DFSFOT

2

我看到了另一个答案,但是我认为你与玩家互动时,可以采用另一种方法(虽然BufferedReader在某些情况下确实有效)。

如果你想的话,你可以将“注册”责任委托给客户端。也就是说,你将拥有一个已连接用户的集合,并记录每个用户上次接收到消息的时间戳...如果客户端超时,你将强制重新注册该客户端,但这会导致以下引语和想法。

我读过一篇文章,它说要实际确定套接字是否关闭,必须向输出流写入数据并捕获异常。这似乎是一种处理此情况的非常不干净的方式。

如果您的Java代码没有关闭/断开Socket连接,那么您还能以其他方式被通知远程主机关闭了您的连接吗?最终,您的try/catch做的事情与监听实际套接字事件的轮询器大致相同。考虑以下内容:

  • 您的本地系统可能会关闭您的套接字而不通知您...这只是Socket的实现方式(即它不会轮询硬件/驱动程序/固件/任何状态变化)。
  • new Socket(Proxy p)...有多个方(实际上有6个端点)可能会关闭与您的连接...

我认为抽象语言的一个特点是,你可以从细节中抽象出来。想想C#(try / finally)中的using关键字,用于SqlConnection s或其他内容...这只是做生意的成本...我认为try / catch / finally是Socket使用的被接受和必要的模式。


1
您的本地系统无法“关闭套接字”,无论是否通知您。您提出的问题:“如果您的Java代码没有关闭/断开套接字……?”也没有任何意义。 - user207421
1
系统(例如Linux)到底是什么阻止了强制关闭套接字?是否仍然可以使用“gdb”命令中的“call close”命令来执行此操作? - Andrey Lebedenko
1
@AndreyLebedenko 没有什么“确切地阻止它”,但它并没有这样做。 - user207421
1
@AndreyLebedenko 没有其他人可以这样做。只有应用程序可以关闭自己的套接字,除非它在退出时没有这样做,在这种情况下,操作系统将进行清理。 - user207421
1
@AndreyLebedenko 我会描述为应用程序在调试器的指导下关闭了自己的套接字。 - user207421
显示剩余2条评论

2

我遇到了类似的问题。在我的情况下,客户端必须定期发送数据。我希望你也有同样的需求。然后我设置了SO_TIMEOUT socket.setSoTimeout(1000 * 60 * 5);,当指定时间过期时会抛出java.net.SocketTimeoutException异常。这样我就可以轻松检测到死亡的客户端。


1

我认为这是TCP连接的本质,根据标准,在传输中静默约6分钟后,我们才得出连接已断开的结论!因此,我认为您无法找到此问题的确切解决方案。也许更好的方法是编写一些便捷的代码来猜测服务器何时应该假定用户连接已关闭。


2
可能需要更长时间,长达两个小时。 - user207421

1
正如@user207421所说,由于TCP/IP协议体系结构模型,无法知道连接的当前状态。因此,服务器必须在关闭连接之前通知您,或者您可以自行检查。这是一个简单的示例,演示了如何知道套接字已被服务器关闭:
sockAdr = new InetSocketAddress(SERVER_HOSTNAME, SERVER_PORT);
socket = new Socket();
timeout = 5000;
socket.connect(sockAdr, timeout);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream());
while ((data = reader.readLine())!=null) 
      log.e(TAG, "received -> " + data);
log.e(TAG, "Socket closed !");

1

这里有另一个适用于任何数据类型的通用解决方案。

int offset = 0;
byte[] buffer = new byte[8192];

try {
    do {
        int b = inputStream.read();

        if (b == -1)
           break;

        buffer[offset++] = (byte) b;

        //check offset with buffer length and reallocate array if needed
    } while (inputStream.available() > 0);
} catch (SocketException e) {
    //connection was lost
}

//process buffer

-2

这就是我处理它的方式

 while(true) {
        if((receiveMessage = receiveRead.readLine()) != null ) {  

        System.out.println("first message same :"+receiveMessage);
        System.out.println(receiveMessage);      

        }
        else if(receiveRead.readLine()==null)
        {

        System.out.println("Client has disconected: "+sock.isClosed()); 
        System.exit(1);
         }    } 

如果 result.code 为 null


1
你不需要调用 readLine() 两次。你已经知道在 else 块中它是 null 的。 - user207421

-3
在Linux中,当向一个你不知道对方已关闭的套接字进行write()操作时,会引发SIGPIPE信号/异常,无论你如何称呼它。但是,如果你不想被SIGPIPE捕获,可以使用带有MSG_NOSIGNAL标志的send()。send()调用将返回-1,在这种情况下,你可以检查errno,它会告诉你尝试使用值EPIPE(在这种情况下是套接字)写入破损的管道。根据errno.h,EPIPE等价于32。作为对EPIPE的反应,你可以回头尝试重新打开套接字并再次发送你的信息。

send() 调用仅在传出数据被缓冲足够长时间以使发送计时器过期时才会返回 -1。由于两端的缓冲和 send() 异步性质,第一次断开连接后发送几乎不可能发生。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接