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.
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:
SocketChannel
(client connections)ServerSocketChannel
(server sockets)DatagramChannel
(UDP sockets)These channels must be in non-blocking mode to be used with a Selector
.
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.
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.
OP_ACCEPT
, OP_CONNECT
, OP_READ
, OP_WRITE
).SelectionKey
is returned. This key represents the registration and holds the interest and readiness sets.OP_ACCEPT
: A server socket is ready to accept a new connection.OP_CONNECT
: A socket channel finished its connection process.OP_READ
: A channel is ready for reading.OP_WRITE
: A channel is ready for writing.select()
to wait for readinessimport 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();
}
}
}
}
Selector
is created to monitor events.ServerSocketChannel
is configured for non-blocking mode and registered with the selector for OP_ACCEPT
.select()
blocks until one or more channels are ready.SelectionKey.isAcceptable()
detects new incoming connections.SelectionKey.isReadable()
handles incoming data from clients.SocketChannel
is also set to non-blocking mode and registered with the selector.OP_ACCEPT
, OP_READ
, and OP_WRITE
allow you to handle different types of IO operations.Selectors enable event-driven IO models that are scalable, efficient, and well-suited for modern networked applications.
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.
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:
SocketChannel
– for client TCP connections.ServerSocketChannel
– for server-side listening sockets.DatagramChannel
– for UDP-based communication.Pipe.SourceChannel
and Pipe.SinkChannel
– for inter-thread communication.All these channels must be configured to non-blocking mode before being registered with a selector.
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);
channel
– A SelectableChannel
such as SocketChannel
.selector
– The Selector
instance that will monitor this channel.ops
– A set of interest operations (like OP_READ
or OP_WRITE
) the channel wants to be notified about.key
– A SelectionKey
object representing this registration.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.
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);
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
}
}
}
}
OP_ACCEPT
, allowing the selector to notify when a client attempts to connect.OP_READ
.When a channel is registered, a SelectionKey
is returned. This object provides:
channel()
– The registered channel.selector()
– The selector managing the key.interestOps()
– The set of operations the key is interested in.readyOps()
– The set of operations the channel is ready for.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();
Selector
.register()
method binds a channel to a selector with a specified interest set (OP_READ
, OP_WRITE
, etc.).SelectionKey
represents each registration and provides APIs to inspect readiness and attach metadata.Using selectors and channel registration together forms the backbone of scalable, non-blocking network servers in Java.
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.
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:
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.
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
}
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
}
}
}
}
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.
SelectionKey
iterator.remove()
to avoid handling them again.if-else
or switch
to check each.read()
returns -1
, it means the client has disconnected. Always close the channel.SelectionKey
objects represent the link between a channel and a selector.Selector
using selectedKeys()
.OP_ACCEPT
, OP_READ
, and OP_WRITE
allow efficient, event-driven IO.SelectionKey
objects is essential for building scalable non-blocking servers.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.
In this tutorial, we'll build a simple non-blocking TCP server that:
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;
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();
}
}
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());
}
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
}
}
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
}
}
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.
This server is a good starting point, but there are areas for improvement:
SelectionKey.attach()
) to store session state.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.
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.
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:
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:
ExecutorService
to manage a pool of workers for background processing.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:
ByteBuffer
objects: Allocate buffers once and reuse them per connection.SelectionKey
to store buffer and session objects.Example:
ByteBuffer buffer = ByteBuffer.allocate(1024);
key.attach(buffer); // Reuse per client
Efficient use of ByteBuffer
is critical for performance. Understand buffer methods like clear()
, flip()
, compact()
to avoid redundant copying or unnecessary allocations.
Best Practice:
ByteBuffer.allocateDirect()
) for large or long-lived buffers.ByteBuffer.allocate()
) for short-lived or small objects where GC is negligible.Caution: Direct buffers avoid heap GC but are more expensive to allocate and harder to monitor. Use them judiciously.
A common pitfall is unnecessary wakeups of the selector, which can degrade performance, especially under high load.
Avoid:
selector.wakeup()
too frequently.Best Practice:
wakeup()
only when needed.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:
OP_WRITE
when you have actual data to write.OP_WRITE
interest once all data is written.if (buffer.hasRemaining()) {
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
} else {
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
Blocking the selector thread (e.g., by reading from disk, calling Thread.sleep()
, or synchronizing on shared objects) severely reduces responsiveness.
Best Practice:
readLine()
or database queries in the selector thread.To tune performance, you must observe your application under load:
NIO selectors are powerful but fragile. Misuse can lead to stale keys, dropped connections, or busy spinning.
Common Issues:
iterator.remove()
causes keys to be reprocessed.IOException
can leave channels in an inconsistent state.Best Practice:
remove()
keys after processing.Using selectors and non-blocking IO in Java NIO can yield high-performance, scalable servers when used correctly. Key takeaways include:
OP_WRITE
only when needed.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:
OP_WRITE
readinessSelector
ExecutorService
for background processingSelector.wakeup()
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();
}
}
OP_READ
, OP_WRITE
)Open multiple terminal tabs and run:
telnet localhost 5000
Each message sent will be echoed back with [Echo]
prefix.