Index

Selectors and Non-blocking IO

Java IO and NIO

7.1 Understanding Selectors

Java NIO (New IO) revolutionized the way Java applications handle input/output by introducing non-blocking IO and scalable architectures. At the heart of this scalable non-blocking model lies the Selector, an object that allows a single thread to monitor multiple channels (e.g., SocketChannel, ServerSocketChannel) for events like read, write, and connect readiness.

This mechanism enables efficient resource usage and is the cornerstone for building high-performance servers and event-driven applications.

What is a Selector?

A Selector is a Java NIO component that can monitor multiple channels simultaneously and detect when one or more of them are ready for a certain type of IO operation (such as accepting a connection, reading, or writing). Instead of dedicating a thread per socket connection, a single thread can use a selector to manage hundreds or thousands of connections.

Selectors support the following channel types:

These channels must be in non-blocking mode to be used with a Selector.

Why Use Selectors?

Traditionally, handling multiple client connections meant using one thread per connection. This model does not scale well, especially in environments with many idle or slow connections. Selectors solve this problem by enabling multiplexed IO — a single thread checks multiple channels to see which ones are ready, then acts accordingly.

Real-world Analogy:

Imagine a security guard monitoring multiple doors (channels). Instead of standing at each door waiting (blocking), the guard walks through a hallway (selector) and checks which doors have visitors (read/write events). This is far more efficient than assigning a guard per door.

Key Concepts

Common Selection Operations

Basic Workflow

  1. Create a selector
  2. Configure channels to non-blocking mode
  3. Register channels with the selector
  4. Call select() to wait for readiness
  5. Process selected keys and perform IO
  6. Repeat the loop

Basic Code Example: Server Using Selector

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class SelectorExample {
    public static void main(String[] args) throws IOException {
        // Step 1: Create Selector
        Selector selector = Selector.open();

        // Step 2: Create ServerSocketChannel and bind port
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(5000));
        serverChannel.configureBlocking(false);

        // Step 3: Register ServerChannel with Selector for OP_ACCEPT
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Server started. Listening on port 5000...");

        // Step 4: Event loop
        while (true) {
            // Wait for events (blocking with timeout optional)
            selector.select();

            // Get keys for channels that are ready
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();

                // Step 5: Check what event occurred
                if (key.isAcceptable()) {
                    // Accept the new client connection
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("New client connected.");
                } else if (key.isReadable()) {
                    // Read data from client
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    int bytesRead = client.read(buffer);
                    if (bytesRead == -1) {
                        client.close();
                        System.out.println("Client disconnected.");
                    } else {
                        String msg = new String(buffer.array()).trim();
                        System.out.println("Received: " + msg);
                    }
                }

                // Remove processed key to avoid reprocessing
                iter.remove();
            }
        }
    }
}

Explanation of the Code:

Benefits of Using Selectors

Summary

Selectors enable event-driven IO models that are scalable, efficient, and well-suited for modern networked applications.

Index

7.2 Registering Channels with Selectors

One of the most powerful features of Java NIO is its support for non-blocking IO using selectors. Selectors allow a single thread to manage multiple IO channels efficiently. To use this mechanism, channels must first be registered with a Selector. This section explains how registration works, the meaning of selection operations like OP_READ and OP_WRITE, and how to configure interest sets to monitor specific IO events.

Selectable Channels in Java NIO

Not all channels in Java NIO are selectable. Only those that implement the SelectableChannel interface can be registered with a Selector. The key channel types that support this include:

All these channels must be configured to non-blocking mode before being registered with a selector.

What Happens During Registration?

When you register a channel with a selector, you’re asking the selector to watch that channel for specific events, such as when it's ready to read data or accept a new connection.

This is done using the channel’s register() method:

SelectionKey key = channel.register(selector, ops);

Selection Operations (Interest Ops)

When registering a channel, you specify the interest set — a bitmask that tells the selector which operations to monitor on the channel.

The selection operations include:

Constant Description
SelectionKey.OP_ACCEPT Channel is ready to accept a new connection (for ServerSocketChannel)
SelectionKey.OP_CONNECT Channel has completed connection process (for SocketChannel)
SelectionKey.OP_READ Channel is ready for reading data
SelectionKey.OP_WRITE Channel is ready for writing data

These constants are bit flags and can be combined using the bitwise OR (|) operator.

Example:

int ops = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
channel.register(selector, ops);

This tells the selector to notify us when the channel is either ready to read or write.

Configuring a Channel and Registering with a Selector

Before registration, the channel must be configured as non-blocking:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);

Then it can be registered:

Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);

Code Example: Registering Multiple Channels

Here’s a complete example showing how to register ServerSocketChannel and accept incoming connections, then register each new SocketChannel for reading:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class ChannelRegistrationExample {
    public static void main(String[] args) throws IOException {
        // Step 1: Create a selector
        Selector selector = Selector.open();

        // Step 2: Create a non-blocking ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(4000));
        serverChannel.configureBlocking(false);

        // Step 3: Register server channel with selector for ACCEPT operation
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server listening on port 4000...");

        // Step 4: Event loop
        while (true) {
            selector.select(); // Block until at least one channel is ready

            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();

                if (key.isAcceptable()) {
                    // Accept connection
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);

                    // Register client for READ operation
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("Accepted new client connection.");
                } else if (key.isReadable()) {
                    // Read data from client
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    int bytesRead = client.read(buffer);

                    if (bytesRead == -1) {
                        client.close(); // Client closed connection
                        System.out.println("Client disconnected.");
                    } else {
                        buffer.flip();
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        System.out.println("Received: " + new String(data));
                    }
                }

                iter.remove(); // Remove processed key
            }
        }
    }
}

Explanation of the Code

The SelectionKey Object

When a channel is registered, a SelectionKey is returned. This object provides:

You can also attach objects to the key (e.g., buffers or session info):

SelectionKey key = clientChannel.register(selector, SelectionKey.OP_READ);
key.attach(ByteBuffer.allocate(1024));

Later, you can retrieve the attachment:

ByteBuffer buffer = (ByteBuffer) key.attachment();

Summary

Using selectors and channel registration together forms the backbone of scalable, non-blocking network servers in Java.

Index

7.3 Handling SelectionKeys and Events

In Java NIO’s non-blocking IO system, the Selector class works closely with the SelectionKey class to monitor and handle IO readiness events across multiple channels. Understanding how to work with SelectionKey objects is essential for building efficient and scalable applications.

This section explores what SelectionKey represents, the types of events it tracks, and how to process these keys during event-driven IO operations.

What is a SelectionKey?

A SelectionKey represents the registration of a channel with a selector. It is created when a SelectableChannel (such as a SocketChannel) is registered to a Selector using the register() method:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

This key maintains information about:

Selection Operations (Event Types)

The SelectionKey class defines four constants representing IO readiness events:

Constant Meaning
OP_ACCEPT Ready to accept a new incoming connection (server socket)
OP_CONNECT A non-blocking connection has finished establishing
OP_READ Channel has data available to read
OP_WRITE Channel is ready to accept data for writing

These are referred to as interest ops when registering the channel and as ready ops when the event has occurred.

Inspecting and Handling Events

Once the Selector.select() method is called and returns, the selectedKeys() method gives you the set of keys for channels that are ready.

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();

You loop through this set to handle events:

while (iterator.hasNext()) {
    SelectionKey key = iterator.next();

    if (key.isAcceptable()) {
        // Handle new connection
    } else if (key.isConnectable()) {
        // Handle client connection finish
    } else if (key.isReadable()) {
        // Handle read from client
    } else if (key.isWritable()) {
        // Handle write to client
    }

    iterator.remove(); // Important: remove the processed key
}

Example: Full Event Handling Logic

Here’s a complete code example of a simple server handling accept and read events using SelectionKey.

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class SelectionKeyExample {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();

        // Create server channel and register for accept
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(5000));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("Server started on port 5000.");

        while (true) {
            selector.select(); // Wait for events
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();

                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("Accepted new client.");
                }

                else if (key.isReadable()) {
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(256);
                    int bytesRead = client.read(buffer);

                    if (bytesRead == -1) {
                        client.close();
                        System.out.println("Client disconnected.");
                    } else {
                        buffer.flip();
                        byte[] data = new byte[buffer.remaining()];
                        buffer.get(data);
                        System.out.println("Received: " + new String(data));
                    }
                }

                iter.remove(); // Important to avoid reprocessing
            }
        }
    }
}

Attaching Context with SelectionKey

You can store additional context (such as a buffer or session object) using the attach() method when registering the channel:

SelectionKey key = clientChannel.register(selector, SelectionKey.OP_READ);
key.attach(ByteBuffer.allocate(1024)); // Attach a buffer

Later, you can retrieve it during event handling:

ByteBuffer buffer = (ByteBuffer) key.attachment();

This approach is useful when each client needs its own data buffer or state tracker.

Best Practices for Using SelectionKey

Summary

By mastering how to work with SelectionKey, you unlock the full power of Java NIO selectors and can build high-performance applications with minimal thread usage.

Index

7.4 Building a Simple Non-blocking Server

In this tutorial, we'll build a simple non-blocking TCP server that:

Step 1: Imports and Setup

We’ll begin by importing the necessary Java NIO classes:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

Step 2: Create and Configure the Server

We need a ServerSocketChannel and a Selector. The server channel must be non-blocking and registered with the selector to watch for OP_ACCEPT events (ready to accept new connections).

public class NonBlockingTCPServer {
    public static void main(String[] args) {
        try {
            // 1. Open a selector
            Selector selector = Selector.open();

            // 2. Open a server socket channel
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.bind(new InetSocketAddress(5000));
            serverChannel.configureBlocking(false); // Non-blocking mode

            // 3. Register the server channel with selector for ACCEPT operations
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("Server listening on port 5000...");

            // 4. Event loop
            while (true) {
                selector.select(); // Blocking call - waits for events

                // 5. Get the set of keys representing ready channels
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectedKeys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();

                    // 6. Acceptable event (new client connection)
                    if (key.isAcceptable()) {
                        handleAccept(key, selector);
                    }

                    // 7. Readable event (client sent data)
                    else if (key.isReadable()) {
                        handleRead(key);
                    }

                    // Remove the key from the set to avoid processing again
                    iterator.remove();
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Step 3: Handle New Client Connections

When a client attempts to connect, the server channel becomes “acceptable”. We then accept the connection and register the new client channel for OP_READ (read readiness).

private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept(); // Accept client
        clientChannel.configureBlocking(false);

        // Register client channel for READ events
        clientChannel.register(selector, SelectionKey.OP_READ);

        System.out.println("New client connected from " + clientChannel.getRemoteAddress());
    }

Step 4: Handle Incoming Data

When a client sends data, the channel becomes “readable”. We read the data from the channel using a ByteBuffer, and in this example, we echo the data back to the client.

private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = -1;

        try {
            bytesRead = clientChannel.read(buffer);
        } catch (IOException e) {
            System.out.println("Client forcibly closed the connection.");
            clientChannel.close();
            key.cancel();
            return;
        }

        if (bytesRead == -1) {
            // Client closed the connection cleanly
            System.out.println("Client disconnected.");
            clientChannel.close();
            key.cancel();
            return;
        }

        // Echo back the received message
        buffer.flip(); // Prepare buffer for reading
        String received = new String(buffer.array(), 0, buffer.limit());
        System.out.println("Received: " + received.trim());

        // Echo it back
        clientChannel.write(buffer); // Buffer still in read mode
    }
}
Click to view full runnable Code

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NonBlockingTCPServer {
    public static void main(String[] args) {
        try {
            // 1. Open a selector
            Selector selector = Selector.open();

            // 2. Open a server socket channel
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.bind(new InetSocketAddress(5000));
            serverChannel.configureBlocking(false); // Non-blocking mode

            // 3. Register the server channel with selector for ACCEPT operations
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("Server listening on port 5000...");

            // 4. Event loop
            while (true) {
                selector.select(); // Blocking call - waits for events

                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectedKeys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();

                    if (key.isAcceptable()) {
                        handleAccept(key, selector);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    }

                    iterator.remove(); // Prevent processing the same key again
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        clientChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("New client connected from " + clientChannel.getRemoteAddress());
    }

    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead;

        try {
            bytesRead = clientChannel.read(buffer);
        } catch (IOException e) {
            System.out.println("Client forcibly closed the connection.");
            clientChannel.close();
            key.cancel();
            return;
        }

        if (bytesRead == -1) {
            System.out.println("Client disconnected.");
            clientChannel.close();
            key.cancel();
            return;
        }

        buffer.flip();
        String received = new String(buffer.array(), 0, buffer.limit());
        System.out.println("Received: " + received.trim());

        clientChannel.write(buffer); // Echo back
    }
}

How It Works

Testing the Server

You can test the server using multiple telnet sessions:

telnet localhost 5000

Type a message and press Enter. The server will echo the message back to you.

Benefits of Non-blocking NIO

Limitations and Enhancements

This server is a good starting point, but there are areas for improvement:

Summary

In this tutorial, you’ve learned how to:

Java NIO selectors are a powerful tool for building scalable network applications. With just a bit more work, this basic server can evolve into a production-grade framework.

Index

7.5 Performance Considerations

Java NIO's selector-based non-blocking IO model is designed for high performance and scalability. Unlike traditional IO, which dedicates a thread per connection, NIO allows a single thread to handle thousands of connections by multiplexing readiness events across channels. While this design provides significant performance benefits, achieving optimal efficiency requires careful attention to several areas: threading, resource management, event handling, and common pitfalls.

In this section, we’ll explore these performance considerations in depth and provide actionable best practices for optimizing selector-based applications.

Scalability Through Fewer Threads

A primary advantage of non-blocking IO is the ability to support many concurrent connections using a small number of threads. Since a selector can monitor thousands of channels, the server can scale horizontally without spawning a thread per client.

Why It Matters:

Best Practice:

Thread Management and Design Patterns

To avoid performance bottlenecks, organize your application into logical thread roles:

This pattern ensures the selector loop remains responsive and doesn't block on operations like disk IO or long-running tasks.

[Selector Thread] → [Worker Pool] → [Processing Logic]

Best Practice:

Minimizing Memory Allocation and Garbage Collection

Non-blocking servers can experience frequent memory allocation from buffers and temporary objects, which leads to garbage collection (GC) pressure. GC pauses can introduce latency and jitter.

Tips to reduce GC overhead:

Example:

ByteBuffer buffer = ByteBuffer.allocate(1024);
key.attach(buffer); // Reuse per client

Efficient Buffer and Channel Management

Efficient use of ByteBuffer is critical for performance. Understand buffer methods like clear(), flip(), compact() to avoid redundant copying or unnecessary allocations.

Best Practice:

Caution: Direct buffers avoid heap GC but are more expensive to allocate and harder to monitor. Use them judiciously.

Selector Wakeup Overhead

A common pitfall is unnecessary wakeups of the selector, which can degrade performance, especially under high load.

Avoid:

Best Practice:

Handling Write Readiness Properly

Another common pitfall is treating OP_WRITE like OP_READ. Unlike read events, write events are always ready unless the buffer is full. Constantly registering for write events can cause busy loops and CPU spikes.

Best Practice:

if (buffer.hasRemaining()) {
    key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
} else {
    key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}

Avoid Blocking Operations in Event Loop

Blocking the selector thread (e.g., by reading from disk, calling Thread.sleep(), or synchronizing on shared objects) severely reduces responsiveness.

Best Practice:

Monitoring and Profiling

To tune performance, you must observe your application under load:

Use Selectors Correctly

NIO selectors are powerful but fragile. Misuse can lead to stale keys, dropped connections, or busy spinning.

Common Issues:

Best Practice:

Summary

Using selectors and non-blocking IO in Java NIO can yield high-performance, scalable servers when used correctly. Key takeaways include:

By adhering to these practices, you can build NIO-based applications that serve thousands of clients efficiently while minimizing CPU, memory, and thread usage.

Below is a complete example of a multi-threaded non-blocking TCP server using Java NIO with:

Features Demonstrated

Full Java Example

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.*;

public class MultiThreadedNIOServer {
    private static final int PORT = 5000;
    private static final int BUFFER_SIZE = 1024;

    private final Selector selector;
    private final ServerSocketChannel serverChannel;
    private final ExecutorService workerPool;
    private final ConcurrentLinkedQueue<Runnable> pendingTasks;

    public MultiThreadedNIOServer() throws IOException {
        this.selector = Selector.open();
        this.serverChannel = ServerSocketChannel.open();
        this.workerPool = Executors.newFixedThreadPool(4); // Worker thread pool
        this.pendingTasks = new ConcurrentLinkedQueue<>();

        serverChannel.bind(new InetSocketAddress(PORT));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    public void start() throws IOException {
        System.out.println("Server started on port " + PORT);

        while (true) {
            // Execute any tasks added from other threads
            Runnable task;
            while ((task = pendingTasks.poll()) != null) {
                task.run();
            }

            selector.select();

            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();

                try {
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    } else if (key.isWritable()) {
                        handleWrite(key);
                    }
                } catch (IOException e) {
                    System.err.println("Closing broken connection: " + e.getMessage());
                    closeKey(key);
                }

                iter.remove();
            }
        }
    }

    private void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        SocketChannel client = server.accept();
        client.configureBlocking(false);

        // Attach a buffer for each client connection
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        client.register(selector, SelectionKey.OP_READ, buffer);

        System.out.println("Accepted connection from " + client.getRemoteAddress());
    }

    private void handleRead(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        int bytesRead = client.read(buffer);

        if (bytesRead == -1) {
            closeKey(key);
            return;
        }

        buffer.flip();
        byte[] data = new byte[buffer.limit()];
        buffer.get(data);
        buffer.clear(); // Ready for next read

        String message = new String(data).trim();
        System.out.println("Received: " + message);

        // Offload processing to worker pool
        workerPool.submit(() -> {
            String response = "[Echo] " + message;
            ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());

            // Schedule write back in selector thread
            scheduleTask(() -> {
                key.attach(responseBuffer);
                key.interestOps(SelectionKey.OP_WRITE);
                selector.wakeup(); // Ensure selector notices change
            });
        });
    }

    private void handleWrite(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        client.write(buffer);
        if (!buffer.hasRemaining()) {
            // Done writing; switch back to read
            ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
            key.attach(readBuffer);
            key.interestOps(SelectionKey.OP_READ);
        }
    }

    private void scheduleTask(Runnable task) {
        pendingTasks.add(task);
        selector.wakeup(); // Wake up selector to execute task
    }

    private void closeKey(SelectionKey key) {
        try {
            key.channel().close();
        } catch (IOException ignored) {}
        key.cancel();
    }

    public static void main(String[] args) throws IOException {
        new MultiThreadedNIOServer().start();
    }
}

How This Works

Test the Server

Open multiple terminal tabs and run:

telnet localhost 5000

Each message sent will be echoed back with [Echo] prefix.

Best Practices Applied

Index