BIO, NIO, AIO

Synchronous and Asynchronous

Synchronous and asynchronous refer to how applications interact with the kernel:

  • Synchronous: When a user process triggers an I/O operation, it must wait for the I/O operation to complete or poll to check if the I/O operation is ready. For example: In traditional BIO mode, when the read() method is called, it must wait until data is truly copied from kernel space to user space before returning.

  • Asynchronous: When an asynchronous call is made, the caller does not immediately get a result. The callee notifies the caller through state changes (such as NIO’s Selector mechanism) or callback functions (such as AIO’s CompletionHandler).

Blocking vs Non-blocking

Blocking and non-blocking refer to the behavior of processes when accessing data:

  • Blocking: Read/write operations wait until the I/O operation completes. When data is not ready, the calling thread is suspended. Advantage is simple programming model; disadvantage is low resource utilization.

  • Non-blocking: Read/write operations immediately return status values without waiting. Advantage is improved system throughput; disadvantage is need to poll for status, increasing CPU burden.

Combined Application Scenarios

  1. Synchronous Blocking (BIO): Typical implementation is traditional Java Socket; suitable for architectures with few and fixed connections.
  2. Synchronous Non-blocking (NIO): Typical implementation is Java NIO’s Selector mechanism; suitable for high-concurrency connection but short-connection architectures (such as chat servers).
  3. Asynchronous Non-blocking (AIO): Typical implementation is Java AIO; suitable for high-concurrency connection and long-connection architectures (such as file servers).

Note: There is no “asynchronous blocking” combination because asynchronous itself means the calling thread is not blocked.


BIO (Blocking I/O)

Basic Introduction

BIO (Blocking I/O) is the synchronous blocking I/O model, where “B” stands for Blocking. This was the only I/O model choice before Java 1.4 and is the most basic I/O processing method in traditional network programming.

Working Principles

The server implementation uses a “one connection, one thread” model. When a client initiates a connection request:

  1. The server must allocate an independent thread for each new connection
  2. This thread is responsible for all I/O operations of that connection throughout
  3. During I/O operations (such as read/write), the thread blocks and waits until the operation completes

Performance Characteristics

  • High Resource Overhead: Each connection requires an independent thread; thread creation and context switching costs are high
  • Simple and Easy to Use: The programming model is intuitive and easy to understand and implement
  • Low Efficiency: When the number of connections increases, the number of threads grows linearly, and system resources are quickly exhausted

Applicable Scenarios

  • Applications with small and fixed number of client connections
  • Services insensitive to latency
  • Prototype systems requiring rapid development
  • Teaching examples and simple demo programs

Server Code

package icu.wzk.io;

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class WzkIOServer {

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress("127.0.0.1", 8889));
        while (true) {
            Socket socket = serverSocket.accept();
            new Thread(() -> {
                try {
                    byte[] bytes = new byte[1024];
                    int len = socket.getInputStream().read(bytes);
                    System.out.println(new String(bytes, 0, len));

                    socket.getOutputStream().write(bytes, 0, len);
                    socket.getOutputStream().flush();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }

}

Client Code

package icu.wzk.io;

import java.net.Socket;

public class WzkIOClient {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1", 8889);
        socket.getOutputStream().write("hello".getBytes());
        socket.getOutputStream().flush();
        System.out.println("============ send ===========");

        byte[] bytes = new byte[1024];
        int len = socket.getInputStream().read(bytes);
        System.out.println("server: " + new String(bytes, 0, len));
        socket.close();
    }

}

NIO (New I/O)

Basic Introduction

NIO (Non-blocking I/O or New I/O) is a completely new I/O API provided by Java starting from JDK 1.4 to replace traditional blocking I/O. Compared to traditional I/O, NIO provides more efficient and flexible I/O processing methods.

Core characteristics of NIO:

  • Synchronous non-blocking: Threads are not blocked while waiting for I/O operations to complete
  • Channel and buffer-based I/O approach
  • Ability to manage multiple connections with a single thread

Server implementation model: NIO uses a “one request, one channel” model. All client connection requests are registered with a multiplexer (Selector). The multiplexer continuously polls these channels. When an I/O request occurs on a channel, a thread is started to process it.

Channels

Channels are the most important abstraction concept introduced by NIO. They represent an open data connection that can be used for reading and writing data. Unlike streams, channels are bidirectional and can be used for both read and write operations.

Main characteristics:

  • Data can be read from a Channel into a Buffer
  • Data can be written from a Buffer into a Channel
  • Support for asynchronous non-blocking operations
  • Multiple implementation types (such as FileChannel, SocketChannel, etc.)

Buffers

Buffers are containers used by NIO to store data. All data is transmitted through buffers.

Key buffer operations:

  1. Write data to Buffer
  2. Call flip() to switch to read mode
  3. Read data from Buffer
  4. Call clear() or compact() to clear the buffer

Selectors

Selectors are key components for achieving multiplexing in NIO, allowing a single thread to handle multiple channels.

Working principles:

  1. Register channels with the selector
  2. The selector continuously polls registered channels
  3. When an interested event occurs on a channel, the selector notifies the application
  4. Application handles these events

Main event types:

  • SelectionKey.OP_CONNECT (connection ready)
  • SelectionKey.OP_ACCEPT (accept ready)
  • SelectionKey.OP_READ (read ready)
  • SelectionKey.OP_WRITE (write ready)

Server Code

package icu.wzk.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Scanner;

public class WzkNIOServer extends Thread {

    private Selector selector;

    private final ByteBuffer readBuffer = ByteBuffer.allocate(1024);

    private final ByteBuffer writeBuffer = ByteBuffer.allocate(1024);

    public WzkNIOServer(int port) {
        init(port);
    }

    private void init(int port) {
        try {
            System.out.println("Server started");
            this.selector = Selector.open();
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.bind(new InetSocketAddress(port));
            serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
            System.out.println("Server startup complete");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                this.selector.select();
                Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();
                while (keys.hasNext()) {
                    SelectionKey key = keys.next();
                    keys.remove();
                    if (key.isValid()) {
                        try {
                            if (key.isAcceptable()) {
                                accept(key);
                            }
                            if (key.isReadable()) {
                                read(key);
                            }
                            if (key.isWritable()) {
                                write(key);
                            }
                        } catch (CancelledKeyException e) {
                            key.cancel();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

    private void accept(SelectionKey key) {
        try {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(this.selector, SelectionKey.OP_READ);
            System.out.println("Client connected: " + socketChannel.getRemoteAddress());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void read(SelectionKey key) {
        try {
            this.readBuffer.clear();
            SocketChannel socketChannel = (SocketChannel) key.channel();
            int readLen = socketChannel.read(this.readBuffer);
            if (readLen == -1) {
                key.channel().close();
                return;
            }

            this.readBuffer.flip();
            byte[] bytes = new byte[readBuffer.remaining()];
            readBuffer.get(bytes);
            System.out.println("Received from client: " + socketChannel.getRemoteAddress() + " " + new String(bytes, StandardCharsets.UTF_8));

            socketChannel.register(this.selector, SelectionKey.OP_WRITE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void write(SelectionKey key) {
        try {
            this.writeBuffer.clear();
            SocketChannel socketChannel = (SocketChannel) key.channel();
            Scanner scanner = new Scanner(System.in);
            System.out.println("Send data to client, press Enter after input");
            String line = scanner.nextLine();
            this.writeBuffer.put(line.getBytes(StandardCharsets.UTF_8));
            writeBuffer.flip();
            socketChannel.write(writeBuffer);

            socketChannel.register(this.selector, SelectionKey.OP_READ);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(new WzkNIOServer(8889)).start();
    }

}

Client Code

package icu.wzk.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;


public class WzkNIOClient {
    public static void main(String[] args) {
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8889);
        SocketChannel socketChannel = null;
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        try {
            socketChannel = SocketChannel.open();
            socketChannel.connect(inetSocketAddress);
            Scanner sc = new Scanner(System.in);
            while (true) {
                System.out.println("Client sends message to server");
                String line = sc.nextLine();
                if ("exit".equals(line)) {
                    break;
                }
                byteBuffer.put(line.getBytes(StandardCharsets.UTF_8));
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
                byteBuffer.clear();

                int len = socketChannel.read(byteBuffer);
                if (len == -1) {
                    socketChannel.close();
                    break;
                }
                byteBuffer.flip();

                byte[] bytes = new byte[byteBuffer.remaining()];
                byteBuffer.get(bytes);
                System.out.println("Server data: " + new String(bytes, StandardCharsets.UTF_8));
                byteBuffer.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != socketChannel) {
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}