Netty源码分析(六)--关于ChannelPipeline

前言

本章将分析Netty中的重要对象ChannelPipeline,探索其生命周期中的一些重难点。

ChannelPipeline添加ChannelHandler

添加handler的核心方法如下,主要分为四个步骤

1.检查是否有重复的Handler。

2.创建节点。

3.添加节点。

4.回调用户方法。

scss 复制代码
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
    final AbstractChannelHandlerContext newCtx;
    synchronized (this) {
        checkMultiplicity(handler);
        newCtx = newContext(group, filterName(name, handler), handler);
        addLast0(newCtx);
        if (!registered) {
            newCtx.setAddPending();
            callHandlerCallbackLater(newCtx, true);
            return this;
        }
        EventExecutor executor = newCtx.executor();
        if (!executor.inEventLoop()) {
            newCtx.setAddPending();
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    callHandlerAdded0(newCtx);
                }
            });
            return this;
        }
    }
    callHandlerAdded0(newCtx);
    return this;
}
  • 检查是否有重复的Handler
java 复制代码
private static void checkMultiplicity(ChannelHandler handler) {
   if (handler instanceof ChannelHandlerAdapter) {
       ChannelHandlerAdapter h = (ChannelHandlerAdapter) handler;
       if (!h.isSharable() && h.added) {
           throw new ChannelPipelineException(
                   h.getClass().getName() +
                   " is not a @Sharable handler, so can't be added or removed multiple times.");
       }
       h.added = true;
   }
}

handler如果集成了ChannelHandlerAdapter,那么会通过ChannelHandlerAdapter内部的成员变量added判断是否重复添加,如果重复添加且没有使用@Sharable标记为可重复添加,那么就会抛出异常。

✅每个连接独立一个 handler 实例

这里可以看到,每一个channel的MyHandler实例都是new出来的,这种方式下的添加的handler不是会重复的。

java 复制代码
// 添加handler
ServerBootstrap b = new ServerBootstrap();
b.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel ch) {
        ch.pipeline().addLast(new MyHandler()); // 每个连接都 new 一次
    }
});

⚠️ 同一个 handler 被多个 Channel 共享 这种场景下,所有channel的pipeline共用同一个MyHandler对象

java 复制代码
MyHandler shared = new MyHandler();
b.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel ch) {
        ch.pipeline().addLast(shared); // 重复使用同一个实例
    }
});

值得注意的是,大部分场景下的Handler是有状态的,开发者需要根据场景选择合适的方式。

关于Unsafe

我们在前面的系列文章中提到过unsafe这个类,这个类是netty内部用来实际处理IO事件的类。Unsafe家族的继承关系如下

1.NioUnsafe增加了可以访问底层JDK的SelectableChannel的功能,定义了从SelectableChannel读取数据的read方法。

2.AbstractUnsafe实现了大部分Unsafe的功能。

3.AbstractNioUnsafe主要是通过代理到其外部类AbstractNioChannel获得了与JDK NIO相关的一些信息,比如SelectableChannel、SelectionKey等。

4.把NioSocketChannelUnsafe和NioByteUnsafe放到一起讲,实现了IO的基本操作------读和写,这些操作都与JDK底层相关。

5.NioMessageUnsafe和NioByteUnsafe是处在同一层次的抽象,Netty将一个新连接的建立也当作一个IO操作来处理,这里Message的含义我们可以当作一个SelectableChannel,读的意思就是接收一个SelectableChannel。


Unsafe 的分类

从 Netty 的继承结构来看,Unsafe 可以分成两大类:

一种负责处理新连接的接入 ,另一种负责处理已建立连接的字节读写。它们分别是:

  • NioMessageUnsafe(处理 accept)
  • NioByteUnsafe(处理 read/write)

下面我们把它们的职责和来源讲清楚。


1. NioMessageUnsafe

NioMessageUnsafeAbstractNioMessageChannel 的内部类。

AbstractNioMessageChannel 又是 NioServerSocketChannel 的父类。

这意味着:

服务端 ServerSocketChannel 使用的 Unsafe,就是 NioMessageUnsafe。

在 Netty 的设计里,新连接的到达同样被当成一种"读事件",因此 accept 流程也是由 read 方法驱动。

也就是说:

NioMessageUnsafe.read() 完成了服务端新连接的整个接入过程。

如果你看过【服务端启动流程】或【客户端连接接入流程解析】,应该知道其中包含:

  • 调用 JDK 的 accept
  • 创建 NioSocketChannel
  • 配置 pipeline
  • 注册到 worker EventLoop

这里就不重复展开。


2. NioByteUnsafe

NioByteUnsafeAbstractNioByteChannel 的内部类。

AbstractNioByteChannel 是客户端 NioSocketChannel 的父类。

这说明:

客户端连接的 IO 读写,都是通过 NioByteUnsafe 来完成的。

它负责:

  • 从 socket 中读取字节
  • 写出字节到对端
  • 处理半包、空读、自动读控制等逻辑

两类 Unsafe 的分工很明确:

Unsafe 类型 适用对象 负责的事情
NioMessageUnsafe NioServerSocketChannel 接受新连接(accept)
NioByteUnsafe NioSocketChannel 读写字节流(read/write)

Unsafe 的调用路径

我们已经了解了两类 Unsafe 接口的功能------分别负责处理新连接和字节流读写。那么程序是如何执行到 Unsafe 的 read() 方法的呢?

实际上,这个调用路径与 Netty 的事件循环机制紧密相关:

NioServerSocketChannel 的 NioMessageUnsafe.read()

  • 在 NioEventLoop 的事件轮询中,当检测到新连接(ACCEPT 事件)时被触发调用
  • 对应的 NioEventLoop 轮询在 Channel 注册时启动

NioSocketChannel 的 NioByteUnsafe.read()

  • 在 NioEventLoop 的事件轮询中,当检测到读写事件(READ/WRITE 事件)时被调用
  • 对应的 NioEventLoop 轮询同样在 Channel 注册时启动

关键区别在于注册时机

  • Boss Group 的注册发生在 initAndRegister() 中,绑定的是 ServerSocketChannel
  • Worker Group 的注册发生在 ServerBootstrapAcceptor.channelRead() 方法的 childGroup.register(child) 中,绑定的是普通的 SocketChannel

这种设计确保了连接建立与数据读写在不同的事件循环中处理,实现了高效的分层架构。

小结

本章主要介绍了ChannelPipeline的一些疑难知识,包括重复handler的检查和unsafe类。下一章稍微提一下netty的writeAndFlush方法。

相关推荐
Luo_xguan2 天前
一、Netty-高并发IO底层原理(5种主要的IO模型)
java·服务器·netty·nio
桦说编程9 天前
深入解析CompletableFuture源码实现(3)———多源输入
java·性能优化·源码阅读
戮戮10 天前
一次深入排查:Spring Cloud Gateway TCP 连接复用导致 K8s 负载均衡失效
tcp/ip·spring cloud·kubernetes·gateway·负载均衡·netty
fat house cat_11 天前
【netty】基于主从Reactor多线程模型|如何解决粘包拆包问题|零拷贝
java·服务器·网络·netty
Moe48813 天前
Netty技术:SimpleChannelInboundHandler<>的使用
netty
poemyang16 天前
jemalloc思想的极致演绎:深度解构Netty内存池的精妙设计与实现
rpc·netty
poemyang17 天前
“化零为整”的智慧:内存池如何绕过系统调用和GC,构建性能的护城河
rpc·netty
月弦笙音17 天前
【Vue3】Keep-Alive 深度解析
前端·vue.js·源码阅读
晓牛开发者18 天前
Netty4 TLS单向安全加密传输案例
netty