java nio socket not detecting when machine goes to sleep or hibernates

968 views Asked by At

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;
    }
}
2

There are 2 answers

2
Zbynek Vyskovsky - kvr000 On BEST ANSWER

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:

channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
0
puiuvlad On

Thanks Zbynek, that solved the issue :-).

Here is what I had to do:

1) in the TestServer code, after client.configureBlocking(false) at line 50 I added:

client.socket().setKeepAlive(true);

which is equivalent to your

client.setOption(StandardSocketOptions.SO_KEEPALIVE, true);

2) in the TestClient code, after line 60:

_channel.socket().setSoLinger(true, 10);

I added:

_channel.socket().setKeepAlive(true);

3) using regedit, on both Windows machines, I added the following value under

HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/Tcpip/Parameters/

KeepAliveTime REG_DWORD 1000

Its default value is 2 hours and I reduced it to 1 second.

I left the KeepAliveInterval at its default value of 1 second and TcpMaxDataRetransmissions at its default value of 5.

Like with any Microsoft software, I had to restart both machines.

Note that one of my machines is Win10 and the other is Win7.

With these changes, whichever machine goes to sleep, the component on the other machine detects the disconnect event (within 5 seconds). As soon as the machine wakes up, the component on it detects that the connection is no longer there and then re-connects fresh. Exactly what I was trying to accomplish.

Thanks again, Vladimir