Netty 第七篇:Channel 注册流程详解

 

📅 2025-05-22    ✍️ 原创    🏷️ Java NIO Netty 源码分析 Channel 注册

上一篇我们讲了 initAndRegister() 中的 init 阶段——NioServerSocketChannel 的创建、AbstractNioChannel 设置非阻塞、AbstractChannel 创建 ID / Unsafe / Pipeline,以及 ServerBootstrap.init() 添加 ServerBootstrapAcceptor

这一篇,我们继续跟进 initAndRegister() 中的第二个阶段——register。这个阶段做的事情,简单来说就是:

把刚刚创建并初始化好的 Channel,注册到 childGroup 中某个 NioEventLoop 的 Selector 上,然后触发一系列事件,最终让这个 Channel 开始监听客户端连接。

register 的调用链非常长,涉及多个类的协作。我们一步一步来拆。


一、register 的宏观入口

回顾一下 AbstractBootstrap.doBind() 中的核心代码:

// AbstractBootstrap.java
final ChannelFuture regFuture = group().register(channel);

这里的 group() 返回的是谁?回顾第四篇的内容——在服务端启动时,我们调用了:

serverBootstrap
    .group(new NioEventLoopGroup(1), new NioEventLoopGroup())
    .channel(NioServerSocketChannel.class)
    .childHandler(...);

group() 返回的是 parentGroup(bossGroup),也就是那个只有 1 个线程的 NioEventLoopGroup

所以这里实际调用的是:

parentGroup.register(channel)

也就是把 ServerSocketChannel 注册到 parentGroup 中的那个唯一 NioEventLoop 上。


二、从 childGroup 到 SingleThreadEventLoop

NioEventLoopGroup 继承自 MultithreadEventLoopGroup,它的 register 方法定义在父类中:

// MultithreadEventLoopGroup.java
public ChannelFuture register(Channel channel) {
    return next().register(channel);
}

next() 方法会按照轮询策略,从 children 数组中选出一个 NioEventLoop。因为我们 parentGroup 只有 1 个线程,所以这里选出来的永远是那一个。

选出来之后,调用 NioEventLoop.register(channel)NioEventLoop 继承自 SingleThreadEventLoop,所以我们看 SingleThreadEventLoopregister 方法:

// SingleThreadEventLoop.java
public ChannelFuture register(Channel channel) {
    return register(new DefaultChannelPromise(channel, this));
}

public ChannelFuture register(final ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    promise.channel().unsafe().register(this, promise);
    return promise;
}

这里出现了两个关键点:

  • unsafe():返回的是 Channel 内部的 Unsafe 对象
  • register(this, promise):把当前 NioEventLoop 和 Promise 传进去
💡 小插曲:Unsafe 这个名字听起来很"不安全",但它其实是 Netty 内部对底层操作的一个封装。所有和 JDK Channel 打交道的事情(注册、绑定、连接、读写),最终都通过 Unsafe 来完成。你可以把它理解为 Channel 的"内部管家"。

三、AbstractChannel 的 register 方法

channel.unsafe().register() 实际调用的是 AbstractChannel 中的 register 方法:

// AbstractChannel.java
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    if (eventLoop == null) {
        throw new NullPointerException("eventLoop");
    }
    // 1️⃣ 保存 eventLoop 引用
    AbstractChannel.this.eventLoop = eventLoop;

    // 2️⃣ 判断当前线程是否在 eventLoop 中
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        eventLoop.execute(new Runnable() {
            @Override
            public void run() {
                register0(promise);
            }
        });
    }
}

这段逻辑非常精妙:

  • 保存 eventLoop 引用:从此这个 Channel 就"绑定"到了这个 NioEventLoop 上,后续所有的 IO 事件都由这个线程处理。
  • 判断是否在 eventLoop 线程中:如果当前线程就是 eventLoop 的线程,直接调用 register0();否则,提交一个任务到 eventLoop 的任务队列中,等 eventLoop 线程自己来执行。
这就是为什么 Netty 能做到"一个 Channel 永远由一个线程处理"——注册的那一刻就绑定了,后续永远不会变。

四、register0() 核心三步

不管走哪条路径,最终都会执行到 register0() 方法:

// AbstractChannel.java
private void register0(ChannelPromise promise) {
    try {
        // 1️⃣ 真正注册到 JDK 的 Selector 上
        doRegister();
        // 2️⃣ 回调 Pipeline 中所有 Handler 的 handlerAdded 方法
        pipeline.invokeHandlerAddedIfNeeded();
        // 3️⃣ 触发 ChannelRegistered 和 ChannelActive 事件
        pipeline.fireChannelRegistered();
        if (isActive()) {
            pipeline.fireChannelActive();
        }
    } catch (Throwable t) {
        // 异常处理
        promise.setFailure(t);
    }
}

三步,每一步都至关重要。我们逐一拆解。


五、doRegister():真正注册到 Selector

doRegister() 是一个抽象方法,实际实现在 AbstractNioChannel 中:

// AbstractNioChannel.java
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(
                eventLoop().selector, 0, this);
            return;
        } catch (CancelledKeyException e) {
            // 处理取消的 Key,重试
            if (!selected) {
                eventLoop().selectNow();
                selected = true;
                continue;
            }
            throw e;
        }
    }
}

核心就一行:

selectionKey = javaChannel().register(eventLoop().selector, 0, this);

这行代码做了什么?它调用了 JDK ServerSocketChannelregister 方法:

  • 第一个参数:eventLoop().selector——NioEventLoop 中持有的那个 Selector
  • 第二个参数:0——暂时不关注任何事件
  • 第三个参数:this——把 Channel 自身作为 attachment 传进去
💡 为什么 attachment 是 this?当 Selector 检测到这个 Channel 上有事件就绪时,可以通过 selectionKey.attachment() 拿回这个 Channel 对象,然后找到对应的 Pipeline 来处理事件。这就是 Netty 事件分发的基石。

六、invokeHandlerAddedIfNeeded():回调 HandlerAdded

注册完成后,Netty 会回调 Pipeline 中所有 Handler 的 handlerAdded 方法:

// DefaultChannelPipeline.java
public final ChannelPipeline invokeHandlerAddedIfNeeded() {
    // 遍历 Pipeline 中所有 Handler
    // 对尚未调用过 handlerAdded 的 Handler,调用其 handlerAdded 方法
}

在我们的服务端代码中,Pipeline 里有什么?回顾第六篇的内容:

  • HeadContext(默认,位于 Pipeline 头部)
  • ServerBootstrapAcceptor(在 ServerBootstrap.init() 中添加的)
  • TailContext(默认,位于 Pipeline 尾部)

所以这一步会依次调用:

  1. HeadContext.handlerAdded()
  2. ServerBootstrapAcceptor.handlerAdded()
  3. TailContext.handlerAdded()

ServerBootstrapAcceptor 来说,它的 handlerAdded 方法继承自 ChannelHandlerAdapter,默认是空实现,什么也不做。但它的存在本身就有意义——它已经在 Pipeline 中就位,等待后续的 Accept 事件到来。


七、fireChannelRegistered() 与 fireChannelActive()

回调完 Handler 后,Netty 开始正向传播两个事件:

pipeline.fireChannelRegistered();
if (isActive()) {
    pipeline.fireChannelActive();
}

fireChannelRegistered()HeadContext 开始,沿着 Pipeline 依次调用每个 Handler 的 channelRegistered 方法。

fireChannelActive() 同理,从 HeadContext 开始,依次调用每个 Handler 的 channelActive 方法。


八、从 channelActive 到 read:连锁反应

HeadContextchannelActive 方法中有这样一段逻辑:

// HeadContext.java
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ctx.fireChannelActive();
    readIfIsAutoRead();
}

readIfIsAutoRead() 会判断 Channel 是否开启了 autoRead(默认是开启的)。如果开启了,就调用 channel.read()

channel.read会调用pipeline.read方法,这个 read 会从 TailContext 开始,沿着 Pipeline 走一遍。最终会到达 HeadContext 自己的 read 方法,在里面调用 unsafe.beginRead()

unsafe.beginRead()AbstractChannel.doBeginRead()AbstractNioChannel.doBeginRead()

// AbstractNioChannel.java
protected void doBeginRead() throws Exception {
    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }
    // 注册感兴趣的事件
    selectionKey.interestOps(readInterestOp);
}

还记得第六篇中提到的 readInterestOp 吗?对于 ServerSocketChannel,它的值是 SelectionKey.OP_ACCEPT(值为 16)。

所以这一步的最终效果是:

告诉 JDK Selector:我对这个 ServerSocketChannel 上的 OP_ACCEPT 事件感兴趣。当有客户端连接进来时,Selector 就能感知到。
💡 为什么 readInterestOp 不在注册时就设置,而是延迟到这里才设置?因为 Netty 的设计理念是"按需注册"。注册到 Selector 只是建立关联,真正要监听什么事件,由业务层通过 read 操作来触发。这样更灵活,也更符合 Pipeline 的责任链模式。

九、完整注册流程总结

把整个调用链串起来,从 group().register(channel) 到最终注册 OP_ACCEPT 事件,一共经历了以下阶段:

序号所在类核心动作
1 MultithreadEventLoopGroup next() 轮询选出一个 NioEventLoop
2 SingleThreadEventLoop 调用 channel.unsafe().register(this, promise)
3 AbstractChannel 保存 eventLoop 引用,判断线程归属
4 AbstractChannel 调用 register0(promise)
5 AbstractNioChannel doRegister():注册到 JDK Selector,attachment = this
6 DefaultChannelPipeline invokeHandlerAddedIfNeeded():回调所有 handlerAdded
7 DefaultChannelPipeline fireChannelRegistered():正向传播 Registered 事件
8 DefaultChannelPipeline fireChannelActive():正向传播 Active 事件
9 HeadContext channelActive() 中调用 readIfIsAutoRead()
10 AbstractNioChannel doBeginRead()interestOps(readInterestOp)

用一张图来描述就是:

group().register(channel)
    → next() 选 NioEventLoop
    → SingleThreadEventLoop.register()
    → AbstractChannel.register()
    → register0()
        → doRegister()          // JDK Selector 注册
        → invokeHandlerAdded()  // 回调 handlerAdded
        → fireRegistered()      // 传播 Registered
        → fireActive()          // 传播 Active
            → readIfIsAutoRead()
            → pipeline.read()
            → unsafe.beginRead()
            → doBeginRead()
            → interestOps(OP_ACCEPT)  // 最终注册监听事件

十、总结

这一篇我们完整追踪了 initAndRegister() 中 register 阶段的全部调用链。核心要点如下:

  1. Channel 与 EventLoop 的绑定:注册的那一刻就确定了"谁管这个 Channel",后续永不改变。
  2. JDK Selector 的注册:通过 javaChannel().register(selector, 0, this) 完成,attachment 是 Channel 自身。
  3. Pipeline 事件传播:Registered → Active → read,层层推进,最终触发对 OP_ACCEPT 的监听。
  4. 为什么延迟注册事件:Netty 采用"按需注册"策略,readInterestOp 在第六篇初始化时埋下伏笔,在这里才真正生效。
  5. 调用链虽长但职责清晰:每个类各司其职,EventLoop 管调度、AbstractChannel 管流程、AbstractNioChannel 管 JDK 交互、Pipeline 管事件传播。

至此,initAndRegister() 的两个阶段(init 和 register)我们都讲完了。接下来还有 doBind0() 没讲——端口绑定的内部逻辑是什么?绑定之后服务端是如何开始监听客户端连接的?这些我们下一篇继续。


— 原创文章,转载请注明出处 —

posted @ 2026-05-29 14:58  小金虫  阅读(19)  评论(0)    收藏  举报