How do you close a AsynchronousSocketChannel cleanly?
Asked Answered
T

2

11

My server uses a AsynchronousServerSocketChannel that listens for client connections using a CompletionHandler. When a client connection is accepted, the AsynchronousSocketChannel is read, again using a CompletionHandler to receive the data with no timeout.

So far so good, my client connects, writes data that is read by the server, which is able to respond sending data back to the client via the same socket.

When my client terminates, it calls AsynchronousSocketChannel.close(), to close the socket. When this call is made the server is waiting to read data from the socket.

I had expected the call to AsynchronousSocketChannel.close() on the client to translate into a callback to CompletionHandler.completed with a read length of -1 on the server, indicating the socket had been closed, however the callback is to CompletionHandler.failed with the following exception:

java.io.IOException: The specified network name is no longer available.
  at sun.nio.ch.Iocp.translateErrorToIOException(Iocp.java:309)
  at sun.nio.ch.Iocp.access$700(Iocp.java:46)
  at sun.nio.ch.Iocp$EventHandlerTask.run(Iocp.java:399)
  at sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  at java.lang.Thread.run(Thread.java:744)    

How should a client close a socket, so that it is not seen as an error on the server?

Thorbert answered 23/12, 2013 at 15:4 Comment(0)
S
3

The documentation on close says that it causes AsynchronousCloseException or ClosedChannelException on the other side.

To cause completed(-1) the client should call shutdownInput.

However, I would treat AsynchronousCloseException and ClosedChannelException as normal shutdown, along with completed(-1).

Sperry answered 23/12, 2013 at 19:5 Comment(10)
It doesn't say anything about 'the other side'. Both those exceptions should be treated as programming errors, not 'normal shutdown'.Demisemiquaver
I've added in the call to AsynchronousSocketChannel.shutdownInput on the client-side, which does indeed cause a callback to CompletionHandler.completed with a read length of -1 on the server.Thorbert
It's a shame the client-side gets a AsynchronousCloseException though at least it's not the very general IOException and because the client initiated the close, I can add a closing flag in order to ignore this error.Thorbert
@EJP - do you know of a way to avoid the AsynchronousCloseException altogether?Thorbert
I was stumped by this behavior as well... Why would you throw an exception if a client closes the connection using a valid method? I want to know. That's like your car alarm triggering every time you close the car door...Abed
The thing is, the exception is translated to java.io.IOException... Which is kind of ambiguous. This has "not well thought out" stink all over it... How the hell am I supposed to know how bad this io Exception is? Do i need to wrap it in extra logic because it was sent to failed method in the handler? Makes me feel dirty just thinking about it and not in a good way.Abed
In my code, I have actually identified several different ways we're told about closed channels. 1) Trying to initiate operation on a closed channel immediately completes with ClosedChannelException. 2) The specialized subclass AsynchronousCloseException is when a pending operation aborts. 3) Trying to initiate when the associated group is closed will get you a ShutdownChannelGroupException. 4) Any IOException which has a cause set to a ShutdownChannelGroupException is a rare breed but means the operation initialization was accepted but failed afterwards because the group shutdown.Trachytic
What's the alternative? I don't see one. How can a close "normally" complete a channel operation? The task of driving a car either gets you there or an exception happened. We could have more result sentinel values than just "-1" but this is a bit smelly and I think will lead down the rabbit hole. It's probably not very coherent with many other APIs in the runtime either. Also, using exceptions we have only one unified way of dealing with failures, and we can even learn about new future scenarios/reasons in the future why a channel was closed without being limited to sentinel values alone.Trachytic
Anyways, in my case the "other side" was a Java client which only sometimes threw a "java.net.SocketException: Connection reset" on read(). I figured as much the server was terminating the connection badly before the client was done. Anyways, solved the problem by changing the server's close procedure from doing only close() to shutdownInput() + shutdownOutput() + close(). Thank you for sharing guys! =)Trachytic
Adding shutdownInput() and shutdownOutput() won't have fixed it. It doesn't change anything if the close follows immediately. You must have done something else as well.Demisemiquaver
N
1

Looking at stack traces and the implementation sources, you might notice that the exception is thrown by the internal sun.nio.ch.UnixAsynchronousSocketChannelImpl#finish() method which checks for pending read/write operations. So the only way to avoid this exception is to prevent new asynchronous read() and write() calls at some point, and that should be a part of the application's logic.

I've dealt with this exception quite a lot, and in most cases the root problem was in the completion handler's unconditional "continue listening" calls:

conn.read(src, attachment, new CompletionHandler<Integer, T>() {
    @Override
    public void completed(Integer result, T attachment) {
        // Some business logic here
        
        // Below is the problem - unconditional read() call
        conn.read(src, attachment, this);
    }

    @Override
    public void failed(Throwable t, T attachment) {
        // Exception handling
    }
});

To gracefully close the connection, there should be no unconditional async read/write calls. To achieve that, one might need to send an additional message which would mean that no new async data is expected and it's safe to close the connection. And the correct pseudo-code would look something like this:

conn.read(src, attachment, new CompletionHandler<Integer, T>() {
    @Override
    public void completed(Integer result, T attachment) {
        // Some business logic here

        if(continueListening()) {
            conn.read(src, attachment, this);
        }
    }
    
    // ... 

});
Newborn answered 20/4, 2022 at 20:45 Comment(2)
No need to call close()?Asper
@JinKwon the idea is to break the handler's recursive reads before closing the connection, thus avoiding the AsynchronousCloseException. That's of course if your app's logic allows for it. If not, one can close the connection and handle the exception in the failed blockNewborn

© 2022 - 2024 — McMap. All rights reserved.