Netty Overview
Netty is an asynchronous event-driven network application framework designed for rapid development of maintainable high-performance protocol servers and clients.
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
Key Characteristics:
- Netty serves as the standard framework for Java network programming, with many Java-based projects relying on it for underlying network communication.
- Netty provides higher-level abstractions over Java NIO, enabling faster development compared to raw NIO implementation.
| Netty Enhancement over NIO | Description |
|---|---|
| Protocol Handling | No need to build network protocols from scratch |
| Data Transmission | Built-in tools for TCP issues (packet sticking, half-packets) |
| Bug Fixes | Resolves epoll empty polling causing 100% CPU usage |
| API Improvements | Enhanced usability: FastThreadLocal vs ThreadLocal, ByteBuf vs ByteBuffer |
Event Loop Fundamentals:
The event loop mechanism is a programming pattern that waits for and dispatches events or messages in a program. It works by making a request to an event provider (generally blocking until an event arrives) and then calling the relevant event handler.
The event loop works by making a request to some internal or external "event provider" (that generally blocks the request until an event has arrived), then calls the relevant event handler ("dispatches the event").
Client/Server Implementation
Server Implementation
package netty_foundation;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
public class BasicServer {
public static void main(String[] args) {
/*
ServerBootstrap: Assembles Netty components and starts the server
NioEventLoopGroup: Contains BossEventLoop and WorkEventLoop for event handling
channel: Selects the socket channel implementation (NIO here)
childHandler: Boss handles accept events, worker handles read/write events
*/
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
/*
Pipeline handlers for server read/write channels:
- StringDecoder: Converts ByteBuffer content to String
- ChannelInboundHandlerAdapter: Custom handler logic
*/
channel.pipeline().addLast(new StringDecoder());
channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(msg);
}
});
}
})
.bind(8080);
}
}
Client Implementation
package netty_foundation;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
public class BasicClient {
public static void main(String[] args) throws InterruptedException {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
})
.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel()
.writeAndFlush("hello world");
}
}
Execution Flow (start server first, then client):
hello world // Server console output
Component Overview
| Component | Purpose | Notes |
|---|---|---|
| Channel | Data read/write conduit | Represents network connections |
| Handler | Data processing units | Multiple handlers form a pipeline;分为Inbound和Outbound两类 |
| EventLoop | Worker for data processing | Binds to channels for their lifetime |
Conceptual Model: When a host communicates with other hosts over the network, multiple data channels are established. Each channel requires a "worker" to manage its lifecycle (establishment, read, write operations). The EventLoop serves as this channel manager. Each EventLoop can manage multiple channels, and each channel is bound to its EventLoop for its entire lifetime.
EventLoop Component
Core Concepts
EventLoop: A single-threaded executor (maintaining one Selector) that handles continuous I/O events from Channels.
package io.netty.channel;
public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {
EventLoopGroup parent();
}
The EventLoop interface extends OrderedEventExecutor and EventLoopGroup:
- ScheduledExecutorService inheritance: Provides all thread pool methods
- OrderedEventExecutor: Manages executor instances
inEventLoop(Thread thread): Checks if a thread belongs to this EventLoopparent(): Returns the parent EventLoopGroup
EventLoopGroup: A collection of EventLoops. Channels typical use the register method to bind to one EventLoop:
- Channel I/O events are processed by the bound EventLoop (ensuring thread safety)
- Implements Iterable for traversing EventLoops
next()method retrieves the next EventLoop from the collection
package io.netty.channel;
public interface EventLoopGroup extends EventExecutorGroup {
EventLoop next();
ChannelFuture register(Channel channel);
ChannelFuture register(ChannelPromise promise);
}
Practical Examples
Goal: Demonstrate that EventLoopGroup uses round-robin selection and each EventLoop is a single-threaded thread pool supporting both regular and scheduled tasks.
Default Thread Count Configuration:
private static final int DEFAULT_EVENT_LOOP_THREADS =
Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads",
NettyRuntime.availableProcessors() * 2));
- Default: CPU cores × 2 when not configured
package netty_foundation;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
public class EventLoopDemo {
private static final Logger log = LoggerFactory.getLogger(EventLoopDemo.class);
public static void main(String[] args) {
// Create a group with two EventLoops
EventLoopGroup loopGroup = new NioEventLoopGroup(2);
for (int i = 0; i < 4; i++) {
System.out.println(loopGroup.next()); // Round-robin access
}
// Submit a regular task to one EventLoop
loopGroup.next().execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("regular task execution");
});
log.debug("main thread continues");
// Submit a scheduled task (can be used for connection keep-alive)
loopGroup.next().scheduleAtFixedRate(() -> {
log.debug("scheduled task");
}, 0, 1, TimeUnit.SECONDS);
}
}
Output Analysis:
io.netty.channel.nio.NioEventLoop@6b71769e
nio.netty.channel.nio.NioEventLoop@2752f6e2
nio.netty.channel.nio.NioEventLoop@6b71769e
nio.netty.channel.nio.NioEventLoop@2752f6e2
14:20:34.158 [main] DEBUG - main thread continues
14:20:34.161 [nioEventLoopGroup-2-2] DEBUG - scheduled task
14:20:35.164 [nioEventLoopGroup-2-2] DEBUG - scheduled task
14:20:35.164 [nioEventLoopGroup-2-1] DEBUG - regular task execution
14:20:36.169 [nioEventLoopGroup-2-2] DEBUG - scheduled task
...
- EventLoop selection uses round-robin scheduling
- Scheduled tasks execute at 1-second intervals
Channel Binding Example
Goal: Verify that each client connection binds to a specific EventLoop on the server
Note: When debugging multi-threaded programs, configure the debugger to suspend only the current thread rather than all threads.
Server Code:
package netty_foundation.loops;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
public class ChannelBindingServer {
private static final Logger log = LoggerFactory.getLogger(ChannelBindingServer.class);
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buffer = (ByteBuf) msg;
log.debug(buffer.toString(StandardCharsets.UTF_8));
}
});
}
})
.bind(8080);
}
}
Client Code:
package netty_foundation.loops;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
public class ChannelBindingClient {
public static void main(String[] args) throws InterruptedException {
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder(StandardCharsets.UTF_8));
}
})
.connect(new InetSocketAddress("localhost", 8080))
.sync()
.channel();
System.out.println(channel);
}
}
Running two client instances produces:
15:01:02.494 [nioEventLoopGroup-2-3] DEBUG - client1 message
15:05:07.445 [nioEventLoopGroup-2-3] DEBUG - client1 message
15:05:53.103 [nioEventLoopGroup-2-4] DEBUG - client2 message
15:05:56.714 [nioEventLoopGroup-2-4] DEBUG - client2 message
Each client binds to a specific EventLoop: client1 to nioEventLoopGroup-2-3, client2 to nioEventLoopGroup-2-4.
Server-Side Responsibility Division
Approach 1: Use two EventLoopGroups for responsibility separation
Rationale: Separate accept events from I/O events.
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
ObjectUtil.checkNotNull(childGroup, "childGroup");
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = childGroup;
return this;
}
Usage:
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
- Boss: Handles ServerSocketChannel accept events only
- Worker: Handles socketChannel read/write events
Approach 2: Additional EventLoopGroups for long-running operations
Rationale: When business operations take significant time processing channel events, other channels managed by the same single-threaded EventLoop may experience delays. Long-running operations should be delegated to separate threads.
package netty_foundation.loops;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
public class SeparatedDutyServer {
private static final Logger log = LoggerFactory.getLogger(SeparatedDutyServer.class);
public static void main(String[] args) {
// Dedicated EventLoopGroup for time-consuming operations
EventLoopGroup cpuIntensiveGroup = new DefaultEventLoop();
new ServerBootstrap()
.group(new NioEventLoopGroup(), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) throws Exception {
channel.pipeline().addLast("handler1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buffer = (ByteBuf) msg;
log.debug(buffer.toString(StandardCharsets.UTF_8));
ctx.fireChannelRead(msg); // Forward to next handler
}
});
// Delegate to separate EventLoopGroup
channel.pipeline().addLast(cpuIntensiveGroup, "handler2",
new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buffer = (ByteBuf) msg;
log.debug(buffer.toString(StandardCharsets.UTF_8));
}
});
}
})
.bind(8080);
}
}
Output with two clients:
16:11:40.917 [nioEventLoopGroup-4-2] DEBUG - client1:hello
16:11:40.918 [defaultEventLoop-1-1] DEBUG - client1:hello
16:12:01.469 [nioEventLoopGroup-4-1] DEBUG - client2:hello
16:12:01.476 [defaultEventLoop-1-1] DEBUG - client2:hello
Messages from each client are processed by two different EventLoops. Client1 messages pass through nioEventLoopGroup-4-2 then defaultEventLoop-1-1, while Client2 messages pass through nioEventLoopGroup-4-1 then defaultEventLoop-1-1.
EventLoop Switching Mechanism:
ctx.fireChannelRead(msg); // Propagate message to next handler
EventLoop Switching Source Analysis
Fundamental Principle: Multiple single-threaded thread pools collaborate to complete tasks through division of labor.
Call Chain:
// ChannelHandlerContext interface method
@Override
ChannelHandlerContext fireChannelRead(Object msg);
// AbstractChannelHandlerContext implementation
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
return this;
}
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor(); // Get next handler's EventLoop
if (executor.inEventLoop()) {
next.invokeChannelRead(m); // Same thread, execute directly
} else {
// Different thread, submit as task to next handler's EventLoop
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
Core Logic: The code determines whether the current thread matches the next handler's EventLoop. If yes, execution proceeds immediately. If no, the call is encapsulated as a Runnable task and submitted to the next handler's EventLoop for execution.
@Override
public boolean inEventLoop() {
return inEventLoop(Thread.currentThread());
}