Here are simplified versions of my socket server and client components.
The primary goal is for the client to detect when the server goes down and for the server to detect when the client goes down.
This works perfectly (on Windows) when either the client or the server are killed (getting IOException "An existing connection was forcibly closed by the remote host").
I would also like to detect when the machine where the client or server is running goes to sleep (or hibernates), eventually using the same mechanism.
Instead, the current behavior is that "the other machine going to sleep" event is not detected, and when the machine is woken up the connection is live again. At this time "the process going down" event is detected as before.
In the case where the client machine goes to sleep, the culprit seems to be "selector.selectedKeys()" not returning a key for the connection to the sleeping machine.
Is this functionality missing in the socket implementation on Windows?
Does anybody have any suggestion on how to fix / go around this issue?
package test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class TestServer {
private ByteBuffer _inBuf;
private int _serverPort;
public static void main(String[] args) {
TestServer server = new TestServer(7071);
server.start();
}
public TestServer(int serverPort) {
_serverPort = serverPort;
}
public void start() {
_inBuf = ByteBuffer.allocate(512);
System.out.println("Server starting on port "+_serverPort);
new Thread() {
public void run() {
try {
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(_serverPort));
server.configureBlocking(false);
SelectionKey serverKey = server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
for (Iterator<SelectionKey> i = keys.iterator(); i.hasNext(); ) {
SelectionKey key = i.next();
i.remove();
if (key == serverKey) {
if (key.isAcceptable()) {
System.out.println("acceptable server key "+Integer.toHexString(key.hashCode()));
try {
SocketChannel client = server.accept();
client.configureBlocking(false);
SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ);
System.out.println("registered client key "+Integer.toHexString(clientKey.hashCode()));
} catch (IOException x) {
x.printStackTrace();
}
}
} else {
if (!key.isReadable()) continue;
SocketChannel client = (SocketChannel) key.channel();
System.out.println("reading "+Integer.toHexString(key.hashCode()));
try {
int no = client.read(_inBuf);
if (no<0) throw new IOException("reached end-of-stream"+Integer.toHexString(key.hashCode()));
if (no>0) System.out.println("read "+no+" bytes from "+Integer.toHexString(key.hashCode()));
} catch (IOException x) {
System.out.println(x.getMessage()+" "+Integer.toHexString(key.hashCode()));
key.cancel();
try {
client.close();
} catch (IOException ignore) {
ignore.printStackTrace();
}
continue;
}
_inBuf.flip();
_inBuf.compact();
}
}
}
} catch (Exception x) {
x.printStackTrace();
}
}
}.start();
}
}
and
package test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class TestClient {
private static final int _connectionTimeoutNanos = 10 * 1000000;
private String _serverHost;
private int _serverPort;
private SocketChannel _channel = null;
private ByteBuffer _inBuf;
public static void main(String[] args) {
TestClient client = new TestClient("192.168.1.180", 7071);
client.start();
}
public TestClient(String serverHost, int serverPort) {
_serverHost = serverHost;
_serverPort = serverPort;
}
public void start() {
_inBuf = ByteBuffer.allocate(512);
ClientThread thread = new ClientThread();
thread.start();
}
private class ClientThread extends Thread {
@Override
public void run() {
System.out.println("Client connecting to "+_serverHost+":"+_serverPort);
SocketAddress socketAddress = new InetSocketAddress(_serverHost, _serverPort);
while (true) {
boolean connected = false;
try {
_channel = SocketChannel.open();
_channel.configureBlocking(false);
try {
connected = _channel.connect(socketAddress);
} catch (IOException x) {
try {
_channel.close();
} catch (Throwable suppressed) {
x.addSuppressed(suppressed);
}
throw x;
}
long nanoStart = System.nanoTime();
while (!connected) {
connected = _channel.finishConnect();
if (!connected && (nanoStart+_connectionTimeoutNanos < System.nanoTime())) {
throw new IOException("Non blocking connect failed");
}
}
_channel.socket().setSoLinger(true, 10);
System.out.println("Connected to "+_serverHost+":"+_serverPort);
while (true) {
if (!readFromChannel()) break;
}
System.out.println("Disconnected from "+_serverHost+":"+_serverPort);
} catch (IOException x) {
if (connected) {
System.out.println("Disconnected from "+_serverHost+":"+_serverPort+" "+x.getMessage());
}
}
try {Thread.sleep(1000);} catch (InterruptedException x) {}
}
}
}
public boolean readFromChannel() throws IOException {
int no = _channel.read(_inBuf);
if (no<0) {
return false;
}
if (no>0) System.out.println("read "+no+" bytes from "+_serverHost+":"+_serverPort);
_inBuf.flip();
_inBuf.compact();
return true;
}
}
This behavior differs from system to system and even its configuration. Old versions of Windows used to shut down all the pending connections when computer became sleeping and even when temporarily lost network connectivity. This is often not what the user wanted, because in case of just temporary outages the user had to reopen all the connections again. So it had changed some time ago and now (by default, it's configurable) it behaves similarly to other systems (Linux, MacOs, ...). So the connection is kept until it timeouts.
To avoid long living dead connections the best option is to set SO_KEEPALIVE option on the socket. Both sides and their operating systems will then send dummy packets over the socket (not payload data so not visible to application layer) and unless receiving response in reasonable time, OS will kill the connection. In Java you can achieve this like following: