Netty源码分析---Netty-Pipeline初始化流程详解

Netty Pipeline源码分析(2) 原文链接:https://wangwei.one/posts/netty-pipeline-source-analyse-2.html 前面 ,我们分析了Netty Pipeline的初始化及节点添加与删除逻辑。接下来,我们将来分析Pipeline的事件传播机制。 Netty版本:4.1.30 inBound事件传播 示例 我们通过下面这个例子来演示Netty Pipeline的事件传播机... 阅读详情

Netty Pipeline 初始化流程详解

前言

在使用 Netty 开发网络应用时,Pipeline(管道)是一个非常核心的概念。它就像一条流水线,负责处理进出的数据。但你有没有想过,当我们调用 b.handler(new MyChannelInitializer()) 后,Netty 是如何把这个 Handler 添加到 Pipeline 中的?又是在什么时候真正完成初始化的呢?

本文将带你深入 Netty 源码,一步步揭开 Pipeline 初始化的神秘面纱。即使你是 Netty 初学者,也能轻松理解整个流程。

什么是 Pipeline?

在深入源码之前,我们先简单了解一下 Pipeline 的概念:

Pipeline 是 Netty 中的责任链模式实现,它维护了一个由多个 ChannelHandler 组成的双向链表。每个 Channel 都有自己的 Pipeline,用于处理入站(Inbound)和出站(Outbound)事件。

Pipeline 的基本结构如下:

head <-> handler1 <-> handler2 <-> ... <-> tail

其中 head 和 tail 是 Netty 自动创建的两个特殊节点,我们添加的 Handler 都会插入到它们之间。

整体流程概览

Pipeline 的初始化主要分为两个阶段:

  1. 准备阶段(Init):在 Channel 创建时,将 ChannelInitializer 添加到 Pipeline,但此时还不会真正执行初始化逻辑
  2. 执行阶段(Register):在 Channel 注册到 EventLoop 后,触发所有 Handler 的 handlerAdded() 方法,完成真正的初始化

为什么要分两个阶段呢?因为在 Channel 还没有注册到 EventLoop 之前,很多操作是不安全的。Netty 采用了一种"延迟初始化"的策略,先把需要执行的任务记录下来,等到合适的时机再统一执行。

接下来,我们从客户端启动代码开始,一步步追踪源码。

第一阶段:准备阶段(Init)

1. 客户端启动入口

让我们从一个典型的 Netty 客户端启动代码开始:

private void connect(String inetHost, int inetPort) {
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
        Bootstrap b = new Bootstrap();
        b.group(workerGroup);
        b.channel(NioSocketChannel.class);
        b.option(ChannelOption.AUTO_READ, true);
        b.handler(new MyChannelInitializer());  // 设置 Handler
        
        // 关键:调用 connect 方法
        ChannelFuture f = b.connect(inetHost, inetPort).sync();
        
        f.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        workerGroup.shutdownGracefully();
    }
}

这里的关键是 b.connect(inetHost, inetPort) 这行代码,它会触发整个 Pipeline 的初始化流程。

2. connect() 方法调用链

当我们调用 b.connect() 时,会经过以下调用链:

// Bootstrap.connect(String, int)
public ChannelFuture connect(String inetHost, int inetPort) {
    return connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}

// Bootstrap.connect(SocketAddress)
public ChannelFuture connect(SocketAddress remoteAddress) {
    if (remoteAddress == null) {
        throw new NullPointerException("remoteAddress");
    }
    validate();
    return doResolveAndConnect(remoteAddress, config.localAddress());
}

// Bootstrap.doResolveAndConnect()
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, 
                                          final SocketAddress localAddress) {
    // 关键方法:初始化并注册 Channel
    final ChannelFuture regFuture = initAndRegister();
    // ... 省略其他代码
}

这个调用链最终会走到 initAndRegister() 方法,这是 Pipeline 初始化的核心入口。

3. initAndRegister() - 初始化和注册的入口

final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        // 1. 创建 Channel 实例(NioSocketChannel)
        channel = channelFactory.newChannel();
        
        // 2. 初始化 Channel(重点关注这里)
        init(channel);
        
    } catch (Throwable t) {
        // ... 异常处理
    }
    
    // 3. 将 Channel 注册到 EventLoop(稍后分析)
    ChannelFuture regFuture = config().group().register(channel);
    return regFuture;
}

这个方法做了三件事:

  1. 创建 Channel 实例
  2. 调用 init() 方法初始化 Channel
  3. 将 Channel 注册到 EventLoop

我们重点关注第 2 步的 init() 方法。

4. init() - 将 ChannelInitializer 添加到 Pipeline

@Override
void init(Channel channel) throws Exception {
    ChannelPipeline p = channel.pipeline();
    
    // 将我们设置的 Handler(ChannelInitializer)添加到 Pipeline
    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }
            // ... 其他初始化逻辑
        }
    });
}

关键点:这里调用了 p.addLast() 方法,将 ChannelInitializer 添加到 Pipeline 中。

此时 Pipeline 的状态变为:

head <-> ChannelInitializer <-> tail

但是,ChannelInitializer 的 initChannel() 方法还没有被调用!这就是"延迟初始化"的体现。

5. addLast() - 添加 Handler 的核心逻辑

让我们深入 addLast() 方法,看看 Netty 是如何处理 Handler 添加的:

@Override
public final ChannelPipeline addLast(ChannelHandler... handlers) {
    return addLast(null, handlers);
}

public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
    for (ChannelHandler h: handlers) {
        if (h == null) {
            break;
        }
        // 遍历每个 Handler,逐个添加
        addLast(executor, null, h);
    }
    return this;
}

最终会调用到这个重载方法:

@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
    final AbstractChannelHandlerContext newCtx;
    
    synchronized (this) {
        // 检查 Handler 是否可以被多次添加
        checkMultiplicity(handler);
        
        // 步骤1:将 Handler 包装成 Context
        newCtx = newContext(group, filterName(name, handler), handler);
        
        // 步骤2:将 Context 添加到链表中
        addLast0(newCtx);
        
        // 步骤3:关键判断 - 如果还没有注册,创建待处理任务
        if (!registered) {
            newCtx.setAddPending();
            // 创建 PendingHandlerAddedTask 并加入队列
            callHandlerCallbackLater(newCtx, true);
            return this;
        }
        
        // 如果已经注册,直接调用 handlerAdded
        EventExecutor executor = newCtx.executor();
        if (!executor.inEventLoop()) {
            // ... 省略
        }
        callHandlerAdded0(newCtx);
    }
    return this;
}

这个方法是 Pipeline 初始化的核心,让我们逐步分析:

步骤1:newContext() - 包装 Handler
newCtx = newContext(group, filterName(name, handler), handler);

作用:将 ChannelHandler 包装成 AbstractChannelHandlerContext。

为什么要包装呢?因为 Pipeline 实际上是一个 Context 的双向链表,而不是 Handler 的链表。每个 Context 都持有一个 Handler 的引用,同时还包含了前后节点的引用、执行器等信息。

可以这样理解:

  • Handler:业务逻辑处理器(我们编写的代码)
  • Context:Handler 的容器,包含了 Handler 以及它在链表中的位置信息
步骤2:addLast0() - 添加到链表
addLast0(newCtx);

作用:将新创建的 Context 添加到 Pipeline 的双向链表末尾(tail 之前)。

这个方法会修改链表的指针,将新节点插入到 tail 之前:

head <-> ... <-> newCtx <-> tail
步骤3:callHandlerCallbackLater() - 延迟回调
if (!registered) {
    newCtx.setAddPending();
    callHandlerCallbackLater(newCtx, true);
    return this;
}

关键判断:if (!registered)

这里判断 Channel 是否已经注册到 EventLoop。在我们的场景中,此时 Channel 还没有注册,所以会进入这个分支。

callHandlerCallbackLater() 方法的作用是:将 Handler 的初始化任务延迟到注册完成后再执行。

6. callHandlerCallbackLater() - 创建待处理任务

让我们看看这个方法的实现:

private void callHandlerCallbackLater(AbstractChannelHandlerContext ctx, boolean added) {
    assert !registered;
    
    // 根据 added 参数创建不同的任务
    // added = true:创建 PendingHandlerAddedTask
    // added = false:创建 PendingHandlerRemovedTask
    PendingHandlerCallback task = added ? 
        new PendingHandlerAddedTask(ctx) : 
        new PendingHandlerRemovedTask(ctx);
    
    // 将任务加入到待处理队列
    PendingHandlerCallback pending = pendingHandlerCallbackHead;
    if (pending == null) {
        // 队列为空,设置为头节点
        pendingHandlerCallbackHead = task;
    } else {
        // 队列不为空,添加到队列尾部
        while (pending.next != null) {
            pending = pending.next;
        }
        pending.next = task;
    }
}

核心思想:

这个方法创建了一个 PendingHandlerAddedTask 任务,并将它加入到一个待处理任务队列中。这个队列是一个简单的单向链表,用 pendingHandlerCallbackHead 作为头节点。

为什么要用任务队列?

因为在 Channel 还没有注册到 EventLoop 之前,很多操作是不安全的。比如:

  • 无法确定在哪个线程执行
  • EventLoop 还没有准备好
  • 可能会导致线程安全问题

所以 Netty 采用了一种巧妙的设计:先把需要执行的操作记录下来(创建任务),等到 Channel 注册完成后,再统一执行这些任务。

小结:

到这里,准备阶段就完成了。此时的状态是:

  1. ChannelInitializer 已经被添加到 Pipeline 链表中
  2. 一个 PendingHandlerAddedTask 任务被创建并加入到待处理队列
  3. 但是 ChannelInitializer 的 initChannel() 方法还没有被调用

Pipeline 状态:

head <-> ChannelInitializer(待初始化) <-> tail

待处理任务队列:

pendingHandlerCallbackHead -> PendingHandlerAddedTask(ChannelInitializer)

接下来,我们进入第二阶段:执行阶段。

第二阶段:执行阶段(Register)

7. register() - 注册 Channel 到 EventLoop

回到 initAndRegister() 方法,在调用完 init() 后,会继续执行注册操作:

final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        channel = channelFactory.newChannel();
        init(channel);  // 准备阶段完成
    } catch (Throwable t) {
        // ...
    }
    
    // 执行阶段开始:注册 Channel
    ChannelFuture regFuture = config().group().register(channel);
    return regFuture;
}

让我们追踪 register() 方法的调用链:

// MultithreadEventLoopGroup.register()
@Override
public ChannelFuture register(Channel channel) {
    return register(new DefaultChannelPromise(channel, this));
}

// SingleThreadEventLoop.register()
@Override
public ChannelFuture register(final ChannelPromise promise) {
    ObjectUtil.checkNotNull(promise, "promise");
    // 调用 Channel 的 Unsafe 进行注册
    promise.channel().unsafe().register(this, promise);
    return promise;
}

// AbstractChannel.AbstractUnsafe.register()
@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // ... 省略一些检查和设置
    
    // 判断是否在 EventLoop 线程中
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        // 如果不在,提交任务到 EventLoop
        eventLoop.execute(new Runnable() {
            @Override
            public void run() {
                register0(promise);
            }
        });
    }
}

最终会调用到 register0() 方法。

8. register0() - 真正的注册逻辑

private void register0(ChannelPromise promise) {
    try {
        // 确保 Channel 还是打开状态
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        
        boolean firstRegistration = neverRegistered;
        
        // 执行底层的注册操作(将 Channel 注册到 Selector)
        doRegister();
        
        neverRegistered = false;
        registered = true;
        
        // 关键:调用 Pipeline 的 invokeHandlerAddedIfNeeded()
        pipeline.invokeHandlerAddedIfNeeded();
        
        // 触发 channelRegistered 事件
        safeSetSuccess(promise);
        pipeline.fireChannelRegistered();
        
        // ... 其他逻辑
    } catch (Throwable t) {
        // ... 异常处理
    }
}

关键代码:pipeline.invokeHandlerAddedIfNeeded()

这行代码会触发所有待处理任务的执行,也就是我们在准备阶段创建的 PendingHandlerAddedTask。

9. invokeHandlerAddedIfNeeded() - 触发待处理任务

final void invokeHandlerAddedIfNeeded() {
    assert channel.eventLoop().inEventLoop();
    
    if (firstRegistration) {
        firstRegistration = false;
        // 调用所有待处理的 handlerAdded 回调
        callHandlerAddedForAllHandlers();
    }
}

作用:检查是否是第一次注册,如果是,则调用 callHandlerAddedForAllHandlers() 执行所有待处理任务。

10. callHandlerAddedForAllHandlers() - 执行所有待处理任务

private void callHandlerAddedForAllHandlers() {
    final PendingHandlerCallback pendingHandlerCallbackHead;
    
    synchronized (this) {
        assert !registered;
        
        // 标记为已注册
        registered = true;
        
        // 获取待处理任务队列的头节点
        pendingHandlerCallbackHead = this.pendingHandlerCallbackHead;
        
        // 清空队列(帮助 GC)
        this.pendingHandlerCallbackHead = null;
    }
    
    // 遍历任务队列,执行每个任务
    PendingHandlerCallback task = pendingHandlerCallbackHead;
    while (task != null) {
        task.execute();  // 执行任务
        task = task.next;  // 移动到下一个任务
    }
}

核心逻辑:

  1. 将 registered 标记设置为 true,表示 Channel 已经注册
  2. 获取待处理任务队列的头节点
  3. 遍历队列,依次执行每个任务的 execute() 方法

这里的 task 就是我们在准备阶段创建的 PendingHandlerAddedTask。

11. PendingHandlerAddedTask.execute() - 执行添加任务

让我们看看 PendingHandlerAddedTask 的 execute() 方法:

@Override
void execute() {
    EventExecutor executor = ctx.executor();
    
    // 判断是否在 EventLoop 线程中
    if (executor.inEventLoop()) {
        // 在 EventLoop 线程中,直接调用
        callHandlerAdded0(ctx);
    } else {
        // 不在 EventLoop 线程中,提交任务
        try {
            executor.execute(this);
        } catch (RejectedExecutionException e) {
            if (logger.isWarnEnabled()) {
                logger.warn(
                    "Can't invoke handlerAdded() as the EventExecutor {} rejected it, removing handler {}.",
                    executor, ctx.name(), e);
            }
            remove0(ctx);
            ctx.setRemoved();
        }
    }
}

作用:

这个方法确保 callHandlerAdded0() 在正确的线程(EventLoop 线程)中执行。如果当前已经在 EventLoop 线程中,就直接调用;否则,将任务提交到 EventLoop 中执行。

在我们的场景中,由于 register0() 已经在 EventLoop 线程中执行,所以会直接调用 callHandlerAdded0(ctx)。

12. callHandlerAdded0() - 调用 handlerAdded 回调

private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) {
    try {
        // 调用 Context 的 callHandlerAdded 方法
        ctx.callHandlerAdded();
    } catch (Throwable t) {
        boolean removed = false;
        try {
            // 如果出现异常,移除这个 Handler
            remove0(ctx);
            ctx.callHandlerRemoved();
            removed = true;
        } catch (Throwable t2) {
            if (logger.isWarnEnabled()) {
                logger.warn("Failed to remove a handler: " + ctx.name(), t2);
            }
        }
        
        // 触发异常事件
        if (removed) {
            fireExceptionCaught(new ChannelPipelineException(
                ctx.handler().getClass().getName() +
                ".handlerAdded() has thrown an exception; removed.", t));
        } else {
            fireExceptionCaught(new ChannelPipelineException(
                ctx.handler().getClass().getName() +
                ".handlerAdded() has thrown an exception; also failed to remove.", t));
        }
    }
}

作用:

调用 Context 的 callHandlerAdded() 方法,并处理可能出现的异常。如果 handlerAdded() 方法抛出异常,会自动将这个 Handler 从 Pipeline 中移除。

13. AbstractChannelHandlerContext.callHandlerAdded() - 最终调用

final void callHandlerAdded() throws Exception {
    // 设置状态为 ADD_COMPLETE
    // 这个方法会检查状态,确保 handlerAdded 只被调用一次
    if (setAddComplete()) {
        // 调用 Handler 的 handlerAdded 方法
        handler().handlerAdded(this);
    }
}

关键点:handler().handlerAdded(this)

这行代码会调用当前 Context 持有的 Handler 的 handlerAdded() 方法。

重要说明:这里调用的是 Bootstrap.init() 方法中创建的匿名 ChannelInitializer(Initializer-A)的 handlerAdded() 方法,而不是我们自定义的 MyChannelInitializer。

为什么呢?因为:

  1. 在准备阶段,p.addLast(Initializer-A) 创建了 ctx-A
  2. ctx-A 持有的 handler 引用指向 Initializer-A
  3. PendingHandlerAddedTask 持有的是 ctx-A 的引用
  4. 所以这里的 handler() 返回的是 Initializer-A

Initializer-A 的 handlerAdded() 方法会:

  1. 调用 Initializer-A 重写的 initChannel() 方法
  2. 在 initChannel() 中,将我们的 MyChannelInitializer 添加到 Pipeline
  3. MyChannelInitializer 被添加后,也会触发它的 handlerAdded() 方法
  4. 最终调用到我们重写的 initChannel() 方法

接下来,我们详细分析这个调用流程。

深入理解:initChannel() 方法的调用机制

核心问题回顾

在第 13 步中,我们看到 handler().handlerAdded(this) 被调用。这里有一个非常重要的问题需要理解:

当 handler().handlerAdded(this) 被调用时,调用的是哪个 ChannelInitializer 实例的方法?

答案是:调用的是 Bootstrap.init() 方法中创建的匿名 ChannelInitializer(Initializer-A)实例的 handlerAdded() 方法。

理解关键:Context 持有的是哪个 Handler?

回顾 Pipeline 初始化的准备阶段

在 Bootstrap.init() 方法中,Netty 创建了一个匿名 ChannelInitializer:

@Override
void init(Channel channel) throws Exception {
    ChannelPipeline p = channel.pipeline();
    
    // 创建匿名 ChannelInitializer(我们称它为 Initializer-A)
    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            // 获取用户设置的 handler(MyChannelInitializer)
            ChannelHandler handler = config.handler();
            if (handler != null) {
                // 将 MyChannelInitializer 添加到 Pipeline
                pipeline.addLast(handler);
            }
        }
    });
}
关键点:addLast() 做了什么?

当调用 p.addLast(new ChannelInitializer<Channel>() {...}) 时:

  1. 创建了一个新的 Context(我们称它为 ctx-A)
  2. ctx-A 持有的 handler 引用指向这个匿名 ChannelInitializer(Initializer-A)
  3. ctx-A 被添加到 Pipeline 链表中
  4. 创建了一个 PendingHandlerAddedTask,持有 ctx-A 的引用

此时的状态:

Pipeline: head <-> ctx-A(持有 Initializer-A) <-> tail
待处理任务: PendingHandlerAddedTask(ctx-A)
执行阶段:调用 handlerAdded

当 Channel 注册完成后,会执行待处理任务:

// PendingHandlerAddedTask.execute()
void execute() {
    callHandlerAdded0(ctx);  // 这里的 ctx 就是 ctx-A
}

// callHandlerAdded0
private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) {
    ctx.callHandlerAdded();  // 调用 ctx-A 的 callHandlerAdded
}

// AbstractChannelHandlerContext.callHandlerAdded
final void callHandlerAdded() throws Exception {
    if (setAddComplete()) {
        // 这里的 handler() 返回的是 ctx-A 持有的 handler
        // 也就是 Initializer-A(匿名 ChannelInitializer)
        handler().handlerAdded(this);
    }
}

关键理解:

  • handler() 方法返回的是当前 Context 持有的 Handler 引用
  • ctx-A 持有的是 Initializer-A(匿名 ChannelInitializer)
  • 所以 handler().handlerAdded(this) 调用的是 Initializer-A 的 handlerAdded() 方法

Initializer-A 的完整调用流程

现在让我们完整地追踪 Initializer-A 的 handlerAdded() 方法是如何最终调用到我们自定义的 MyChannelInitializer 的 initChannel() 方法的。

步骤1:调用 Initializer-A 的 handlerAdded()
// ChannelInitializer.handlerAdded()
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    // 检查 Channel 是否已经注册
    if (ctx.channel().isRegistered()) {
        // 调用 initChannel(ctx)
        if (initChannel(ctx)) {
            // 初始化成功后,移除自己
            removeState(ctx);
        }
    }
}

作用:

这个方法是 ChannelInitializer 的核心方法,它会:

  1. 检查 Channel 是否已经注册(此时已经注册)
  2. 调用 initChannel(ctx) 方法
  3. 初始化完成后,将自己从 Pipeline 中移除
步骤2:调用 initChannel(ChannelHandlerContext)
// ChannelInitializer.initChannel(ChannelHandlerContext)
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
    // 使用 Set 确保每个 Context 只初始化一次
    if (initMap.add(ctx)) {
        try {
            // 关键:调用抽象方法 initChannel(Channel)
            // 由于多态,这里会调用子类(Initializer-A)重写的版本
            initChannel((C) ctx.channel());
        } catch (Throwable cause) {
            // 如果初始化过程中出现异常,触发异常处理
            exceptionCaught(ctx, cause);
        } finally {
            // 无论成功还是失败,都要将 ChannelInitializer 从 Pipeline 中移除
            ChannelPipeline pipeline = ctx.pipeline();
            if (pipeline.context(this) != null) {
                pipeline.remove(this);
            }
        }
        return true;
    }
    return false;
}

作用:

这个方法做了三件重要的事:

  1. 使用 initMap 确保每个 Context 只初始化一次(防止重复初始化)
  2. 调用抽象方法 initChannel(Channel),由于 Java 的多态机制,会调用 Initializer-A 重写的版本
  3. 在 finally 块中移除 ChannelInitializer,确保它只执行一次

为什么要移除?

ChannelInitializer 是一个"一次性"的 Handler,它的唯一作用就是在合适的时机初始化 Pipeline。一旦初始化完成,它就没有存在的必要了,所以要从 Pipeline 中移除。

步骤3:调用 Initializer-A 重写的 initChannel(Channel)
// 这是在 Bootstrap.init() 中定义的匿名内部类
new ChannelInitializer<Channel>() {
    @Override
    public void initChannel(final Channel ch) throws Exception {
        final ChannelPipeline pipeline = ch.pipeline();
        
        // 获取用户设置的 handler(MyChannelInitializer)
        ChannelHandler handler = config.handler();
        
        if (handler != null) {
            // 将 MyChannelInitializer 添加到 Pipeline
            pipeline.addLast(handler);
        }
        
        // 注意:这里可能还有其他逻辑,比如添加 ServerBootstrapAcceptor
        // 但对于客户端来说,主要就是添加用户的 handler
    }
}

作用:

这是 Initializer-A 重写的 initChannel() 方法,它的作用是:

  1. 获取用户通过 b.handler() 设置的 Handler(也就是 MyChannelInitializer)
  2. 将 MyChannelInitializer 添加到 Pipeline 中

关键理解:

  • config.handler() 返回的是我们在客户端启动代码中设置的 new MyChannelInitializer()
  • 这里调用 pipeline.addLast(handler) 会将 MyChannelInitializer 添加到 Pipeline

此时 Pipeline 的状态变化:

之前:head <-> Initializer-A <-> tail
之后:head <-> MyChannelInitializer <-> tail
(Initializer-A 在 finally 块中被移除)

MyChannelInitializer 的初始化流程

现在,MyChannelInitializer 已经被添加到 Pipeline 中了。接下来,它也会经历同样的初始化流程。

步骤4:addLast(MyChannelInitializer)

当调用 pipeline.addLast(handler) 时(这里的 handler 是 MyChannelInitializer):

@Override
public final ChannelPipeline addLast(ChannelHandler... handlers) {
    return addLast(null, handlers);
}

public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
    if (handlers == null) {
        throw new NullPointerException("handlers");
    }
    
    for (ChannelHandler h: handlers) {
        if (h == null) {
            break;
        }
        // 遍历每个 Handler,逐个添加
        addLast(executor, null, h);
    }
    
    return this;
}

最终会调用到:

@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
    final AbstractChannelHandlerContext newCtx;
    
    synchronized (this) {
        // 检查 Handler 是否可以被多次添加
        checkMultiplicity(handler);
        
        // 将 MyChannelInitializer 包装成 Context(我们称它为 ctx-B)
        newCtx = newContext(group, filterName(name, handler), handler);
        
        // 将 ctx-B 添加到 Pipeline 链表中
        addLast0(newCtx);
        
        // 关键判断:此时 Channel 已经注册(registered = true)
        if (!registered) {
            // 如果还没注册,创建待处理任务
            newCtx.setAddPending();
            callHandlerCallbackLater(newCtx, true);
            return this;
        }
        
        // 因为已经注册,所以直接调用 handlerAdded
        EventExecutor executor = newCtx.executor();
        if (!executor.inEventLoop()) {
            // 如果不在 EventLoop 线程中,提交任务
            callHandlerAddedInEventLoop(newCtx, executor);
            return this;
        }
    }
    
    // 在 EventLoop 线程中,直接调用
    callHandlerAdded0(newCtx);
    return this;
}

关键区别:

与 Initializer-A 不同,此时 Channel 已经注册(registered = true),所以不会创建待处理任务,而是直接调用 callHandlerAdded0(newCtx)。

步骤5:调用 callHandlerAdded0(ctx-B)
private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) {
    try {
        // 调用 ctx-B 的 callHandlerAdded
        ctx.callHandlerAdded();
    } catch (Throwable t) {
        // 异常处理:如果出现异常,移除这个 Handler
        boolean removed = false;
        try {
            remove0(ctx);
            ctx.callHandlerRemoved();
            removed = true;
        } catch (Throwable t2) {
            if (logger.isWarnEnabled()) {
                logger.warn("Failed to remove a handler: " + ctx.name(), t2);
            }
        }
        
        // 触发异常事件
        if (removed) {
            fireExceptionCaught(new ChannelPipelineException(
                ctx.handler().getClass().getName() +
                ".handlerAdded() has thrown an exception; removed.", t));
        } else {
            fireExceptionCaught(new ChannelPipelineException(
                ctx.handler().getClass().getName() +
                ".handlerAdded() has thrown an exception; also failed to remove.", t));
        }
    }
}

作用:

调用 ctx-B 的 callHandlerAdded() 方法,并处理可能出现的异常。

步骤6:调用 ctx-B.callHandlerAdded()
final void callHandlerAdded() throws Exception {
    // 设置状态为 ADD_COMPLETE,确保 handlerAdded 只被调用一次
    if (setAddComplete()) {
        // 这里的 handler() 返回的是 ctx-B 持有的 handler
        // 也就是 MyChannelInitializer
        handler().handlerAdded(this);
    }
}

关键理解:

  • 这里的 handler() 返回的是 ctx-B 持有的 Handler
  • ctx-B 持有的是 MyChannelInitializer
  • 所以这里调用的是 MyChannelInitializer 的 handlerAdded() 方法
步骤7:调用 MyChannelInitializer 的 handlerAdded()
// ChannelInitializer.handlerAdded()
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    if (ctx.channel().isRegistered()) {
        // 调用 initChannel(ctx)
        if (initChannel(ctx)) {
            removeState(ctx);
        }
    }
}

这个方法和 Initializer-A 的 handlerAdded() 是同一个方法(都是 ChannelInitializer 类的方法)。

步骤8:调用 initChannel(ChannelHandlerContext)
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
    if (initMap.add(ctx)) {
        try {
            // 调用抽象方法 initChannel(Channel)
            // 由于多态,这里会调用 MyChannelInitializer 重写的版本
            initChannel((C) ctx.channel());
        } catch (Throwable cause) {
            exceptionCaught(ctx, cause);
        } finally {
            // 将 MyChannelInitializer 从 Pipeline 中移除
            ChannelPipeline pipeline = ctx.pipeline();
            if (pipeline.context(this) != null) {
                pipeline.remove(this);
            }
        }
        return true;
    }
    return false;
}
步骤9:调用 MyChannelInitializer 重写的 initChannel(Channel)
// 这是我们自定义的 MyChannelInitializer
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        // 这里是我们的业务逻辑
        System.out.println("链接报告开始");
        System.out.println("链接报告信息:本客户端链接到服务端。channelId:" + channel.id());
        System.out.println("链接报告完毕");
        
        // 我们可以在这里添加其他 Handler
        // channel.pipeline().addLast(new StringDecoder());
        // channel.pipeline().addLast(new StringEncoder());
        // channel.pipeline().addLast(new MyBusinessHandler());
    }
}

作用:

这就是我们自定义的初始化逻辑!在这里,我们可以:

  1. 打印日志
  2. 添加编解码器
  3. 添加业务 Handler
  4. 进行其他初始化操作

执行完成后,MyChannelInitializer 也会在 finally 块中被移除。

完整流程图

让我们用一张图来总结整个调用流程:

时间线:T1 - 客户端启动
    |
    v
b.handler(new MyChannelInitializer())
    [MyChannelInitializer 实例被保存在 config 中]

-------------------------------------------------------------------

时间线:T2 - 调用 b.connect()
    |
    v
Bootstrap.init(channel)
    |
    v
创建 Initializer-A(匿名 ChannelInitializer)
    |
    v
p.addLast(Initializer-A)
    |
    v
创建 ctx-A,持有 Initializer-A 的引用
    |
    v
将 ctx-A 添加到 Pipeline
    |
    v
创建 PendingHandlerAddedTask(ctx-A)

Pipeline 状态:head <-> ctx-A(Initializer-A) <-> tail

-------------------------------------------------------------------

时间线:T3 - Channel 注册完成
    |
    v
执行 PendingHandlerAddedTask
    |
    v
callHandlerAdded0(ctx-A)
    |
    v
ctx-A.callHandlerAdded()
    |
    v
ctx-A.handler().handlerAdded(ctx-A)
    [这里的 handler() 返回 Initializer-A]
    |
    v
Initializer-A.handlerAdded(ctx-A)
    |
    v
Initializer-A.initChannel(ctx-A)
    |
    v
Initializer-A.initChannel(channel)  ← 调用匿名内部类重写的方法
    |
    v
获取 config.handler()  [返回 MyChannelInitializer]
    |
    v
pipeline.addLast(MyChannelInitializer)
    |
    v
Initializer-A 被移除

Pipeline 状态:head <-> ctx-B(MyChannelInitializer) <-> tail

-------------------------------------------------------------------

时间线:T4 - MyChannelInitializer 被添加
    |
    v
创建 ctx-B,持有 MyChannelInitializer 的引用
    |
    v
因为 registered = true,直接调用 callHandlerAdded0(ctx-B)
    |
    v
ctx-B.callHandlerAdded()
    |
    v
ctx-B.handler().handlerAdded(ctx-B)
    [这里的 handler() 返回 MyChannelInitializer]
    |
    v
MyChannelInitializer.handlerAdded(ctx-B)
    |
    v
MyChannelInitializer.initChannel(ctx-B)
    |
    v
MyChannelInitializer.initChannel(channel)  ← 调用我们重写的方法
    |
    v
执行我们的业务逻辑:
    - 打印日志
    - 添加其他 Handler
    |
    v
MyChannelInitializer 被移除

最终 Pipeline 状态:head <-> [我们添加的 Handler] <-> tail

核心要点总结

1. 两个不同的 ChannelInitializer 实例
  • Initializer-A:Bootstrap.init() 中创建的匿名内部类

    • 作用:将用户设置的 Handler(MyChannelInitializer)添加到 Pipeline
    • 生命周期:在 register 完成后被调用,执行完立即移除
  • MyChannelInitializer:用户自定义的 ChannelInitializer

    • 作用:执行用户的初始化逻辑,添加业务 Handler
    • 生命周期:被 Initializer-A 添加到 Pipeline 后立即被调用,执行完立即移除
2. 为什么 callHandlerAdded() 调用的是 Initializer-A?

因为:

  1. 在准备阶段,p.addLast(Initializer-A) 创建了 ctx-A
  2. ctx-A 持有的 handler 引用指向 Initializer-A
  3. PendingHandlerAddedTask 持有的是 ctx-A 的引用
  4. 执行阶段调用 ctx-A.handler().handlerAdded() 时,返回的是 Initializer-A

关键理解:Context 持有哪个 Handler 的引用,handler() 方法就返回哪个 Handler。

3. registered 标志的作用

registered 标志决定了 Handler 的初始化时机:

  • registered = false(Channel 还没注册)

    • 创建 PendingHandlerAddedTask,延迟到注册完成后执行
    • Initializer-A 就是这种情况
  • registered = true(Channel 已经注册)

    • 直接调用 callHandlerAdded0(),立即执行初始化
    • MyChannelInitializer 就是这种情况
4. ChannelInitializer 的自动移除机制

ChannelInitializer 在 initChannel(ChannelHandlerContext) 方法的 finally 块中会自动移除自己:

finally {
    ChannelPipeline pipeline = ctx.pipeline();
    if (pipeline.context(this) != null) {
        pipeline.remove(this);
    }
}

这确保了 ChannelInitializer 只执行一次,执行完就从 Pipeline 中消失。

5. 多态机制的应用

整个流程中,Java 的多态机制起到了关键作用:

// ChannelInitializer 中的代码
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
    // 调用抽象方法
    initChannel((C) ctx.channel());
}

虽然这里调用的是抽象方法 initChannel(Channel),但由于多态:

  • 如果当前实例是 Initializer-A,就调用 Initializer-A 重写的版本
  • 如果当前实例是 MyChannelInitializer,就调用 MyChannelInitializer 重写的版本

最终总结

通过本文的详细分析,我们完整地理解了 Netty Pipeline 的初始化流程。让我们用最简洁的方式总结一下核心要点:

整个流程的本质

Pipeline 初始化实际上是一个两阶段、两层 ChannelInitializer 的过程:

阶段一:准备阶段(Init)
  • 创建 Initializer-A(匿名 ChannelInitializer)
  • 将 Initializer-A 添加到 Pipeline
  • 创建 PendingHandlerAddedTask,等待执行
阶段二:执行阶段(Register)
  • 第一层初始化:执行 Initializer-A 的 initChannel()

    • 作用:将 MyChannelInitializer 添加到 Pipeline
    • 结果:Initializer-A 被移除,MyChannelInitializer 被添加
  • 第二层初始化:执行 MyChannelInitializer 的 initChannel()

    • 作用:执行用户的业务逻辑,添加业务 Handler
    • 结果:MyChannelInitializer 被移除,业务 Handler 留在 Pipeline

为什么要这样设计?

  1. 统一性:客户端和服务端使用相同的初始化流程
  2. 灵活性:通过 Initializer-A 作为中间层,可以在添加用户 Handler 前后做额外处理
  3. 安全性:延迟初始化确保在正确的时机(Channel 注册后)执行
  4. 清晰性:职责分离,Initializer-A 负责框架逻辑,MyChannelInitializer 负责业务逻辑

关键理解点

  1. Context 持有 Handler:handler() 方法返回的是 Context 持有的 Handler 引用
  2. 多态机制:调用抽象方法时,会根据实际实例类型调用对应的重写版本
  3. registered 标志:决定是延迟执行还是立即执行
  4. 自动移除:ChannelInitializer 执行完后会自动从 Pipeline 中移除

Pipeline 状态变化总结

初始状态:
    head <-> tail

准备阶段后:
    head <-> ctx-A(Initializer-A) <-> tail
    [待处理任务队列:PendingHandlerAddedTask(ctx-A)]

第一层初始化后:
    head <-> ctx-B(MyChannelInitializer) <-> tail
    [Initializer-A 已移除]

第二层初始化后:
    head <-> [业务 Handler] <-> tail
    [MyChannelInitializer 已移除]
Netty源码分析(六)pipeline初始化,handlerContext添加和删除handler,inbound和outbound事件的传播 pipeline初始化 创建channel的时候创建pipeline 创建tail和head 两个ChannelHandlerContext 并连接起来,成为一个双向链表 一个channel对应一个pipeline class AbstractChannel { protected AbstractChannel(Channel parent) { this.parent = parent; id = newId(); unsafe = newUn 阅读详情

相关推荐

Netty Channel源码分析

原文链接:https://wangwei.one/posts/netty-channel-source-analyse.html 前面,我们大致了解了Netty中的几个核心组件。今天我们就来先来介绍Netty的网络通信组件,用于执行网络I/O操作 —— Channel。 Netty版本:4.1.30 概述 数据在网络中总是以字节的形式进行流通。我们在进行网络编程时选用何种传输方式编码(OI...

王维 1920

Netty源码分析篇:Netty Pipeline 初始化流程

Netty源码分析篇:Netty Pipeline 初始化流程

java_rutu的博客 657

Win10 桌面回收站右键没有“清空回收站”选项怎么解决

Win10 桌面回收站右键没有“清空回收站” 选项怎么解决? 我之前不知道怎么捣鼓注册表(胡乱删了一些。。。) 导致想清空回收站的时候,右键却没有“清空回收站”这个选项了 以下是解决方法: 1、在桌面上新建一个txt文本文档,将以下代码拷贝进去: Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\*\shell\empty] "Com...

菜鸟的后花园 2万+

Netty源码分析(一)—服务端初始化

Netty源码分析(一)—服务端初始化 传统Java NIO在服务端启动时会涉及到ServerSocketChannel,Selector,selectorKey等类;Netty对Java NIO基础类进行了封装,减少用户开发工作量,降低开发难度; 个人主页:tuzhenyu’s page 原文地址:Netty源码分析(一)—服务端初始化 (0) 服务端

tuzhenyu的博客 4037

2022年电赛E题声源定位跟踪系统

电赛声源定位

qq_53357019的博客 2万+

SAP SD跨公司销售IDOC自动记账配置实战指南

本文详细介绍了SAP SD模块中跨公司销售IDOC自动记账的配置实战指南。通过IDOC技术实现集团企业间交易的自动化处理,显著提升财务效率和准确性。文章涵盖前置配置、RFC连接、合作伙伴参数设置及财务科目分配等关键步骤,并提供了常见错误排查和性能优化建议,帮助用户快速掌握这一高效业务流程。

weixin_29284885的博客 772

【硬核】肝了一月的Netty知识点

说实话我不信有人能看完

敖丙 10万+

PostgreSQL14 离线安装全攻略:从下载到配置详解

本文详细介绍了PostgreSQL14在无网络环境下的离线安装全流程,涵盖RPM包与源码编译两种主流方法。文章重点解析了安装前的系统环境检查、依赖包准备、数据库初始化、远程访问配置以及安装后的关键检查与性能调优,旨在帮助运维和开发人员在离线服务器上高效、稳定地部署PostgreSQL14数据库。

p5l2m9n4o6q的博客 723

Netty源码分析系列之服务端Channel初始化

Netty中服务端的Channel是如何初始化的

天堂的博客 444

Vue - 解决报错 Error: Cannot find module ‘XXX\@vue\cli-service\bin\vue-cli-service.js‘(vue项目编译运行出错,启动失败)

vue报错,Error: Cannot find module ‘@vue\cli-service\bin\vue-cli-service.js‘,vue2项目启动报错,vue3项目运行出错,vue执行npm run serve报错,找不到@vue/cli-service模块,vue项目关于:Error: Cannot find module ‘D:\@vue\cli-service\bin\vue-cli-service.js‘的解决方案,vue项目跑不起来了,Vue项目报错,跑项目报错Cannot fi

🏆 前端领域优质创作者 3115

剪映专业版教程:用AI生成音乐MV并重新制作画面—《漂泊与归梦》完全原创实战

本文详细拆解如何用AI生成音乐MV并重新制作画面的全流程。以歌曲《漂泊与归梦》为例,首先用AI将歌词转化为带音乐的MP4文件;导入剪映后分离音频并删除原视频,通过“识别字幕”功能提取歌词文本并逐句核对修正;接着设置“向右擦除”动画并锁定字幕轨道;最后根据歌词意境配置动漫、风景等非真人画面规避审核风险。教程涵盖从AI生成、音频分离、字幕提取到画面重制的每个环节,助你打造完全原创、无侵权风险的音乐短视频。

深耕计算机专业知识与技术。 1077

Netty源码分析--认真系列(一)

Netty源码分析--认真系列(一)

java_rutu的博客 2056

RK3399平台开发系列讲解(外设篇)交换芯片RTL8306M

网络:交换芯片RTL8306M 电路: make menuconfig 配置: 目录:arch/arm64/configs/rk3399_b201_defconfig CONFIG_FIXED_PHY=y 设备树: arch/arm64/boot/dts/rockchip/rk3399-b201-rev1.dts 时钟: Open之后首先校验网卡mac地址是否正确(stmmac_chec...

内核笔记 2393

【Netty源码分析摘录】(三)服务端Channel初始化

文章目录问题组件说明服务端启动channel初始化总结 问题 Netty作为一款基于事件驱动的高性能网络框架,其底层实际上仍然使用的是JDK里面的NIO,Netty在JDK的NIO上做了大量优化,以及封装,降低了开发人员使用NIO的难度。 使用JDK原生的NIO进行网络编程时,首先得做两件事: 创建以及初始化ServerSocketChannel 将ServerSocketChannel绑定到多路复用器Selector上。 既然说Netty对JDK的NIO做了封装,那么在Netty中是什么时候进行这两步

m0_45406092的博客 654

基于STM32的嵌入式平台实现EtherCAT主站

本文将介绍如何在基于STM32的嵌入式平台上构建EtherCAT主站,并提供相应的源代码。使用SOEM库提供的API函数,我们可以实现与EtherCAT从站设备的实时通信和数据交换。在STM32的代码中,使用SOEM库提供的API函数初始化EtherCAT主站。在SOEM库中,我们可以找到EtherCAT主站的实现代码和相关的API函数。使用SOEM库提供的API函数实现EtherCAT主站的功能,包括发送和接收EtherCAT数据帧、处理从站设备的状态等。确保连接正常并且设备能够相互通信。

CodeSageX的博客 2835

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

【转】【Netty】Netty Pipeline 源码分析 (1)

前面,我们分析了 Netty EventLoop 的创建与启动原理,接下里我们来分析 Netty 中另外两个重要组件 ——ChannelHandler与Pipeline。Netty 中 I/O 事件的传播机制均由它负责,下面我们来看看它是如何实现的。 Netty 版本:4.1.30 我们前面在讲Channel 创建时,在 AbstractChannel 的构造函数中,...

中年油腻男人的转型之路 225

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Netty源码分析第4章(pipeline)---->第1节: pipeline的创建

Netty源码分析第4章(pipeline)---->第1节: pipeline的创建 Netty源码分析第四章: pipeline 概述: pipeline, 顾名思义, 就是管道的意思, 在netty中, 事件在pipeline中传输, 用户可以中断事件, 添加自己的事件处理逻辑, 可以直接将事件中断...

weixin_30699831的博客 121

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

netty源码分析之pipeline(一)

通过前面的源码系列文章中的netty reactor线程三部曲,我们已经知道,netty的reactor线程就像是一个发动机,驱动着整个netty框架的运行,而服务端的绑定和新连接的建立正是发动机的导火线,将发动机点燃 netty在服务端端口绑定和新连接建立的过程中会建立相应的channel,而与channel的动作密切相关的是pipeline这个概念,pipeline像是可以看作是一条流水线...

不朽丶 67

Netty源码分析----pipeline

(*文章基于Netty4.1.22版本) 介绍 Netty中随着一个Channel的创建,会连带创建一个ChannelPipeline,这个ChannelPipeline就像一个处理各种事件的管道,负责去处理Channel上发生的事件,例如连接事件,读事件,写事件等。 更深入的说,处理的并不是ChannelPipeline,而是ChannelPipeline中一个个的ChannelHandl...

菜鸟一名 2574

Netty源码解析之Pipeline解析

首先初始化Pipeline如下 创建channel会调用到如下方法 protected AbstractChannel(Channel parent) { this.parent = parent; id = newId(); unsafe = newUnsafe(); pipeline = newChannelPipeline(); } ...

Lisam Blog 1024

Netty Pipeline源码分析(1)

原文链接:https://wangwei.one/posts/netty-pipeline-source-analyse-1.html 前面,我们分析了Netty EventLoop的 创建 与 启动 原理,接下里我们来分析Netty中另外两个重要组件—— ChannelHandler 与 Pipeline。Netty中I/O事件的传播机制均由它负责,下面我们来看看它是如何实现的。 Net...

王维 555

写自己的netty源码分析--pipeline

绪论 三个问题 netty是如何判断ChannelHandler类型的? 对于ChannelHandler的添加应该遵循什么样的顺序? 用户手动触发事件传播,不同的触发方式有什么样的的区别? 主要内容 pipeline的初始化 添加和删除ChannelHandler 事件和异常的传播 pipeline的初始化 pipeline在创建Channel的时候被创建; pipeline节...

qq_40722284的博客 496

Netty源码分析---invokeHandlerAddedIfNeeded方法详解

Netty 的 invokeHandlerAddedIfNeeded() 方法解析 核心功能 invokeHandlerAddedIfNeeded() 是 Netty 中用于触发 pipeline 中所有 handler 的 handlerAdded() 回调的关键方法,主要特点: 延迟执行机制:通过 PendingHandlerCallback 链表管理待执行的回调任务 线程安全保证:确保在正确的 EventLoop 线程中执行回调 初始化关键点:在 Channel 注册完成后统一执行所有 pending

java_rutu的博客 375

netty服务端启动流程总结

参考资料 NioEventLoopGroup源码分析 netty服务端启动流程源码分析 服务端启动做了哪些事情 //创建EventLoopGroup,初始化所有的EventLoop,初始化线程工具 EventLoopGroup bossEventLoopGroup = new NioEventLoopGroup(2); executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); //创建channel,初始化channel的pipel

weixin_38312719的博客 327

Netty核心线程模型源码分析

Netty的线程模型图如下所示:具体细节看这篇博客。

qq_43456605的博客 957
上一篇: Netty源码分析---Netty-ByteBuf核心详解-简化版
下一篇: Netty源码分析---netty-init()方法解析
不早睡不改名@
博客等级 码龄5年 536粉丝 306原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值