NIO

Last modified by Sebastian Marsching on 2022/05/27 22:36

Non-blocking UDP socket

In Java, a non-blocking UDP socket can be created with

DatagramChannel channel = DatagramChannel.open();
channel.bind(null);
channel.configureBlocking(false);

This channel can then be registered with a selector in order to be informed when it is ready for a write or read operation. For example:

Selector selector = Selector.open();
channel.register(selector, OP_READ, null);

selector.select();

for (Iterator<SelectionKey> selectionKeyIterator = selector
       .selectedKeys().iterator(); selectionKeyIterator.hasNext();) {
    SelectionKey selectionKey = selectionKeyIterator.next();
   // Do something with the channel.
   selectionKeyIterator.remove();
}

The big difference to blocking I/O is, that you can register multiple channels with the same selector and be informed when one of it is ready. The select operation can also have a timeout if you want to do other tasks in the same thread. An object can be attached to selection keys, so you can attach some handler object that is responsible for the channel.

Problems with non-blocking I/O and zero-size UDP packets

There is one severe bug in the NIO API and implementation (I really believe it is both) when it comes to handling empty (zero size) UDP packets. Those packets are not just a theoretical thing, but I have seen them as a part of real-world protocols.

When sending a UDP packet in non-blocking mode, the send method returns the number of bytes sent or zero, if the channel was not ready and thus the packet could not be sent.

public static void sendData(DatagramChannel channel, InetSocketAddress targetAddress, ByteBuffer data) throws IOException {
   if (channel.send(targetAddress, data) == 0) {
       // Try again later.
   }
}

However, if you want to send an empty packet (and thus your buffer is empty), the return value is zero, even if the packet was sent. So the bad news is that there is no way to check whether a zero-size packet has actually been sent when the channel is in non-blocking mode. The good news is, that the packet is still sent. I guess that if a select with OP_WRITE was successful, there is probably enough space in the sockets send buffer to send an empty packet, so usually it is a good guess to say that the send operation was successful.

When it comes to receiving empty UDP packets, however, the situation is worse: The return value of the receive method tells us whether we have received any data:

public static void receiveData(DatagramChannel channel) throws IOException {
    SocketAddress senderAddress = channel.receive(buffer);
   if (senderAddress != null) {
       // Process received data.
   } else {
       // No data received, try again later.
   }
}

So this looks pretty simple and in fact it works as exepected: For an empty UDP packet, senderAddress is not null but the buffer is empty. However, there is a problem with the select operation. A select operation with an interest set containing OP_READ will not return when an empty UDP packet is received. Instead, it will block, until the timeout is reached or a non-empty packet is received. At this point, it will also deliver the empty packet (or packets), but depending on your application, it might be too late.

If you have to react on empty UDP packets, there are only two solutions: Use a short timeout for the select operation and check whether the channel received data, even if its selection key has not been selected. With this approach you lose the biggest advantage of using non-blocking I/O, but it can be an alternative if you have to process a lot of different channels from the same thread and only expect empty UDP packets for a few of them. The other alternative is simple: Use blocking I/O. When using blocking I/O, the receive method blocks and returns when a packet is received, even if it is empty.