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