Skip to content

架构与事件驱动

1. 这是什么

如果只用一句话解释 Netty,可以这样记:

  • Netty 是一个基于事件驱动模型的网络编程框架
  • 它帮你把“收连接、读数据、写数据、编解码、异常处理”这一整套事情组织起来

很多人第一次接触 Netty,会被 ChannelEventLoopPipelineHandler 这些名词劝退。 其实先别急着背 API,先建立一个直觉:

Netty 像一个高效的快递分拣中心。

  • 连接进来,先登记到哪个通道(Channel
  • 每个通道上的消息,不是随便找线程处理,而是交给固定的事件循环(EventLoop
  • 数据在处理中会经过一道道工序(Pipeline
  • 每一道工序由一个处理器(Handler)负责

这样你就不会把 Netty 只看成“Socket 的高级封装”,而会把它看成一套高并发网络事件处理框架


2. 为什么重要

不理解 Netty 的事件驱动,就会出现三个常见问题:

  • 只会照着样例写服务端,但不知道为什么这么写
  • 遇到线程问题、粘包拆包、性能瓶颈时无从下手
  • 把耗时业务直接塞进 I/O 线程,结果把整个服务拖慢

Netty 之所以常见于:

  • RPC 框架
  • IM 即时通讯
  • 网关
  • 游戏服务器
  • 自定义二进制协议服务

不是因为它“语法高级”,而是因为它的模型适合处理:

  • 大量连接
  • 频繁网络事件
  • 低延迟读写
  • 可插拔的处理链

所以这一章的真正目标不是记住类名,而是理解:

为什么一个线程能管很多连接,为什么 Netty 不喜欢阻塞,为什么 Handler 要走责任链。


3. 先建立最核心的直觉

3.1 传统阻塞模型的问题

传统 BIO 模型通常是:

  • 一个连接一个线程
  • 线程阻塞等待数据
  • 连接数一多,线程数就上来

它的问题不是“不能用”,而是:

  • 线程切换成本高
  • 内存消耗大
  • 高并发连接下不经济

比如你有 1 万个连接:

  • 如果一连接一线程,线程数量会非常夸张
  • 但这些连接大部分时间可能都在“等消息”

也就是说,大量线程其实在空等

3.2 事件驱动模型在解决什么

事件驱动模型的核心思路是:

  • 不让每个连接独占一个线程
  • 而是由少量线程轮询并处理多个连接上的事件

这些事件包括:

  • 连接建立
  • 收到数据
  • 数据可写
  • 连接关闭
  • 异常发生

于是线程不再“傻等”,而是:

  • 有事件就处理
  • 没事件就继续监听别的连接

这就是 Netty 高效的基础之一。


4. Netty 的核心角色

4.1 Channel:连接的抽象

Channel 可以理解成:

  • 一条网络连接
  • 或者一个 I/O 通道

你可以把它想成“一个客户端连接在服务端的代表对象”。

比如:

  • 一个浏览器连进来
  • 一个移动端连进来
  • 一个下游服务连进来

在 Netty 里,通常都会对应一个 Channel

你对连接的大部分操作,最终都绕不开 Channel

  • 关闭
  • 绑定属性

4.2 EventLoop:事件循环

EventLoop 是 Netty 最核心的概念之一。

它负责:

  • 监听 I/O 事件
  • 分发事件
  • 执行普通任务
  • 执行定时任务

可以先记住一句最重要的话:

一个 Channel 在生命周期内,通常绑定到一个固定的 EventLoop

这带来的好处是:

  • 同一个连接上的事件,通常由同一个线程处理
  • 能减少并发竞争
  • 能简化线程安全问题

4.3 EventLoopGroup:事件循环组

一个 EventLoop 不够时,就需要一组。

EventLoopGroup 可以理解成:

  • 多个 EventLoop 的集合
  • 负责管理一批事件循环线程

在服务端里,常见会有两组:

  • bossGroup:负责接收新连接
  • workerGroup:负责处理已建立连接的读写事件

你可以把它理解成:

  • 前台接待(接新连接)
  • 后台工人(处理连接上的业务 I/O)

4.4 Pipeline:处理链

Netty 不会把“解码、鉴权、业务处理、编码”都写在一个大方法里。

它采用的是责任链模式:

  • 一个 Channel 对应一个 ChannelPipeline
  • Pipeline 里可以挂多个 Handler
  • 数据来了,按顺序经过这些处理器

比如一个典型链路:

  1. LoggingHandler
  2. LengthFieldBasedFrameDecoder
  3. StringDecoder
  4. 登录校验处理器
  5. 业务处理器
  6. StringEncoder

这样做的优点是:

  • 职责清晰
  • 可插拔
  • 容易复用
  • 容易排查问题

4.5 Handler:真正干活的处理器

Handler 是你最常写的代码位置。

它通常负责:

  • 处理连接建立/断开
  • 处理收到的消息
  • 处理异常
  • 写回响应

最常见的方法例如:

  • channelActive
  • channelRead
  • channelInactive
  • exceptionCaught

5. 一张脑图式理解

可以用下面这条链路快速记忆:

text
客户端连接进来

bossGroup 接收连接

把连接注册到 workerGroup 某个 EventLoop

该连接后续读写事件交给这个 EventLoop

收到数据后进入 ChannelPipeline

依次经过多个 Handler 处理

业务处理完成后回写响应

把这条链路记住,后面学 ByteBufPipeline、线程模型就不会散。


6. 最小可运行示例

下面给一个最小版 Netty 服务端,让你先看到这些角色是怎么出现的。

java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class SimpleNettyServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast(new StringDecoder(CharsetUtil.UTF_8))
                                    .addLast(new StringEncoder(CharsetUtil.UTF_8))
                                    .addLast(new SimpleServerHandler());
                        }
                    });

            ChannelFuture future = bootstrap.bind(8080).sync();
            System.out.println("Netty server started on port 8080");
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

配套的 Handler:

java
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class SimpleServerHandler extends SimpleChannelInboundHandler<String> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        System.out.println("客户端连接建立: " + ctx.channel().remoteAddress());
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        System.out.println("收到消息: " + msg);
        ctx.writeAndFlush("服务端已收到: " + msg + "\n");
    }

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

6.1 这个示例里每个对象对应什么

  • ServerBootstrap:启动器,负责把各个组件组装起来
  • bossGroup:负责接收连接
  • workerGroup:负责处理连接上的读写事件
  • NioServerSocketChannel:服务端监听通道
  • ChannelInitializer:给每个新连接初始化处理链
  • pipeline():获得当前连接的处理链
  • SimpleServerHandler:你的业务逻辑入口

7. 动手验证

如果你本地有 Maven 项目,可以按这个顺序做一遍。

7.1 添加依赖

xml
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.108.Final</version>
</dependency>

7.2 启动服务端

运行 SimpleNettyServer,看到:

text
Netty server started on port 8080

7.3 用 telnet 或 nc 测试

bash
nc 127.0.0.1 8080

连接成功后输入:

text
hello netty

你应该能在服务端看到:

text
收到消息: hello netty

客户端会收到:

text
服务端已收到: hello netty

7.4 你要重点观察什么

第一次跑通时,不要急着改业务,先观察:

  • 连接建立时是否进入 channelActive
  • 收消息时是否进入 channelRead0
  • 回写是否成功
  • 异常时是否进入 exceptionCaught

这一步是帮你把“事件驱动”从抽象概念变成肉眼可见的运行过程。


8. 为什么一个 EventLoop 能管理多个连接

这是面试和实战都很常问的问题。

原因是:

  • 它不是死盯一个连接阻塞等待
  • 它是基于多路复用机制监听很多连接的事件
  • 哪个连接有事件,就处理哪个

所以高效的关键不在于“线程更快”,而在于:

  • 线程不被大量空等待浪费掉
  • 连接和线程不是 1 对 1 关系

但注意:

  • 一个线程能管很多连接
  • 不代表你的业务代码可以随便写阻塞操作

如果你在 I/O 线程里直接:

  • 查慢 SQL
  • 远程调用别的服务
  • Thread.sleep
  • 做复杂计算

那这个线程就被你卡住了,它负责的其他连接也会受影响。


9. 最容易踩的坑

9.1 把 Netty 当成“高级 Socket API”

这会导致你只盯着“怎么发消息”,却忽略:

  • 线程模型
  • 解码边界
  • Handler 职责划分

9.2 在 Handler 里写耗时逻辑

这是最常见的问题之一。

错误写法的后果:

  • I/O 线程被阻塞
  • 延迟升高
  • 吞吐下降
  • 心跳超时

正确做法通常是:

  • I/O 线程只做轻量处理
  • 耗时逻辑丢到业务线程池

9.3 处理链职责不清

比如把这些都塞进一个 Handler:

  • 拆包
  • 解码
  • 鉴权
  • 路由
  • 业务处理
  • 回包

这样后期会越来越难维护。

9.4 不理解事件传播方向

Netty 里有:

  • inbound 事件
  • outbound 事件

如果这个方向感没建立起来,后面学编解码和写出链路时会比较乱。


10. 一套适合初学者的学习顺序

建议按下面顺序学,不容易散:

  1. 先理解这一章的整体架构和事件驱动
  2. 再学 ChannelEventLoopPipeline 的关系
  3. 再学 ByteBuf
  4. 再学粘包拆包
  5. 再学线程模型与性能优化
  6. 最后自己写一个简单协议服务

如果反过来一上来就背 API,通常会越学越碎。


11. 练习建议

练习 1:画架构图

把下面几个角色画出来并标注职责:

  • bossGroup
  • workerGroup
  • EventLoop
  • Channel
  • Pipeline
  • Handler

练习 2:改造最小示例

把上面的示例改成:

  • 客户端发 ping
  • 服务端回 pong

练习 3:模拟错误用法

故意在 channelRead0 里加一段耗时逻辑:

java
Thread.sleep(5000);

然后观察:

  • 响应是否变慢
  • 多个客户端时是否互相影响

这能帮你真正理解“不要阻塞 I/O 线程”不是口号,而是模型要求。


12. 自测问题

  • Netty 为什么比“一连接一线程”更适合高并发连接场景?
  • ChannelEventLoopPipeline 分别是什么?
  • 为什么一个连接通常绑定一个固定的 EventLoop
  • 为什么 I/O 线程里不适合做慢 SQL 和远程调用?
  • bossGroupworkerGroup 的职责差异是什么?

13. 这一章你至少要带走什么

如果你看完这一章只记住 3 件事,也够了:

  1. Netty 的核心不是 API,而是事件驱动模型
  2. 连接不再和线程 1 对 1 绑定,而是由少量 EventLoop 管理大量连接
  3. 业务处理要尊重线程模型,别把耗时操作直接塞进 I/O 线程

后面再学 ByteBufPipeline、线程模型时,你就会发现这些知识是串起来的。