Channels: SocketChannel and ServerSocketChannel
◈ 5 cardsOpening, connecting, reading and writing with channels; what configureBlocking(false) changes about accept and connect; and why a non-blocking loop without a selector burns a whole core.
A channel is the block-oriented replacement for a stream
A stream is byte-oriented: the model is one byte after another, with arrays offered for speed. A channel is block-oriented: bytes move a whole ByteBuffer at a time, and one channel usually reads and writes, where streams come in one-directional pairs. Three channel classes matter for network programming — SocketChannel, ServerSocketChannel and (Module 13) DatagramChannel — and none of them has a public constructor. You get one from a static open() factory.
Connecting
SocketChannel.open(SocketAddress) creates the channel and connects it, blocking until the handshake completes or an exception is thrown. SocketChannel.open() with no argument gives you an unconnected channel so you can configure it first, then call connect(address).
That second form is the one you need for non-blocking work, because in non-blocking mode connect() returns immediately — almost certainly before the TCP handshake has finished. Before you may use the connection you must call finishConnect(), which returns true when the connection is ready, false when it is still being established, and throws if it failed outright. isConnected() and isConnectionPending() report the same state without advancing it. In blocking mode finishConnect() simply returns true.
Reads and writes return counts
int read(ByteBuffer) stores bytes at the buffer position, advances it, and returns how many it stored. It returns -1 at end of stream — the peer closed — and on a non-blocking channel it may return 0, meaning "nothing available right now", which is a completely different thing.
int write(ByteBuffer) drains from the buffer position and returns how many bytes it took. Unlike an OutputStream.write, it is not obliged to take them all: on a non-blocking channel it takes whatever fits in the kernel send buffer and returns. That is why every drain is a loop over hasRemaining(), and it is the second-most-common NIO bug after the missing flip(). Array-taking forms of both methods give you scatter reads and gather writes — useful when a header and a body live in separate buffers.
The server side
ServerSocketChannel.open() creates the object but does not bind it; the name misleads. Bind it with bind(SocketAddress) (Java 7 and later; older code retrieves the peer with socket() and binds that), then call accept(). Accepting before binding throws NotYetBoundException, a runtime exception.
accept() returns a connected SocketChannel. In blocking mode — the default — it waits. Call configureBlocking(false) first and it returns null almost immediately whenever no connection is pending, which is a NullPointerException waiting to happen if you forget to test for it. Every network channel is AutoCloseable, so try-with-resources works exactly as it did in Module 7.
The bridge back to streams
The Channels utility class converts in both directions: newInputStream, newOutputStream, newReader, newWriter, newChannel. Use it when one part of a program is NIO and a library you must call still expects a stream — an XML parser, say. It is the escape hatch that stops NIO from being all-or-nothing.
Worked example — a non-blocking echo server with no selector
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(7));
server.configureBlocking(false);
List<SocketChannel> clients = new ArrayList<>();
ByteBuffer buf = ByteBuffer.allocate(256);
while (true) {
SocketChannel fresh = server.accept();
if (fresh != null) {
fresh.configureBlocking(false);
clients.add(fresh);
}
for (SocketChannel client : clients) {
buf.clear();
if (client.read(buf) > 0) {
buf.flip();
while (buf.hasRemaining()) client.write(buf);
}
}
}
This genuinely works, and it is genuinely wrong. One thread now serves any number of clients with no threads at all — but when nobody is connected and nobody is sending, accept() returns null immediately and every read() returns 0 immediately, so the loop spins at 100% CPU achieving nothing. You have replaced a thread that was asleep with a core that is awake and idle, which is a worse trade than the one you started with.
That is the shape of the gap. Non-blocking mode can tell you "not now"; it can never tell you "now". Supplying the missing half is the entire job of a Selector, and it is the next lesson.