Netty 速通 · 新手友好版

Netty 速通

一份写给新手的极简指南:用最少的术语、最直白的例子,搞懂 Netty 到底在干什么、怎么写、以及千万别踩的坑。

异步事件驱动 高性能网络框架 基于 Java NIO 服务端 / 客户端通用

1Netty 是什么?

先有个整体印象,再抠细节。

Netty 是一个基于 Java NIO异步、事件驱动的网络应用框架。说人话就是:

  • 它帮你用 Java 写网络通信程序(服务器 / 客户端);
  • 它把底层难用的 NIO(Selector、Channel、Buffer)包成了简单好用的 API;
  • 它天生高性能、高并发,很多著名项目都用它(Dubbo、RocketMQ、Elasticsearch、ZooKeeper…)。
一句话记忆:Netty = 把 Java NIO 的"脏活累活"包起来,让你专心写业务逻辑的网络框架。

2为什么不直接用 Java NIO?

不是不能,是太痛苦。

原生 Java NIONetty
API 繁琐,Selector / 附件管理很容易写错封装成 Channel + Pipeline,心智负担低
要自己处理粘包 / 拆包内置一大堆 FrameDecoder,开箱即用
断连、异常、半包要自己兜底事件回调清晰(channelActive / exceptionCaught…)
ByteBuffer 的 flip() 反人类ByteBuf 读写双指针,不用 flip
线程模型自己搭成熟的 Reactor 主从多线程模型
注意:"异步"不是说网络变快了,而是一个线程能同时管很多连接。Netty 的"快"来自:少量线程 + 非阻塞 I/O + 零拷贝,而不是魔法。

36 个核心概念(必背)

这是全文重点。把下面 6 个词记住,Netty 你就懂一半了。

Channel

一条网络连接(类似 Socket)。读写数据都通过它。客户端和服务端各持有一个。

EventLoop

"事件循环线程"。一个 EventLoop 不断轮询 I/O 事件并执行任务。连接会被绑定到固定的 EventLoop。

ChannelFuture

异步操作的结果载体。Netty 里大部分操作不立即返回结果,而是返回 Future,用 addListener 拿结果。

ChannelHandler

真正写业务逻辑的地方。入站事件用 ChannelInboundHandler,出站用 ChannelOutboundHandler

ChannelPipeline

Handler 组成的责任链。数据进来/出去会依次流经每个 Handler,像流水线。

ByteBuf

Netty 的数据容器,替代 NIO 的 ByteBuffer。读写指针分离,用起来舒服得多。

它们怎么串起来?

① 结构:谁管谁 Channel 网络连接 绑定到 EventLoop 事件循环线程 (负责轮询 I/O 事件) ② 数据怎么流动 ByteBuf 数据容器 写入 / 读出 ChannelPipeline Handler 责任链 (流水线,有序排列) 依次经过 Handler 解码器 Handler 业务处理 Handler 编码器 Channel 绑定到 EventLoop, 数据用 ByteBuf 承载, 沿 Pipeline 上各 Handler 依次流转

Channel 绑定 EventLoop,数据用 ByteBuf 装,沿 Pipeline 上的 Handler 流转

4线程模型:Reactor

不用背名词,看图就懂。Netty 用的是"主从 Reactor 多线程"。

Boss (Main) 接收连接 Worker-1 Worker-2 Worker-N… 连接 A 连接 B 连接 C
  • Boss 线程(1 个或几个):只负责accept 新连接,拿到连接就甩给 Worker。
  • Worker 线程(一组):负责已连接通道的读写、编解码、业务处理
  • 每个连接固定绑定一个 Worker 线程 → 无锁串行,既快又不容易出并发 bug。
重点:因为"一个连接只由一个 Worker 线程处理",所以在 Handler 里不要做耗时操作,否则会卡住这个线程上的所有连接(详见第 9 节)。

5第一个 Netty 服务端

照着抄一遍,跑通就有感觉了。

// 服务端启动:绑定端口、配置线程、装配 Pipeline
public class ServerDemo {
    public static void main(String[] args) throws Exception {
        EventLoopGroup boss = new NioEventLoopGroup(1);      // 接收连接
        EventLoopGroup worker = new NioEventLoopGroup();     // 处理连接

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(boss, worker)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 protected void initChannel(SocketChannel ch) {
                     ch.pipeline().addLast(new StringDecoder());  // 字节→字符串
                     ch.pipeline().addLast(new StringEncoder());  // 字符串→字节
                     ch.pipeline().addLast(new MyServerHandler());
                 }
             });
            ChannelFuture f = b.bind(8080).sync();   // 绑定端口,阻塞等待
            System.out.println("服务端启动,端口 8080");
            f.channel().closeFuture().sync();          // 等待关闭
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
// 业务 Handler:收到消息 → 打印 → 回一句
class MyServerHandler extends ChannelInboundHandlerAdapter {
    // 客户端发来消息时触发
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String text = (String) msg;
        System.out.println("收到:" + text);
        ctx.writeAndFlush("服务器说:收到啦!\n");
    }

    // 连接建立时触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush("欢迎连接 Netty 服务端!\n");
    }

    // 出异常一定要处理,否则连接会"悄悄"泄漏
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) {
        e.printStackTrace();
        ctx.close();
    }
}
记忆点:ServerBootstrap 管服务端、Bootstrap 管客户端;group(boss, worker) 就是第 4 节那个主从线程模型。

6第一个 Netty 客户端

和服务端几乎一样,只是换成 Bootstrap、只配一个线程组。

public class ClientDemo {
    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .handler(new ChannelInitializer<SocketChannel>() {
                 protected void initChannel(SocketChannel ch) {
                     ch.pipeline().addLast(new StringDecoder());
                     ch.pipeline().addLast(new StringEncoder());
                     ch.pipeline().addLast(new MyClientHandler());
                 }
             });
            ChannelFuture f = b.connect("localhost", 8080).sync();
            f.channel().writeAndFlush("你好,Netty!\n");  // 发一句话
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}
跑通验证:先运行服务端,再运行客户端,控制台应分别看到"收到:你好,Netty!"和服务器回的欢迎语。这就证明整条链路通了。

7编解码器(Codec):解决粘包 / 拆包

新手第二个最容易翻车的地方。TCP 是"流",没有消息边界。

TCP 会把你的数据随意拼接或拆分后发送。比如你发了两条 "A\n" "B\n",对方可能一次收到 "AB\n"(粘包),也可能先收到 "A" 再收到 "B\n"(拆包)。

错误做法:直接按"一次 read 就是一条消息"来写 → 数据错乱、解析崩溃。

正确做法:用 Netty 内置的"按边界切包"解码器,放在 Pipeline 最前面:

解码器切包规则适用场景
LineBasedFrameDecoder\n\r\n 为一条消息边界文本行协议(上面例子用这个最省事)
DelimiterBasedFrameDecoder自定义分隔符有特殊结尾符的协议
LengthFieldBasedFrameDecoder消息头里带长度字段二进制协议(最常用、最严谨)
口诀:切包(FrameDecoder)再转换(StringDecoder / 自定义 Decoder),顺序不能反,切包器必须排在最前面。
// 正确的 Pipeline 顺序示例
pipeline.addLast(new LineBasedFrameDecoder(1024)); // ① 先按行切包(限长 1024 防攻击)
pipeline.addLast(new StringDecoder());              // ② 字节转字符串
pipeline.addLast(new MyServerHandler());            // ③ 业务处理

8ByteBuf 速记

比 NIO 的 ByteBuffer 好用在哪?看一眼就懂。

readerIndex(读指针) writerIndex(写指针) 已读区 可读待处理区(写了多少算多少)
  • 两个指针readerIndex(读到哪)、writerIndex(写到哪)。读写互不影响,不用 flip()
  • 自动扩容:写超了容量它会自己长大,不像 ByteBuffer 要手动 allocate。
  • 池化 + 引用计数:高性能,但也意味着用完了要释放(下一节讲)。
一句话:记住"读指针 / 写指针"两个词,ByteBuf 就过关了。写数据往后挪写指针,读数据往后挪读指针,永不需要 flip。

9新手最常踩的 4 个坑

看完这节,你能避开 80% 的 Netty 线上事故。

坑 1:在 Handler 里做耗时 / 阻塞操作
数据库查询、HTTP 调用、sleep 都算。这会卡死整个 Worker 线程,同线程的所有连接都跟着卡。
✅ 解决:把耗时任务交给独立的业务线程池(EventExecutorGroup 或自己 Executor),Handler 里只做 I/O。
坑 2:ByteBuf 没释放,内存泄漏
自己 alloc().buffer() 出来的 ByteBuf 用完后要 release();在 channelRead 里如果不继续往下传也要释放。
✅ 解决:用 SimpleChannelInboundHandler 代替 ChannelInboundHandlerAdapter,它会在读完自动释放。
坑 3:忘记处理粘包
没加 FrameDecoder 就直接按消息解析,数据一多就乱套(见第 7 节)。
坑 4:exceptionCaught 空着不写
异常不处理,连接不会正常关闭,时间长了一堆"僵尸连接"占着资源。
✅ 解决:至少打日志 + ctx.close()

10给你的学习路径建议

别一上来就啃源码,按这个顺序最稳。

  1. 先跑通第 5、6 节的 Echo 例子,建立"连接 → Pipeline → Handler"的直觉。
  2. 搞懂线程模型(第 4 节)和 6 个核心概念(第 3 节)——这是骨架。
  3. 吃透编解码(第 7 节),写一个"自定义协议"(长度 + 内容)的服务端。
  4. 练习心跳机制IdleStateHandler 检测空闲连接并关闭。
  5. 再深入:零拷贝(FileRegion / CompositeByteBuf)、内存池、源码里的 Reactor 实现。
最后一句:Netty 不难,难的是"异步思维"的切换。多写几个例子,把"我调用就能立刻拿到结果"换成"我注册回调,结果好了再通知我",就通了。

11Epoll 是什么?(Linux 高性能的秘密)

你在第 4 节看到的"少量线程管海量连接",底层靠的就是它。

Epoll 是 Linux 内核提供的 I/O 多路复用机制,让一个线程能同时监听成千上万个连接(文件描述符 fd),谁"可读/可写"了就通知你去处理。它是 select / poll 的升级版。

select / poll(旧) epoll(新) 用户线程调用 select 每次传入全部 fd 集合 内核遍历全部 N 个 fd 逐个检查是否就绪 返回就绪数量 并回拷整个 fd 集合 fd 注册到 epoll 只需注册一次 就绪时内核回调 fd 加入就绪链表 epoll_wait 直接取 仅返回就绪的 fd 时间复杂度 O(N):连接越多越慢 时间复杂度 O(1):与连接数无关
  • select / poll:每次调用都要把"全部 fd 集合"从用户态拷进内核,内核遍历所有 fd找就绪的 → 复杂度 O(N),连接越多越慢。
  • epoll:fd 只注册一次,内核通过回调把就绪的 fd 放进"就绪链表",epoll_wait 直接拿就绪列表 → 复杂度 O(1),与总连接数无关。
触发模式:水平触发 LT(默认,只要还能读就一直通知,安全);边缘触发 ET(只在状态变化时通知一次,更高效,Netty 即用 ET,但必须一次读完)。
和 Netty 的关系NioEventLoopGroup 底层用 Java NIO 的 Selector,Linux 上其实就是 epoll;想更极致可用 Netty 原生的 EpollEventLoopGroup(额外支持 SO_REUSEPORT)。换平台 Netty 自动切换,业务代码不动。

12TCP_NODELAY:实时服务为何要关掉 Nagle

TCP_NODELAY = true 禁用 Nagle 算法,让小数据立即发出,降低延迟。

Nagle 算法(默认开启)的初衷是省带宽:应用频繁写小数据时,OS 不会马上发,而是暂存起来,等攒够一个 MSS 或收到对端 ACK 才发,从而把多个小包合并成一个大包

Nagle 开启(默认) TCP_NODELAY = true 应用写小数据 "Hi" OS 收到待发送 OS 暂存,等 ACK 顺带合并后续小包 收到 ACK 后才发出 数据已在外等待 应用写 "Hi" 立即发出,不等 应用写 "World" 也立即发出 每个小包各自送达 不合并 延迟 ≈ 1 个 RTT,最高 40ms+ 延迟最低,但包数量变多
坑:Nagle + 延迟 ACK:发送方等 ACK 才发下一个小包,而接收方开"延迟 ACK"(默认攒 ~40ms 再回),互相干等 → 最多卡 40ms+。聊天 / 游戏 / RPC 这类"小消息要即时响应"的场景体感发涩。
在 Netty 里这样用:用 .childOption(ChannelOption.TCP_NODELAY, true)(注意是 childOption,对每条连接生效,不是 option)。实时 / 交互型服务几乎无脑加这句;仅"大文件传输、批量吞吐优先"才保留 Nagle。

13绑定端口到底干了什么?

概念上 bind 和 listen 是两步,但在 Netty / Java 里被合成一步了。

一部服务端套接字要经历四个阶段:socket()bind()listen()accept()

服务端套接字的四个阶段 socket() 创建套接字 bind(端口) 绑定本地地址 listen() 进入监听态 accept() 取连接处理 分配文件描述符 端口被本进程占用 内核维护连接队列 交给 worker 线程 概念上 bind(绑定端口)和 listen(开始监听)是两步: bind = 把套接字和某个本地端口 / IP 关联,端口被本进程「占住」; 但在 Netty / Java NIO 中,bind(8080) 一步把两者都做了 — 调用返回即已开始监听。
  • bind(端口):把套接字和某个本地端口 + IP 关联,端口被本进程「占住」(别人一般绑不了同一个)。
  • listen():进入被动监听态,内核为它维护"待处理连接队列"(backlog),开始接客。
  • accept():从队列取出已完成三次握手的连接,交给 Worker(对应第 4 节 Boss → Worker)。
关键点:Java NIO 的 ServerSocketChannel 本身就是"监听型"套接字,bind() 完自动进入监听态;Netty 的 bind(8080) 一步把 bind + listen 都做了,sync() 返回即已在 8080 监听、可接收连接。你无法、也不用再单独调 listen()
补充bind(0) 让 OS 随机挑空闲端口;backlog 可用 .option(ChannelOption.SO_BACKLOG, 128) 设置,控制瞬间大量连接时内核能排多少。
为新手而写 · 一份 Netty 速通笔记 · 建议配合官方示例动手敲一遍

评论
公告
Welcome to my Blog!Have fun!
最新文章
网站信息
文章数目 :
47
本站总字数 :
126.8k
本站访客数 :
本站总浏览量 :
最后更新时间 :