Selectors: one thread, many connections
◈ 6 cardsSelector.open, register with interest ops and an attachment, the three select methods, the key removal that keeps the loop honest, and a non-blocking single-file HTTP server as the A2 alternative.
Readiness selection is the missing half
Non-blocking mode answers "can I act on this channel without waiting?" one channel at a time, and the answer is usually no. A Selector answers a better question about a whole set at once: which of my registered channels can I act on right now? That is readiness selection, and it is what turns a spinning loop into a server.
Registering a channel
Selector selector = Selector.open();
The registration method lives on the channel, not on the selector — SelectableChannel.register(selector, ops) and register(selector, ops, attachment). It reads backwards the first time. FileChannel is not selectable; every network channel is. A channel must be in non-blocking mode before it can register, or you get IllegalBlockingModeException.
ops is a bitmask built from four constants on SelectionKey: OP_ACCEPT, OP_CONNECT, OP_READ, OP_WRITE. They are bit flags, so combine them with |.
The optional third argument is the attachment — an arbitrary object holding that connection’s state. This is where NIO puts what a thread-per-connection server would have kept in local variables: the response buffer, a half-parsed request, a byte count for the access log. Retrieve it with key.attachment(), set it later with key.attach(obj).
Selecting
select()blocks until at least one registered channel is ready.select(long timeout)waits at most that many milliseconds and may return 0.selectNow()never blocks and returns 0 if nothing is ready.
All three return the number of ready channels. selector.wakeup() from another thread forces a blocked select() to return, which is how you shut a select loop down cleanly.
The loop, and the removal rule
selectedKeys() returns a Set<SelectionKey>. Iterate it, and remove each key from the iterator as you take it. The selector never clears that set for you: a key you leave behind is handed back on every future pass whether or not its channel is ready, so your loop stops being driven by readiness and starts spinning. it.remove() is one line and its absence is invisible until the CPU graph flatlines at 100%.
For each key, test what it is ready for with isAcceptable(), isConnectable(), isReadable() or isWritable(), take the channel with key.channel(), and act.
OP_WRITE is a trap
A socket is "writable" whenever the kernel send buffer has room — which is nearly always true. A channel registered permanently for OP_WRITE is therefore reported ready on every single select(), and you are back to a busy loop with extra steps. Register for OP_READ in the normal case and switch to OP_WRITE with key.interestOps(...) only while you actually have bytes queued for that client.
Winding down
key.cancel() deregisters a key. Closing a channel cancels its keys in every selector automatically, and closing the selector invalidates all of its keys and wakes any thread blocked in select().
Worked example — a non-blocking single-file HTTP server
This is the A2 alternative: one thread serving one file to every client that connects.
- Prepare the response once. Read the file, build the status line and headers, and put header and body into one
ByteBufferso a response is a single contiguous blast. Keep that buffer read-only and never drain it. - Set up.
ServerSocketChannel.open(),bind(port),configureBlocking(false),register(selector, OP_ACCEPT). - Loop.
select(), then walkselectedKeys()callingit.remove()on every key, and dispatch: - -
isAcceptable()→accept(), set the new channel non-blocking,register(selector, OP_READ). - -
isReadable()→ read up to a few kilobytes of request. A complete server would parse the request line here and choose a file; this one serves the same file regardless. Thenkey.interestOps(SelectionKey.OP_WRITE)andkey.attach(response.duplicate()). - -
isWritable()→ write the attached buffer; whenhasRemaining()goes false the response is complete, so close the channel. - Wrap the per-key work in try/catch. One malformed connection should cancel its own key and close its own channel — not kill the loop that is serving everybody else. In a thread-per-connection server the thread dies alone; here there is only one thread, so an uncaught exception takes the whole server down.
The duplicate() in step 3 is what makes the design work. Every client gets its own position and limit over the same underlying bytes, so a client on a slow link never holds up a fast one and the file is stored exactly once no matter how many clients arrive.