Netty
Li Wei
Title: Netty
Preface
References:
Java NIO
Three Main Components
Channel & Buffer
A Channel is somewhat similar to a stream; it is a bidirectional conduit for reading and writing data. Data can be read from a Channel into a Buffer, and data can be written from a Buffer into a Channel. Traditional streams are either input or output, whereas Channels operate at a lower level.
Common Channels include:
FileChannelDatagramChannelSocketChannelServerSocketChannel
A Buffer is used to hold data being read or written. Common buffers are:
ByteBufferMappedByteBufferDirectByteBufferHeapByteBufferShortBufferIntBufferLongBufferFloatBufferDoubleBufferCharBuffer
Selector
A Selector works with a single thread to manage multiple channels and retrieve the events that occur on those channels. The channels operate in non‑blocking mode, so the thread is not stuck on any single channel. This is ideal for scenarios with a very large number of connections but low traffic.
Calling selector.select() blocks until one or more channels become ready for read/write. When such events occur, the select method returns, and the thread can handle the events.
ByteBuffer
Basic Overview
ByteBuffer is a class in Java NIO (non‑blocking I/O) that provides the ability to work with raw byte data. It is mainly used for:
- Reading from and writing to files, networks, and other I/O sources
- Supplying a buffer that supports asynchronous or non‑blocking I/O operations
Two Types
ByteBuffer comes in two varieties:
- Regular
**ByteBuffer**: created viaByteBuffer.allocate(int capacity); the buffer’s memory resides in the JVM heap. - Direct
**ByteBuffer**: created viaByteBuffer.allocateDirect(int capacity); the buffer’s memory is allocated in the operating system’s direct memory, typically used for high‑performance I/O.
| Feature | Regular ByteBuffer | Direct ByteBuffer |
|---|---|---|
| Memory location | JVM heap | OS direct memory |
| Access efficiency | Lower (requires copies) | Higher (avoids copies) |
| Garbage collection | Managed by JVM | Managed by OS, not affected by JVM GC |
| Suitable for | Small data and infrequent I/O | Large data and high‑frequency I/O |
Use cases
- Regular
**ByteBuffer**– good for small files or occasional I/O. - Direct
**ByteBuffer**– good for large files, high‑frequency I/O, or performance‑critical scenarios.
Key Properties
Important ByteBuffer properties:
capacity: total size of the bufferposition: current read/write pointerlimit: the limit up to which data may be read (i.e., the size of valid data)
When a buffer is created, position = 0 and limit defaults to the capacity.
In write mode, position indicates where the next byte will be written, and limit equals the capacity. The diagram below shows the state after writing 4 bytes:
(illustration omitted)
After calling flip(), position is set to 0 (the start of the readable region) and limit is set to the previous position (the amount of data written).
When 4 bytes have been read, the state looks like this:
(illustration omitted)
Calling clear() resets the buffer to its initial write state:
(illustration omitted)
Switching from read mode back to write mode mid‑stream can be done with compact(). This compresses the unread portion to the front and then switches to write mode, as shown:
(illustration omitted)
Common API
Debug tool – a utility class that can display the internal state of a buffer after each API call.
Creating Buffers
**static ByteBuffer allocate(int capacity)**: allocate a new byte buffer with the specified capacity (in bytes)**static ByteBuffer wrap(byte[] array**: wrap an existing byte array in aByteBuffer. No copy is made; the buffer uses the supplied array directly
Property Operations
int capacity(): returns the buffer’s total capacity (maximum number of bytes it can hold)int limit(): returns the current limit (the highest index that may be read or written)int position(): returns the current positionByteBuffer position(int newPosition): sets the position to a new valueByteBuffer limit(int newLimit): sets a new limitByteBuffer rewind(): resets the position to 0**void mark()**: marks the current position; later you can return to it withreset()
Note: both rewind and flip clear any previously set mark.
**void reset()**: resets the position to the value that was set by the most recent call tomark(); together these APIs implementrewind()
Reading/Writing Data
ByteBuffer put(byte b): writes a single byte at the current position and increments the position by 1byte get(): reads a single byte from the current position and increments the position by 1ByteBuffer put(byte[] src): writes multiple bytes from a byte array into the bufferByteBuffer get(byte[] dst): reads multiple bytes from the buffer into a byte array
State Management
ByteBuffer flip(): switches to read mode, setting the limit to the current position and resetting the position to 0ByteBuffer clear(): clears the buffer, preparing it for writing; position becomes 0 and limit is set to capacityboolean hasRemaining(): checks whether there are remaining bytes to be readint remaining(): returns the number of bytes between the current position and the limit
Slicing and Copying
ByteBuffer slice(): creates a newByteBufferthat is a slice of the current buffer (shares the underlying data)ByteBuffer duplicate(): creates a newByteBufferwith the same content but independent position and limit
File Programming
FileChannel
FileChannel is a class in the Java NIO (New I/O) library for efficient file read/write. Unlike traditional stream I/O, FileChannel supports buffer‑based operations (Buffer) and offers extra features such as memory‑mapped files and file locking.
Features
FileChannelis bidirectional—it can read and write data.FileChannelcan only operate in blocking mode, unlikeSocketChannel(see below) which supports non‑blocking.
Acquisition
You cannot open a FileChannel directly; you must obtain it through one of three stream types:
FileInputStream: the resultingFileChannelis read‑onlyFileOutputStream: the resultingFileChannelis write‑onlyRandomAccessFile: can read or write depending on the construction mode ofRandomAccessFile
Common API
Reading data – use
read(ByteBuffer dst)to read fromFileChannelintoByteBuffer. The return value is the number of bytes read;-1indicates end‑of‑file.Writing data – typically involves:
- Calling
put()to put data intoByteBuffer - Calling
flip()to flipByteBufferinto read mode - Looping with
whileand repeatedly invokingchannel.write(buffer)until all buffered data are written to the channel.
Note:
write()may not write the entire contents ofbuffertoFileChannelin one go, so you must keep calling it inside awhileloop.- Calling
Getting file size –
size()returns the size in bytes.Closing the channel – after file operations, close
FileChannelto free resources. Invoking theclose()method onFileInputStream,FileOutputStream, orRandomAccessFilewill indirectly closeFileChannel.Forcing data to disk – the OS usually caches writes for performance. Call
force(true)to force both file contents and metadata (e.g., permissions) to be flushed to disk, ensuring durability.Truncating a file –
FileChannelprovidestruncate(long size)to truncate the file to a specified length; excess bytes are discarded.File position control –
position()andposition(long newPosition)get and set the current position of aFileChannel, useful for random access.Memory‑mapped files –
map()can map a portion or the whole file into memory, which is especially handy for large files because it allows direct memory access.map()returns aMappedByteBufferobject.
Channel‑to‑Channel Data Transfer – code example showing data transfer between two channels.
Path & Paths
Path is a class introduced in Java NIO to represent a file‑system path. It lives in the java.nio.file package, and the Path interface replaces the old java.io.File, offering a more flexible and efficient way to work with files.
Path represents a path, which can be absolute or relative. Path objects are created via Paths.get() or FileSystems.getDefault().getPath(), providing a more intuitive, chainable API.
Common API
Retrieve basic path information:
Path getFileName(): returns the file namePath getParent(): returns the parent path, ornullif there is nonePath getRoot(): returns the root component (e.g.,C:/or/); returnsnullif there is no rootint getNameCount(): returns the number of name elements (excluding the root)Path getName(int index): returns the n‑th name element (index).
Path operations:
**Path resolve(Path other)**: resolves the current path against another, producing a new path. If the argument is absolute, the result is that absolute path (other→other).**Path relativize(Path other)**: computes the relative path from the current path to a target path; both must share the same root.**Path normalize()**: normalizes the path by removing redundant.and..components.**Path toAbsolutePath()**: converts a relative path to an absolute one.
Path comparison:
boolean startsWith(Path other): checks whether the current path starts with a given path.boolean endsWith(Path other): checks whether the current path ends with a given path.
Converting to a
File:File toFile(): converts aPathto aFileso that legacyjava.ioAPIs can be used.
Files
The Files class offers many static methods for file and directory operations, typically requiring a Path argument.
Common tasks include:
Checking file status:
boolean exists(Path path): does the file or directory exist?boolean isDirectory(Path path): is the given path a directory?
Creating and deleting:
Path createFile(Path path): create a file (throws if it already exists)Path createDirectory(Path path): create a directoryvoid delete(Path path): delete a file or directory; throwsDirectoryNotEmptyExceptionif the directory is non‑empty
Reading and writing:
**List readAllLines(Path path)**: read all lines of a file, returningList**void write(Path path, byte[] bytes)**: write a byte array to a file, overwriting existing content
Copying and moving:
**void copy(Path source, Path target, CopyOption... options)**: copy a file or directory; throwsFileAlreadyExistsExceptionif the target already exists**void move(Path source, Path target, CopyOption... options)**: move a file or directory
Traversing directories: before JDK 1.7 you had to recurse manually; since 1.7 you can use
Files.walkFileTreefor directory traversal.
Note: Deletion is dangerous—ensure that recursively deleted directories contain no important data.
Network Programming
Blocking
In blocking mode, the relevant methods cause the thread to pause:
ServerSocketChannel.acceptblocks the thread until a connection is established.SocketChannel.readblocks the thread until data is available to read.
A blocked thread does not consume CPU while paused, but it is essentially idle.
With a single thread, blocking calls interfere with each other and the program can hardly work; multithreading is required.
However, multithreading introduces new problems:
- On a 32‑bit JVM each thread consumes ~320 KB; on a 64‑bit JVM each thread consumes ~1 MB. With many connections you can quickly run out of memory (OOM). Moreover, too many threads cause frequent context switches, degrading performance.
- Thread pools can reduce the number of threads and context switches, but they do not solve the fundamental issue: if many connections stay idle for a long time, all pool threads become blocked. Therefore blocking I/O is unsuitable for long‑lived connections; it works only for short‑lived ones.
Code example:
(omitted)
Non‑Blocking
In non‑blocking mode, the related methods never pause the thread:
- When
ServerSocketChannel.acceptfinds no pending connection, it returnsnulland the thread continues. SocketChannel.readreturns0when no data is available, allowing the thread to perform other work (e.g., read from anotherSocketChannelor callServerSocketChannel.accept()).- When writing, the thread only waits for the data to be placed into the channel; it does not wait for the OS to actually transmit the bytes.
Even so, the thread continuously polls for readiness, which can waste CPU cycles when there is no activity.
During data copying, the thread is still effectively blocked (this is what Asynchronous I/O—AIO—aims to improve).
Code example: server side; client code unchanged.
Multiplexing
Overview – a single thread can use a Selector to monitor read/write events on multiple channels; this is called multiplexing.
- Multiplexing applies only to network I/O; ordinary file I/O cannot benefit from it.
- Without a selector, a non‑blocking thread would spend most of its time doing nothing. A selector ensures that:
- It attempts to connect only when a connection‑ready event occurs.
- It reads only when a read‑ready event occurs.
- It writes only when a write‑ready event occurs (a channel may not always be writable; when it becomes writable, the selector fires a write event).
Advantages
- One thread plus a selector can monitor many channels; the thread works only when an event occurs, avoiding wasted effort.
- The thread is fully utilized.
- Fewer threads are needed.
- Context‑switch overhead is reduced.
Basic Operations
Creation
Registering (binding) channel events – the channel must be in non‑blocking mode;
FileChannellacks a non‑blocking mode and therefore cannot be used with a selector.Event types that can be registered:
connect– triggered when a client successfully connectsaccept– triggered when the server accepts a connectionread– triggered when data is available to read (may fire later if the receiver is slow)write– triggered when the channel is ready for writing (may fire later if the sender is slow)
Listening for channel events – three methods can be used to detect whether events have occurred.
(content truncated)
Originally written by Li Wei (李唯_) and published in Chinese on 后端技术栈全书 (Full-Stack Backend Engineering). Translated and adapted for DriftSeas with permission.