By default in Java, the method accept()
of ServerSocketChannel
is blocking: it blocks
the execution of the thread until a connection can be accepted.
The methods read() and
write() of the class SocketChannel
have the same behaviour.
ServerSocketChannel and SocketChannel can be configured for accept(), read() and write() to be non-blocking.
socket.configureBlocking(false);
In non-blocking mode, the methods
accept
,read
and write
always
terminate immediately.
accept
returns null
if there is no pending connection.
read
returns 0
if no data is ready to be read.
write
no longer guaranties that the entire work-zone of
the buffer will be written: it can write part of it or even nothing.
How to accept connexion on a ServerSocketChannel ssc configured in non-blocking mode ?
ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.bind(new InetSocketAddress(7777)); ssc.configureBlocking(false);
Bad idea: perform an unconditionnal accept() ?
SocketChannel sc = ssc.accept(); // NON: sc may be null
Bad idea: accept while sc is null ?
SocketChannel sc; do { sc=ssc.accept(); } while(sc==null); // NO: Active waiting !
Good idea: use a selector to be notified of a pending connection
Mechanism used to monitor several types of channels for different types of operations. In particular, we can be notified of:
Therefore, for a non-blocking TCP server, the ServerSocketChannel and all the SocketChannel of the clients, can be monitored by the same selector.
At the start, there is only one ServerSocketChannel to monitor
Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.bind(new InetSocketAddress(7777)); ssc.configureBlocking(false); // register to selector for accepts SelectionKey sKey = ssc.register(selector,SelectionKey.OP_ACCEPT);
Registration is done via the register() method
on the channel with the actions we want to monitor:
OP_ACCEPT indicates that we want to be notified of pending connections.
It is the only operation available on ServerSocketChannels.
Once a client has connected to the server, we need to accept it and to register the SocketChannel with the selector.
SocketChannel sc = ssc.accept(); if (sc == null) { // We need to check if the accept has worked // remember that the selector only gives an hint } else { sc.configureBlocking(false); SelectionKey cKey = sc.register(selector,SelectionKey.OP_READ); }
A client SocketChannel is registered to the same selector, with the operation we want to monitor:
OP_READ to be notified when data can be read,
OP_WRITE to be
notified when data can be written
OP_READ | OP_WRITE to be notified in both cases.
Selectors use SelectionKeys to represent channels, either the ServerSocketChannel or the SocketChannel of the clients. The keys contain 3 pieces of information:
Idea: replace the different blocking calls to accept(), read() or write() on the various channels by one call to select() on the selector and then successively treat the keys for which an operation is available.
The selector only provides a hint, so we need to check that the operation was successful.
SetselectedKeys = selector.selectedKeys(); while (!Thread.interrupted()) { selector.select(); processSelectedKeys(); selectedKeys.clear(); }
processSelectedKeys
for (SelectionKey key : selectedKeys) { if (key.isValid() && key.isAcceptable()) { doAccept(key); } if (key.isValid() && key.isWritable()) { doWrite(key); } if (key.isValid() && key.isReadable()) { doRead(key); } }
Be carefull about the exceptions.
What does it mean to have an IOException in the doAccept()?
And in doWrite() or doRead()?
while (!Thread.interrupted()) { selector.select(this::treatKey); }where
void treatKey(SelectionKey key) { if (key.isValid() && key.isAcceptable()) { doAccept(key); } if (key.isValid() && key.isWritable()) { doWrite(key); } if (key.isValid() && key.isReadable()) { doRead(key); } }
Careful with the exceptions ...
private void doAccept(SelectionKey key) throws IOException { // only the ServerSocketChannel is registered in OP_ACCEPT ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel sc = ssc.accept(); if (sc==null) return; // the selector gave a bad hint sc.configureBlocking(false); sc.register(selector,SelectionKey.OP_READ); }
We can attach an object to the key of a channel (i.e., an attachement), this can be done during registration:
sc.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(SIZE));
In a blocking server, we had at least one thread per active connexion. In a non-blocking server, there is only one thread handling all connections.
It is possible to handle more connections at the price of a possibly diminished response time.