侧边栏壁纸
  • 累计撰写 151 篇文章
  • 累计创建 21 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

从 NIO 到 Netty,那 Netty 到底封装了什么?

YaFuX
2026-06-26 / 0 评论 / 0 点赞 / 2 阅读 / 0 字
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

从 NIO 到 Netty,那 Netty 到底封装了什么?

前言

先回忆一下使用 Java NIO 编写服务端时,我们需要做什么:

ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(9090));

Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();

    Iterator<SelectionKey> iterator =
            selector.selectedKeys().iterator();

    while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        iterator.remove();

        if (key.isAcceptable()) {
            handleAccept(selector, key);
        }

        if (key.isReadable()) {
            handleRead(key);
        }

        if (key.isWritable()) {
            handleWrite(key);
        }
    }
}

这还只是一个最简版本。

真正用于生产时,还要继续处理:

跨线程注册 Channel
Selector.wakeup()
读半包与粘包
写半包
待发送队列
缓冲区扩容
连接关闭
异常传播
业务线程调度
内存回收
Handler 编排

于是很容易产生疑问:

Netty 是不是只把 Selector 包装了一层?

NioEventLoopGroup 是线程池,还是多路复用器?

一个 NioEventLoop 对应几个线程、几个 Selector?

Netty 为什么不用原生 ByteBuffer,而要重新设计 ByteBuf

ByteBuf 为什么不需要调用 flip()

BootstrapServerBootstrap 到底启动了什么?

ChannelPipelineHandler 是什么关系?

ChannelInitializer 为什么使用一次后就消失了?

@Sharable 加上以后,Handler 就自动线程安全吗?

connect()bind()writeAndFlush() 为什么都返回 Future?

调用 sync() 后,Netty 还是异步框架吗?

这些问题最终指向一个核心:

Netty 并不是简单隐藏了 NIO API,而是把网络程序中反复出现的线程模型、事件循环、缓冲区、状态传播和异步结果管理,抽象成了一套可组合的框架。


一、NIO 还缺什么?

Java NIO 已经提供了三块重要能力:

Channel
    网络连接或监听通道

ByteBuffer
    数据缓冲区

Selector
    就绪事件多路复用器

原始素材也把 Channel、缓冲区和 Selector 作为进入 Netty 前必须理解的三个基础概念。

但这些 API 只提供了较底层的能力。

它们没有直接规定:

Selector 应由哪个线程运行
新连接应该分配给哪个 Selector
跨线程任务如何提交
业务处理器如何排列
异常怎样沿处理链传播
读写结果怎样异步通知
缓冲区怎样池化和回收

因此,手写 NIO 最困难的部分往往不再是:

怎样调用 read()

而是:

怎样长期、正确地管理成千上万条连接

Netty 就是在这些空白之上建立了一组更高层抽象。


二、五个核心角色

可以先把 Netty 的核心结构记成五层:

Bootstrap
    ↓
EventLoopGroup
    ↓
Channel
    ↓
ChannelPipeline
    ↓
ChannelHandler

数据则主要通过:

ByteBuf

在这些组件之间传递。

1. Bootstrap

负责描述一条 Channel 应该如何被创建和启动:

使用哪个 EventLoopGroup
创建什么类型的 Channel
安装哪些 Handler
配置哪些 ChannelOption
连接哪个地址或绑定哪个端口

客户端使用:

Bootstrap

服务端使用:

ServerBootstrap

Netty 官方把 Bootstrap 包定义为典型客户端和服务端 Channel 初始化的流式辅助工具。

2. EventLoopGroup

管理一组 EventLoop,并负责选择某个 EventLoop 注册 Channel。

3. Channel

表示一条网络通道。

客户端连接、服务端监听通道和服务端接收到的连接,都会表现为不同类型的 Channel。

4. Pipeline

每个 Channel 都有自己的一条 Pipeline,用于保存并组织多个 Handler。

5. Handler

处理连接状态、入站数据、出站操作和异常事件。

把这些组件串起来:

Bootstrap 创建 Channel
        ↓
Channel 注册到 EventLoop
        ↓
EventLoop 监听网络事件
        ↓
事件进入 ChannelPipeline
        ↓
多个 ChannelHandler 依次处理
        ↓
数据使用 ByteBuf 表示

三、ByteBuf 改了什么?

原生 Java NIO 的 ByteBuffer 通常使用:

position
limit
capacity

同一个 position 同时参与读取和写入。

写完以后需要执行:

buffer.flip();

把缓冲区从写模式切换到读模式。

读取完以后又可能调用:

buffer.clear();

或者:

buffer.compact();

这种状态切换很容易写错。

1. 双索引

Netty 的 ByteBuf 使用两个独立索引:

readerIndex
writerIndex

它把缓冲区划分为三个区域:

0
│
├── 已读区域
│
readerIndex
│
├── 可读区域
│
writerIndex
│
├── 可写区域
│
capacity

始终满足:

0 <= readerIndex <= writerIndex <= capacity

read 开头的操作从 readerIndex 读取,并推进读索引;以 write 开头的操作从 writerIndex 写入,并推进写索引。正因为读写索引彼此独立,常规读写不需要像 ByteBuffer 那样调用 flip()

示例:

ByteBuf buffer =
        ByteBufAllocator.DEFAULT.buffer(8, 64);

try {
    buffer.writeInt(100);
    buffer.writeInt(200);

    System.out.println(buffer.readerIndex()); // 0
    System.out.println(buffer.writerIndex()); // 8

    int first = buffer.readInt();

    System.out.println(first);                // 100
    System.out.println(buffer.readerIndex()); // 4
    System.out.println(buffer.writerIndex()); // 8
} finally {
    buffer.release();
}

写操作只移动 writerIndex

写入前:
readerIndex = 0
writerIndex = 0

写入两个 int 后:
readerIndex = 0
writerIndex = 8

读操作只移动 readerIndex

读取一个 int 后:
readerIndex = 4
writerIndex = 8

2. read 与 get

readInt() 会移动读索引:

int value = buffer.readInt();

相当于:

读取数据
+
readerIndex 向后移动

getInt(index) 不会修改索引:

int value = buffer.getInt(buffer.readerIndex());

相当于:

查看指定位置的数据
但不消费数据

所以需要重复读取同一段内容时,不应该误用会推进索引的 read 方法。ByteBuf 官方文档将随机访问和顺序访问分开:getset 按指定位置访问,readwrite 则推进对应索引。

3. write 与 set

对应关系是:

readXXX
    读取并推进 readerIndex

getXXX
    读取但不移动索引

writeXXX
    写入并推进 writerIndex

setXXX
    写入但不移动索引

示例:

buffer.writeInt(100);
buffer.setInt(0, 200);

第二行修改了索引 0 位置的数据,但不会改变 writerIndex

4. clear 不清数据

调用:

buffer.clear();

只会把:

readerIndex = 0
writerIndex = 0

它不会保证把底层内存全部填充为零。Netty 官方文档明确指出,ByteBuf.clear() 清除的是两个索引,不是缓冲区中的实际字节。

因此:

clear()

更接近:

把整块容量重新标记为可写

而不是:

安全擦除数据

5. 容量可以扩展

可以指定初始容量和最大容量:

ByteBuf buffer =
        ByteBufAllocator.DEFAULT.buffer(8, 1024);

含义是:

初始容量:8
最大容量:1024

当写入空间不足时,部分写操作会调用扩容逻辑,但不能超过 maxCapacityByteBufAllocator.buffer(initialCapacity, maxCapacity) 的两个参数分别表示初始容量和最大容量。

6. 堆内还是堆外

可以检查:

buffer.isDirect();

但不能简单认为:

ByteBufAllocator.DEFAULT.buffer()
一定返回堆外内存

官方接口明确说明,buffer() 返回堆内还是直接缓冲区,取决于实际的 Allocator 实现;ioBuffer() 才表达“优先分配适合 I/O 的直接缓冲区”。

因此更准确的理解是:

ByteBuf
    统一缓冲区接口

ByteBufAllocator
    决定怎样分配

具体实现
    决定堆内、堆外、池化或非池化

7. 池化不是必然

Netty 提供:

PooledByteBufAllocator
UnpooledByteBufAllocator

所以 ByteBuf 具备池化能力,并不表示每个 ByteBuf 都一定来自内存池。官方 ByteBufAllocator 接口同时列出了池化和非池化实现,并提供 isDirectBufferPooled() 查询直接缓冲区是否池化。

8. 引用计数

ByteBuf 实现了 ReferenceCounted。引用计数降到 0 后,底层资源可以被释放或归还到池中。

手工分配的 ByteBuf 应注意释放:

ByteBuf buffer =
        ByteBufAllocator.DEFAULT.buffer();

try {
    buffer.writeInt(42);
    consume(buffer);
} finally {
    buffer.release();
}

但不能看到 ByteBuf 就无条件调用 release()

例如 SimpleChannelInboundHandler 默认会自动释放已经处理的引用计数消息;若还要把消息继续传给下游,需要先正确地 retain()


四、EventLoop 是什么?

NioEventLoopGroup 经常被称为线程池,但这个说法容易让人误解。

它确实实现了 Executor、ExecutorService 和 ScheduledExecutorService 等执行器接口,但它首先是:

一组面向 NIO Selector Channel 的 EventLoop。

官方定义中,NioEventLoopGroup 是用于基于 NIO Selector 的 Channel 的多线程 EventLoopGroup。

结构可以理解为:

NioEventLoopGroup
    ├── NioEventLoop 0
    │      ├── 一个执行线程
    │      ├── 一个 Selector
    │      ├── 一组 Channel
    │      └── 一条任务队列
    │
    ├── NioEventLoop 1
    │      ├── 一个执行线程
    │      ├── 一个 Selector
    │      ├── 一组 Channel
    │      └── 一条任务队列
    │
    └── NioEventLoop 2

NioEventLoop 本身继承自 SingleThreadEventLoop,负责把多个 Channel 注册到 Selector,并在事件循环中完成多路复用。

1. 一个 EventLoop 一个线程

一个 NioEventLoop 的核心执行逻辑由一个线程串行运行:

选择 I/O 事件
    ↓
处理就绪 Channel
    ↓
运行普通任务
    ↓
运行定时任务
    ↓
再次选择

这也是同一条 Channel 上的事件通常能够保持有序执行的重要基础。

2. 一个 EventLoop 多个 Channel

不是:

一个 EventLoop
    ↓
一条 Channel

而通常是:

一个 EventLoop
    ↓
多条 Channel

Netty 的 EventLoop 文档明确说明,一个 Channel 注册后由对应 EventLoop 处理其 I/O,而一个 EventLoop 通常处理多条 Channel。

3. Group 负责选择

Channel 注册时,EventLoopGroup 会从内部选择一个 EventLoop:

Channel A → EventLoop 0
Channel B → EventLoop 1
Channel C → EventLoop 2
Channel D → EventLoop 0

Channel 一旦注册,后续 I/O 通常继续由这个 EventLoop 负责。

4. 任务不会凭空消失

假设创建一个单线程 Group:

NioEventLoopGroup group =
        new NioEventLoopGroup(1);

再提交两个普通任务:

group.execute(taskA);
group.execute(taskB);

两个任务不会因为只有一个线程而导致第二个永远无法执行。

正常情况是:

执行 taskA
    ↓
taskA 返回
    ↓
执行 taskB

只有当第一个任务永久不返回时:

group.execute(() -> {
    while (true) {
        // 永不结束
    }
});

后续任务才会一直排队。

所以真正的问题不是:

单线程不能提交多个任务

而是:

EventLoop 上不能执行长期阻塞或永不结束的任务

5. 不要阻塞 EventLoop

一个 EventLoop 管理很多 Channel。

如果 Handler 中执行:

Thread.sleep(5000);

或者同步查询慢数据库:

当前 Channel 阻塞
    ↓
同一 EventLoop 的其他 Channel
也无法及时处理

因此 EventLoop 更适合执行:

快速读取
协议解码
状态转换
任务分发
快速写入

长时间阻塞业务应交给专门的业务执行器。


五、客户端如何启动?

Netty 客户端使用:

Bootstrap

典型配置包含:

group
channel
handler
option
remoteAddress

官方 Bootstrap 专门用于客户端 Channel,TCP 客户端通过 connect() 发起连接,并返回 ChannelFuture

一个完整客户端如下:

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.nio.charset.StandardCharsets;

public final class NettyClient {

    public static void main(String[] args)
            throws InterruptedException {

        NioEventLoopGroup group =
                new NioEventLoopGroup(1);

        try {
            Bootstrap bootstrap = new Bootstrap();

            bootstrap
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(
                            new ChannelInitializer<SocketChannel>() {
                                @Override
                                protected void initChannel(
                                        SocketChannel channel
                                ) {
                                    channel.pipeline().addLast(
                                            new LineBasedFrameDecoder(8192),
                                            new StringDecoder(
                                                    StandardCharsets.UTF_8
                                            ),
                                            new StringEncoder(
                                                    StandardCharsets.UTF_8
                                            ),
                                            new ClientHandler()
                                    );
                                }
                            }
                    );

            ChannelFuture connectFuture =
                    bootstrap.connect("127.0.0.1", 9090);

            Channel channel =
                    connectFuture.sync().channel();

            channel.writeAndFlush("hello server\n")
                    .sync();

            channel.closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }

    private static final class ClientHandler
            extends SimpleChannelInboundHandler<String> {

        @Override
        public void channelActive(
                ChannelHandlerContext context
        ) {
            System.out.println("client connected");
        }

        @Override
        protected void channelRead0(
                ChannelHandlerContext context,
                String message
        ) {
            System.out.println(
                    "server response: " + message
            );
        }

        @Override
        public void exceptionCaught(
                ChannelHandlerContext context,
                Throwable cause
        ) {
            cause.printStackTrace();
            context.close();
        }
    }
}

1. group

.group(group)

指定新 Channel 应注册到哪个 EventLoopGroup。

2. channel

.channel(NioSocketChannel.class)

指定创建哪一种客户端 Channel。

这里使用的是 Netty 的:

NioSocketChannel

而不是 JDK 原生的:

java.nio.channels.SocketChannel

3. handler

.handler(new ChannelInitializer<SocketChannel>() {
    ...
})

配置客户端 Channel 创建后需要安装的 Handler。

4. connect

bootstrap.connect(host, port)

发起异步连接,立即返回一个 ChannelFuture。官方文档将 Netty Channel I/O 定义为异步操作,调用后通过 ChannelFuture 查询最终成功、失败或取消状态。


六、服务端如何启动?

服务端使用:

ServerBootstrap

典型结构是:

Boss Group
    负责监听 Channel

Worker Group
    负责已连接 Channel

ServerBootstrap.group(parentGroup, childGroup) 分别配置服务端父 Channel 与客户端子 Channel 使用的 EventLoopGroup。

完整服务端如下:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.nio.charset.StandardCharsets;

public final class NettyServer {

    public static void main(String[] args)
            throws InterruptedException {

        NioEventLoopGroup bossGroup =
                new NioEventLoopGroup(1);

        NioEventLoopGroup workerGroup =
                new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap =
                    new ServerBootstrap();

            bootstrap
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(
                            new ChannelInitializer<SocketChannel>() {
                                @Override
                                protected void initChannel(
                                        SocketChannel channel
                                ) {
                                    channel.pipeline().addLast(
                                            new LineBasedFrameDecoder(8192),
                                            new StringDecoder(
                                                    StandardCharsets.UTF_8
                                            ),
                                            new StringEncoder(
                                                    StandardCharsets.UTF_8
                                            ),
                                            new ServerHandler()
                                    );
                                }
                            }
                    );

            ChannelFuture bindFuture =
                    bootstrap.bind(9090);

            Channel serverChannel =
                    bindFuture.sync().channel();

            System.out.println(
                    "server started on port 9090"
            );

            serverChannel.closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully().sync();
            workerGroup.shutdownGracefully().sync();
        }
    }

    private static final class ServerHandler
            extends SimpleChannelInboundHandler<String> {

        @Override
        public void channelActive(
                ChannelHandlerContext context
        ) {
            System.out.println(
                    "connected: "
                            + context.channel().remoteAddress()
            );
        }

        @Override
        protected void channelRead0(
                ChannelHandlerContext context,
                String message
        ) {
            System.out.println(
                    "received: " + message
            );

            context.writeAndFlush(
                    "server received: " + message + '\n'
            );
        }

        @Override
        public void channelInactive(
                ChannelHandlerContext context
        ) {
            System.out.println(
                    "disconnected: "
                            + context.channel().remoteAddress()
            );
        }

        @Override
        public void exceptionCaught(
                ChannelHandlerContext context,
                Throwable cause
        ) {
            cause.printStackTrace();
            context.close();
        }
    }
}

1. parent 与 child

服务端有两类 Channel:

NioServerSocketChannel
    负责监听端口

NioSocketChannel
    表示 accept 得到的客户端连接

因此也有两类配置:

handler
    配置服务端监听 Channel

childHandler
    配置每条已接收客户端 Channel

业务编解码和消息处理通常放在:

childHandler(...)

因为真正承载客户端数据的是 child Channel。ServerBootstrap.childHandler() 官方定义的就是用于已接收 Channel 的处理器。

2. bind 是异步的

ChannelFuture future =
        bootstrap.bind(9090);

返回时,端口绑定操作不一定已经完成。

启动代码中常写:

Channel channel =
        future.sync().channel();

表示主线程等待绑定完成;失败时,sync() 会重新抛出失败原因。

3. closeFuture 不会关闭 Channel

serverChannel.closeFuture().sync();

不是主动关闭服务端。

它表示:

取得“Channel 关闭完成”对应的 Future
    ↓
主线程等待这个 Future 完成

常用于防止 main 方法执行结束后立即进入资源清理流程。


七、Pipeline 如何流动?

每个 Channel 都有一条独立的 ChannelPipeline,并在 Channel 创建时自动创建。Pipeline 是一个 Handler 列表,负责拦截和处理 Channel 的入站事件与出站操作。

例如:

ChannelPipeline
    ├── FrameDecoder
    ├── StringDecoder
    ├── BusinessHandler
    └── StringEncoder

1. 入站事件

入站事件来自底层网络:

Socket 收到数据
    ↓
Channel 可读
    ↓
Netty 读取 ByteBuf
    ↓
Pipeline
    ↓
Inbound Handler

常见入站事件包括:

channelRegistered
channelActive
channelRead
channelReadComplete
channelInactive
exceptionCaught

官方 Pipeline 文档说明,入站事件由入站 Handler 沿管线传播,通常来源于底层 I/O 线程执行的实际读取操作。

2. 出站操作

出站操作由应用主动发起:

context.writeAndFlush(message);

可能经过:

业务对象
    ↓
编码 Handler
    ↓
ByteBuf
    ↓
发送缓冲区
    ↓
Socket 写出

常见出站操作包括:

bind
connect
write
flush
close

官方 Pipeline 文档将出站处理描述为对写请求等出站流量的生成或转换,最终由 Channel 对应的 I/O 线程执行真实写操作。

3. Handler 必须传播事件

使用 ChannelInboundHandlerAdapter 时,如果当前 Handler 不消费事件,通常需要继续传播:

@Override
public void channelRead(
        ChannelHandlerContext context,
        Object message
) {
    inspect(message);

    context.fireChannelRead(message);
}

如果既不处理,也不调用:

context.fireChannelRead(message);

事件就可能在这里终止,后面的 Handler 无法收到。


八、Initializer 为什么消失?

ChannelInitializer 是一个特殊的入站 Handler,用来在 Channel 注册到 EventLoop 时初始化该 Channel 的 Pipeline。

典型写法:

new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(
            SocketChannel channel
    ) {
        channel.pipeline().addLast(
                new StringDecoder(),
                new StringEncoder(),
                new BusinessHandler()
        );
    }
}

执行过程:

Channel 创建
    ↓
ChannelInitializer 加入 Pipeline
    ↓
Channel 注册到 EventLoop
    ↓
调用 initChannel()
    ↓
添加业务 Handler
    ↓
ChannelInitializer 从 Pipeline 移除

官方文档明确说明,initChannel() 会在 Channel 注册时调用,方法返回后,Initializer 会从该 Channel 的 Pipeline 中移除。

所以它不是一个长期处理消息的 Handler。

它更像:

脚手架
安装器
一次性装配器

它的职责是:

组装 Pipeline

而不是:

长期处理 channelRead

九、Sharable 安全吗?

假设这样写:

private static final ServerHandler HANDLER =
        new ServerHandler();

@Override
protected void initChannel(SocketChannel channel) {
    channel.pipeline().addLast(HANDLER);
}

同一个 Handler 实例会被多个 Channel 的 Pipeline 共享。

如果 Handler 没有声明可共享,Netty 通常不允许把同一个实例重复加入多个 Pipeline。

1. 注解的含义

@ChannelHandler.Sharable
public final class ServerHandler
        extends ChannelInboundHandlerAdapter {
}

@Sharable 表示:

同一个 Handler 实例可以安全地被多次加入一个或多个 ChannelPipeline,而不会产生竞态条件。

如果没有这个注解,通常应为每条 Pipeline 创建新的 Handler 实例。

2. 注解不会制造线程安全

下面的代码即使加了 @Sharable,仍然可能错误:

@ChannelHandler.Sharable
public final class CounterHandler
        extends ChannelInboundHandlerAdapter {

    private int count;

    @Override
    public void channelRead(
            ChannelHandlerContext context,
            Object message
    ) {
        count++;
        context.fireChannelRead(message);
    }
}

多个连接可能由不同 EventLoop 线程处理,共享修改同一个 count

@Sharable 只是声明:

这个类的设计允许共享

它不会自动:

增加锁
复制成员变量
创建线程本地副本
修复竞态条件

官方文档也明确把 @Sharable 定义为一种说明性注解:开发者仍需保证同一个实例在多个 Pipeline 中复用时不会产生竞态。

3. 两种选择

无连接状态的 Handler 可以共享:

@ChannelHandler.Sharable
public final class LoggingHandler
        extends ChannelInboundHandlerAdapter {
}

有连接独立状态的 Handler,通常每个 Channel 创建一个:

@Override
protected void initChannel(SocketChannel channel) {
    channel.pipeline().addLast(
            new SessionHandler()
    );
}

也可以把连接状态放在:

Channel Attribute
ChannelHandlerContext Attribute

中,而不是放在共享 Handler 的普通成员变量中。

4. Initializer 是可共享的

ChannelInitializer 自身标注了 @Sharable,Netty 内部也按同一个 Initializer 可能被 Bootstrap 中的多个 Channel 复用来设计。

但这不代表可以随意在自定义 Initializer 中保存非线程安全的共享状态。

Initializer 通常应保持无状态,只负责向当前 Channel 添加 Handler。


十、Future 表示什么?

Netty 的 I/O 操作通常立即返回:

ChannelFuture future =
        channel.writeAndFlush(message);

立即返回不代表操作已经成功完成。

Future 可能处于:

未完成
成功
失败
取消

状态。

Netty 官方文档明确说明,Channel I/O 是异步的,调用返回时不能保证操作已经完成,结果通过 ChannelFuture 表达。

1. sync

future.sync();

表示:

阻塞当前调用线程
    ↓
等待 Future 完成
    ↓
失败时重新抛出原因

它不是把 Netty 的底层 I/O 改成同步 I/O。

只是:

异步操作
+
调用方选择同步等待结果

2. listener

事件驱动写法是:

future.addListener(result -> {
    if (result.isSuccess()) {
        System.out.println("operation success");
    } else {
        result.cause().printStackTrace();
    }
});

官方文档建议,在可能的情况下优先使用 Listener,因为 Listener 不会阻塞等待线程。

3. Handler 中别 sync

下面的写法很危险:

@Override
public void channelRead(
        ChannelHandlerContext context,
        Object message
) throws Exception {

    context.writeAndFlush(message).sync();
}

channelRead() 通常由 EventLoop 线程调用。

如果 EventLoop 线程阻塞等待一个需要自己继续推进才能完成的操作,就可能形成死锁。Netty 官方文档明确警告,不应在 ChannelHandler 的 I/O 线程中调用 await() 一类阻塞等待方法,并可能通过 BlockingOperationException 阻止这种行为。

正确方式是:

context.writeAndFlush(message)
        .addListener(future -> {
            if (!future.isSuccess()) {
                future.cause().printStackTrace();
                context.close();
            }
        });

4. 写成功不等于业务成功

writeAndFlush(message).sync();

只能说明对应 Netty 写操作已经完成或失败。

它不等于:

对方业务代码已经处理
数据库已经提交
远端已经返回业务结果

如果应用需要确认远端处理成功,必须在应用协议中设计:

请求 ID
响应消息
确认消息
超时
重试
幂等

十一、服务端内部做了什么?

表面上,我们只写了:

bootstrap.bind(9090);

内部却经历了多个阶段。

可以简化为:

bind()
    ↓
创建 NioServerSocketChannel
    ↓
初始化 ServerChannel
    ↓
安装服务端 Handler
    ↓
注册到 Boss EventLoopGroup
    ↓
绑定端口
    ↓
开始接收连接

AbstractBootstrap.register()bind() 都会进入 Channel 创建、初始化和注册流程;官方源码中的 initAndRegister() 正是这条链路的核心入口。

1. 安装 Acceptor

ServerBootstrap 初始化监听 Channel 时,会向它的 Pipeline 安装内部的:

ServerBootstrapAcceptor

它负责处理 Boss 接收到的新 child Channel。

2. 接收 child Channel

当服务端 accept 到客户端连接时,内部 Acceptor 获得一个新的 child Channel:

NioSocketChannel

随后执行:

把 childHandler 加入 child Pipeline
设置 childOption
设置 childAttr
注册到 childGroup

当前 Netty 4.1 源码中的 ServerBootstrapAcceptor.channelRead() 正是先把 childHandler 加到 child Pipeline,再调用 childGroup.register(child);注册失败时会强制关闭该 child Channel。

这和我们上一篇手写的 Boss/Worker 模型完全对应:

Boss accept
    ↓
获得客户端 Channel
    ↓
安装业务 Handler
    ↓
交给 Worker Group
    ↓
选择 Worker EventLoop
    ↓
注册并处理后续 I/O

所以 Netty 没有绕开 Java NIO 的核心模型。

它把这套容易写错的流程封装成了:

bootstrap
        .group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .childHandler(initializer);

十二、客户端内部做了什么?

客户端代码:

bootstrap.connect(host, port);

可以简化为:

创建 NioSocketChannel
    ↓
安装 bootstrap.handler()
    ↓
配置 ChannelOption
    ↓
注册到 EventLoopGroup
    ↓
在 EventLoop 中执行 connect
    ↓
连接完成后更新 ChannelFuture

Netty 的客户端 Bootstrap 源码会把配置的 Handler 加入新 Channel 的 Pipeline,然后设置 ChannelOption 和属性;真正的连接操作由 Channel 所属 EventLoop 执行。

因此,客户端同样不是:

主线程直接阻塞执行 connect

而是:

主线程提交连接请求
    ↓
EventLoop 推进连接状态
    ↓
ChannelFuture 通知结果

十三、Netty 没解决什么?

Netty 解决了大量网络工程问题,但它不是万能的。

1. 不会自动定义消息边界

TCP 只提供字节流。

一次写入:

hello

对端可能分两次读取:

hel
lo

两次写入:

hello
world

对端也可能一次读到:

helloworld

所以仍然需要:

长度字段
分隔符
固定长度
协议头

等帧协议。

示例代码中加入:

new LineBasedFrameDecoder(8192)

就是用换行符定义消息边界。

2. 不会自动让业务变快

EventLoop 只负责高效调度 I/O。

数据库慢、远程服务慢、磁盘慢,Netty 不会让它们自动变快。

3. 不会自动处理背压

发送速度超过网络速度时,数据会积累在:

业务队列
Channel 出站缓冲区
内核发送缓冲区

应用仍需根据:

Channel 是否可写
队列长度
连接限流
请求超时

设计流量控制。

4. 不会自动保证 Handler 安全

Handler 是否能共享、状态放在哪里、是否阻塞 EventLoop,仍由开发者负责。

5. 不会自动管理业务线程

可以把业务任务提交给其他 Executor,但线程数量、队列大小、拒绝策略和上下文传播仍需设计。


十四、常见误区

1. Netty 就是 Selector 的包装

不完整。

它同时封装了 EventLoop、线程归属、任务调度、Channel 生命周期、Pipeline、Handler、ByteBuf 和异步结果。

2. NioEventLoopGroup 就是普通线程池

不准确。

它是一组面向 Channel 和 Selector 的 EventLoop,同时实现了执行器接口。

3. 一个 EventLoop 只处理一个 Channel

错误。

一个 EventLoop 通常处理多条 Channel。

4. EventLoopGroup 线程越多越好

错误。

线程过多会增加调度和上下文切换成本,还可能破坏缓存局部性。线程数应根据连接活跃度、每次事件处理成本和 CPU 核心数测量。

5. ByteBuf 不需要索引

错误。

它不是没有索引,而是把原来的单一位置状态拆成了 readerIndexwriterIndex

6. ByteBuf.clear() 会清空数据

错误。

它只重置读写索引。

7. read 和 get 完全一样

错误。

read 会推进 readerIndexget 不会。

8. 默认 ByteBuf 一定是堆外的

错误。

具体内存类型取决于 Allocator 实现。

9. ByteBuf 一定池化

错误。

Netty 同时提供池化和非池化 Allocator。

10. ByteBuf 由 GC 自动管理即可

错误。

很多 ByteBuf 使用引用计数;在需要手工负责生命周期的场景中,必须正确 retain 和 release。

11. Bootstrap 是客户端连接本身

错误。

Bootstrap 是启动和配置辅助对象,真正的连接由 Channel 表示。

12. ServerBootstrap 只创建一个 Channel

错误。

它先创建监听 ServerChannel,随后每 accept 一个连接,还会产生一个 child Channel。

13. handler 与 childHandler 一样

错误。

handler 作用于服务端监听 Channel,childHandler 作用于被接收的客户端 Channel。

14. ChannelInitializer 会处理每条消息

错误。

它在 Channel 初始化阶段添加 Handler,完成后会从 Pipeline 移除。

15. ChannelInitializer 每个连接都要手工 new

不一定。

它本身标注为 @Sharable,通常可以被 Bootstrap 复用,但自定义实现必须保持共享安全。

16. 加上 @Sharable 就线程安全

错误。

它只是声明该 Handler 可以安全共享,不会自动修复成员变量竞态。

17. sync 会把 Netty 变成 BIO

错误。

底层操作仍然是异步的,只是调用线程选择阻塞等待 Future。

18. Handler 中可以随便 sync

错误。

EventLoop 线程中阻塞等待可能导致死锁或触发 BlockingOperationException

19. writeAndFlush 成功代表对端处理成功

错误。

它不等于远端业务确认。业务成功必须由应用层响应协议表达。

20. Pipeline 是所有连接共享的

错误。

每个 Channel 都有自己的 Pipeline。


总结

Java NIO 提供:

Channel
ByteBuffer
Selector

但完整网络框架还需要解决:

线程归属
事件循环
跨线程任务
缓冲区管理
协议处理链
异步结果
连接生命周期

Netty 将这些能力重新组织为:

Bootstrap
    负责创建和启动 Channel

EventLoopGroup
    管理一组 EventLoop

EventLoop
    使用单线程驱动 Selector、Channel 和任务队列

Channel
    表示监听通道或网络连接

ChannelPipeline
    保存一条 Channel 的处理链

ChannelHandler
    处理入站事件和出站操作

ByteBuf
    承载网络字节

ChannelFuture
    表示异步 I/O 的最终结果

ByteBuf 使用:

readerIndex
writerIndex

分离读写位置,因此常规读写不需要 flip()

EventLoopGroup 的结构是:

一组 EventLoop
    ↓
每个 EventLoop 一个线程
    ↓
每个 EventLoop 管理多条 Channel

客户端启动流程是:

Bootstrap
    ↓
创建 NioSocketChannel
    ↓
安装 Handler
    ↓
注册到 EventLoop
    ↓
异步 connect

服务端启动流程是:

ServerBootstrap
    ↓
创建 NioServerSocketChannel
    ↓
注册到 Boss Group
    ↓
bind
    ↓
accept child Channel
    ↓
安装 childHandler
    ↓
注册到 Worker Group

Pipeline 则把网络事件转换成可组合的处理链:

网络字节
    ↓
解帧
    ↓
解码
    ↓
业务处理
    ↓
编码
    ↓
网络写出

ChannelInitializer 只负责初始化 Pipeline,完成后自动移除。

@Sharable 只是一份共享安全声明,不会自动产生线程安全。

ChannelFuture 让网络操作保持异步:

提交操作
    ↓
立即得到 Future
    ↓
完成后 Listener 通知

sync() 只是让当前调用线程等待结果,不会改变 Netty 底层的异步事件驱动模型。

最后,可以用一句话概括 Netty 的核心价值:

Netty 没有取代 Java NIO,而是把 Channel、Selector 和 Buffer 组织成了 EventLoop、Pipeline、Handler 与 Future,让连接管理从一堆系统调用,变成一套可组合、可扩展的网络编程模型。

0

评论区