Non-blocking UDP

Non-blocking UDP

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

Non-blocking receive

In non-blocking mode, a call datagramChannel.receive() returns immediately even if no packet has been received.

  • if a packet is present in the system's receive buffer, the payload (i.e., the data) is copied to the work-zone of buff and the sender's address is returned.
  • if no packet has been received by the system, the buffer is not modified and the method returns 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");
}

Non-blocking send

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.

  • if the packet can be sent immediately, all the data from buff work-zone is consumed and the packet is sent to the system send-buffer;
  • if the packet cannot be sent immediately, no packet is sent and the 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.");
}

Simulating a blocking receive ?

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 (1/2)

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.

Selectors (2/2)

In Java: java.nio.channels.Selector

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:

  • the set of keys corresponding to monitored channels selector.keys()
  • the set of selected keys on which some monitored event occured selector.selectedKeys()

Selector (1/2)

Selector (2/2)

SelectionKey

A SelectionKey contains:

  • a Channel corresponding to the DatagramChannel registered with that key, accessible through the getter selectionKey.channel().
  • an 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 receiving

    We can modify this value using selectionKey.interestOps(int interestOps).

Registering a channel to a selector

To register a DatagramChannel in the Selector, the DatagramChannel must be configured in non-blocking mode.

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.

Selection loop (1/2)

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:

  • a call to selector.select() which places, in selector.selectedKeys(), the keys of the DatagramChannel for which an operation of interest is available,
  • we treat each key of selector.selectedKeys(),
  • we empty the set selector.selectedKeys().

Selection loop (2/2)

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

Selector hints

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.

The new selection loop

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.

New selection loop

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 ?

Handling the exceptions (1/3)

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).

Handling exceptions (2/3)

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.

Handling exceptions (3/3)

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