One of the difficulties we encountered when writing UDP clients is that, by default, the methods datagramChannel.receive()
and datagramChannel.send()
are blocking.
Recall that to handle the fact we might be blocked in a call to datagramChannel.receive()
(in case of a lost packet), we had to use several
threads.
It is possible to change this default behaviour and to render the two methods
send()
and receive()
non-blocking.
DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false);
In non-blocking mode, a call datagramChannel.receive()
returns immediately even if no packet has been received.
buff
and the sender's address is returned.null
.DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); InetSocketAddress exp = dc.receive(buff); if (exp!=null){ System.out.println("Packet received !"); } else { System.out.println("No packet received"); }
In non-blocking mode, datagramChannel.send()
returns immediately, even if it is not possible to send the packet at the moment because the system's send-buffer is full.
buff
work-zone is consumed and the packet is sent to the system send-buffer;buff
is not modified.DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); ... dc.send(buff,dest); if (!buff.hasRemaining()){ System.out.println("Packet sent!"); } else { System.out.println("No packet sent."); }
How can we wait to receive data in non-blocking mode ?
ByteBuffer buff = ByteBuffer.allocate(BUFFER_SIZE); DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); InetSocketAddress exp = null; while(exp==null) { // Argh : active waiting !!! exp=dc.receive(buff); }Absolutely forbidden !!! It is active waiting.
To avoid active waiting, we need to use an external mecanism called a selector to get notified when a packet is received or when a packet can be sent.
Selectors are mechanism to be notified when a packet is received or when a packet can be sent.
A selector
monitors a certain number of DatagramChannel
which are registered to it.
For each DatagramChannel
registered,
we can ask to be notified of the reception of a packet and/or of the possibility to send a packet.
When asked, selector will provide us with all DatagramChannel
on which at least one operation is available.
A Selector is built with the factory method Selector.open()
.
Selector selector = Selector.open();
A Selector uses the class
SelectionKey
to store the data related to the
DatagramChannel
it is monitoring.
A Selector maintains two sets of SelectionKey
:
selector.keys()
selector.selectedKeys()
A SelectionKey contains:
Channel
corresponding to the DatagramChannel
registered with that key, accessible through the getter selectionKey.channel()
.int
which encodes the operations we want to monitor (getter selectionKey.interestOps()
):
SelectionKey.OP_READ
for receving only,SelectionKey.OP_WRITE
for sending only,SelectionKey.OP_READ|SelectionKey.OP_WRITE
to be notified for both sending AND receivingWe can modify this value using selectionKey.interestOps(int interestOps)
.
To register a DatagramChannel
in the Selector
, the
Selector selector = Selector.open(); DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); // registration to the selector asking to be notify when packet are received dc.register(selector, SelectionKey.OP_READ);
After the call dc.register(selector,SelectionKey.OP_READ)
, the selector creates a SelectionKey
which is added to the set of monitored keys selector.keys()
, so that the selector notifies us when a packet is received on dc
.
Once one or more DatagramChannel
have been
registered to a selector, with their interest operations (and hence represented in selector.keys()
), we only need to ask selector with selector.select().
We repeat in a loop:
selector.select()
which places, in
selector.selectedKeys()
, the keys of the DatagramChannel
for which an operation of interest is available,selector.selectedKeys()
,
selector.selectedKeys()
.Set<SelectionKey> selectedKeys = selector.selectedKeys(); while (!Thread.interrupted()) { selector.select(); for (var key : selectedKeys) { if (key.isValid() && key.isWritable()) { doWrite(key); } if (key.isValid() && key.isReadable()) { doRead(key); } } selectedKeys.clear(); }
key.isValid() && key.isWritable()
returns true
if the corresponding DatagramChannel
is ready to send a packet.
key.isValid() && key.isReadable()
returns true
if a packet is ready to be received on the corresponding DatagramChannel
.
Warning: even with a hint from the selector, there is no guarantee that a call to datagramChannel.send()
or datagramChannel.receive()
will be successful.
In other words, it is written in the Javadoc that a Selector
is allowed to be wrong.
Since Java 11, Selector offers an version of select using a lambda.
Selector.select(Consumer<SelectionKey> action)
The Consumer action
is applied to each selected key. There is no need to deal with the selectedKeys set
This method is possibly more efficient than performing the selection loop by hand.
However because it uses lambdas, the handling of checked exceptions like IOException
is less natural.
while (!Thread.interrupted()) { selector.select(this::treatKey); } private void treatKey(SelectionKey key) { try{ if (key.isValid() && key.isWritable()) { doWrite(key); } if (key.isValid() && key.isReadable()) { doRead(key); } } catch (IOException e) { // TODO : ???? } }
How to handle the IOException
?
We have to think about what an IOException
can signal.
As there is no thread involved (this is one of the big advantages of non-blocking IO),
the exceptions AsynchronousCloseException
and ClosedByInterruptException
cannot occur.
So in the case of a UDP client/server, the IOException
can only signal a real IO problem. We have no choice but to close the program (and may be log the exception as SEVERE).
A common mistake :
public void launch() throws IOException { while(!Thread.interrupted()) { selector.select(this::treatKey); } } private void treatKey(SelectionKey key) { try { if (key.isValid() && key.isWritable()) { ... } if (key.isValid() && key.isReadable()) { ... } } catch (IOException ioe) { logger.log(Level.SEVERE,"Severe IO problem ",ioe); } }
The program does not stop even though a irrecoverable problem has occured.
public void launch() throws IOException { while(!Thread.interrupted()) { try { selector.select(this::treatKey); } catch (UncheckedIOException tunneled) { throw tunneled.getCause(); } } } private void treatKey(SelectionKey key) { try { if (key.isValid() && key.isWritable()) { ... } if (key.isValid() && key.isReadable()) { ... } } catch (IOException ioe) { throw new UncheckedIOException(ioe); } }