Understanding Java I/O: BIO, NIO, Buffers, Channels, and Selectors
This article provides a comprehensive guide to Java I/O, covering traditional BIO streams, the differences between BIO, NIO and AIO, detailed explanations of byte and character streams, buffers, channels, selectors, and practical examples such as file copying and a simple client‑server chat implementation.
Java I/O is a large ecosystem that can be overwhelming for beginners. This article starts from the classic blocking I/O (BIO) model, introduces the evolution to non‑blocking I/O (NIO) after JDK 1.4, and compares BIO, NIO, and asynchronous I/O (AIO) using a simple water‑boiling analogy.
Traditional BIO
BIO uses separate input and output streams, each operating on bytes. Common classes include FileInputStream , FileOutputStream , ByteArrayInputStream , and various processing streams that wrap node streams to add buffering, data conversion, or push‑back capabilities.
Byte Streams
All byte‑oriented streams inherit from InputStream and OutputStream . Important methods of InputStream are int read() , int read(byte[] b) , and void close() . Important methods of OutputStream are void write(int b) , void write(byte[] b) , void flush() , and void close() .
Character Streams
Character streams inherit from Reader and Writer . They operate on Unicode characters and internally use a conversion stream ( InputStreamReader / OutputStreamWriter ) to translate between bytes and characters.
Conversion Between Byte and Character Streams
Use InputStreamReader to read characters from a byte stream and OutputStreamWriter to write characters to a byte stream. These conversion streams are essential when handling text data.
Buffers (ByteBuffer, CharBuffer, …)
In NIO, data is stored in buffers rather than streams. Each buffer has capacity , limit , position , and an optional mark . Core operations are put() to write data and get() to read data. Switching between write and read mode is done with flip() , clearing with clear() , and rewinding with rewind() .
Example of allocating a buffer:
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);Channels
Channels are bidirectional I/O conduits that work together with buffers. Important channel types are FileChannel (file I/O), SocketChannel and ServerSocketChannel (TCP), and DatagramChannel (UDP). Obtain a channel from a stream or socket via getChannel() .
File copy example using NIO:
FileChannel inChannel = new FileInputStream("src.jpg").getChannel();
FileChannel outChannel = new FileOutputStream("dest.jpg").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (inChannel.read(buffer) != -1) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
inChannel.close();
outChannel.close();Zero‑Copy
Traditional BIO copies data between kernel space and user space multiple times, causing CPU overhead. NIO can use zero‑copy techniques where the same physical memory is shared between kernel and user buffers, reducing context switches and eliminating unnecessary copies.
Selectors
Selectors enable a single thread to monitor many channels for readiness events (read, write, accept, connect). This replaces the one‑thread‑per‑socket model of BIO and dramatically reduces thread‑switching overhead. Key methods are Selector.open() , select() , selectNow() , and select(long timeout) . Ready events are represented by SelectionKey constants such as OP_READ , OP_WRITE , OP_ACCEPT , and OP_CONNECT .
Simple Chat Server Example
The server creates two selectors: one for accepting new connections and another for handling read events. Each accepted SocketChannel is registered with the read selector. When data arrives, the server reads it into a ByteBuffer and prints the message.
Selector acceptSelector = Selector.open();
Selector readSelector = Selector.open();
ServerSocketChannel listener = ServerSocketChannel.open();
listener.socket().bind(new InetSocketAddress(3333));
listener.configureBlocking(false);
listener.register(acceptSelector, SelectionKey.OP_ACCEPT);
// accept loop
if (acceptSelector.select(1) > 0) {
for (SelectionKey key : acceptSelector.selectedKeys()) {
if (key.isAcceptable()) {
SocketChannel client = ((ServerSocketChannel) key.channel()).accept();
client.configureBlocking(false);
client.register(readSelector, SelectionKey.OP_READ);
}
}
}
// read loop
if (readSelector.select(1) > 0) {
for (SelectionKey key : readSelector.selectedKeys()) {
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
client.read(buf);
buf.flip();
System.out.println(Charset.defaultCharset().decode(buf).toString());
}
}
}The client connects to the server, writes user input to a non‑blocking SocketChannel , and clears the buffer after each send.
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 3333));
channel.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
while (true) {
String msg = scanner.next();
buf.put(msg.getBytes());
buf.flip();
channel.write(buf);
buf.clear();
}Conclusion
The article covered the full Java I/O stack: classic BIO streams, the transition to NIO with buffers, channels, and selectors, and demonstrated practical use cases such as file copying, zero‑copy optimization, and a minimal chat server. Understanding these fundamentals is essential before using higher‑level frameworks like Netty or Mina.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.