深入分析netty(二)ChannelPipeline

2. Netty大动脉ChannelPipeline

2.1. ChannelPipeline

2.1.1 Channel 与ChannelPipeline

相信大家都知道了,在Netty中每个Channel 都有且仅有一个ChannelPipeline 与之对应,它们的组成关系如下:
image-20200818165548308

通过上图我们可以看到,一个Channel 包含了一个ChannelPipeline, 而ChannelPipeline中又维护了一个由ChannelHandlerContext 组成的双向链表.这个链表的头是HeadContext,链表的尾是TailContext, 并且每个ChannelHandl erContext 中又关联着一个ChannelHandler.
上面的图示给了我们一个对ChannelPipeline 的直观认识,但是实际上Netty 实现的Channel是否真的是这样的呢?我们继续用源码说话. .
在前我们已经知道了一个Channel 的初始化的基本过程,下面我们再回顾一下.
下面的代码是AbstractChannel 构造器:

protected AbstractChannel(Channel parent, ChannelId id) {
    this.parent = parent;
    this.id = id;
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}

AbstractChannel有一个pipeline 字段,在构造器中会初始化它为Defaul tChannelPipeline的实例.这里的代码就印证了一点:每个Channel都有一个ChannelPipeline.接着我们跟踪一下DefaultChannelPipeline的初始化过程。

首先进入到Defaul tChannelPipeline 构造器中:

protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);

    tail = new TailContext(this);
    head = new HeadContext(this);

    head.next = tail;
    tail.prev = head;
}

可以看到,在DefaultChannelPipeline的构造方法中,将传入的channel 赋值给字段this. channel,接着又实例化了两个特殊的字段: tail与head. 这两个字段是一个双向链表的头和尾.其实在Defaul tChannelPipeline中,维 护了一个以AbstractChanne lHandlexContext为节点的双向链表,这个链表是Netty 实现Pipeline 机制的关键.
head实现了Channel InboundHandler,而tail 实现了Channe l0utboundHandler接口,并且它们都实现了ChannelHandlerContext 接口,因此可以说head和tail即是一个ChannelHandler, 又是一个ChannelHandlerContext.
接着看一下HeadContext的构造器:

HeadContext(DefaultChannelPipeline pipeline) {
    super(pipeline, null, HEAD_NAME, false, true);
    unsafe = pipeline.channel().unsafe();
    setAddComplete();
}

它调用了父类AbstractChanne lHandlerContext 的构造器,并传入参数inbound = false, outbound = true.

TailContext的构造器与HeadContext的相反它调用了父类AbstractChanne lHandlerContext的构造器,并传入参数inbound = true, outbound = false.
即header 是一个outboundHandler, 而tail 是一个inboundHandler,关于这一点,大家要特别注意,因为在后面的分析中,我们会反复用到inbound 和outbound 这两个属性.

2.1. 3 ChannelInitializer的添加

前面我们已经分析了Channel 的组成,其中我们了解到,最开始的时候ChannelPipeline 中含有两个Channe lHandl erContext(同时也是ChannelHandler), 但是这个Pipeline 并不能实现什么特殊的功能,因为我们还没有给它添加自定义的ChannelHandler.
通常来说,我们在初始化Bootstrap, 会添加我们自定义的ChannelHandler, 就以我们熟悉的
ChatClient来举例吧:

Bootstrap bootstrap = new Bootstrap();
bootstrap.group(workerGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new IMDecoder());
        socketChannel.pipeline().addLast(new IMEncoder());
        socketChannel.pipeline().addLast(chatClientHandler);
    }
});
ChannelFuture channelFuture = bootstrap.connect(this.host, this.port).sync();
channelFuture.channel().closeFuture().sync();

上面代码的初始化过程,相信大家都不陌生.在调用handler时,传入了ChannelInitializer对象,它提供了一个initChannel 方法供我们初始化ChannelHandler.
那么这个初始化过程是怎样的呢?下面我们就来揭开它的神秘面纱.
ChannelInitializer实现了ChannelHandler, 那么它是在什么时候添加到ChannelPipeline中的呢?进行了一番搜索后,我们发现它是在Bootstrap.init 方法中添加到ChannelPipeline中的.
其代码如下:

void init(Channel channel) throws Exception {
    ChannelPipeline p = channel.pipeline();
    p.addLast(config.handler());

    final Map<ChannelOption<?>, Object> options = options0();
    synchronized (options) {
        for (Entry<ChannelOption<?>, Object> e: options.entrySet()) {
            try {
                if (!channel.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
                    logger.warn("Unknown channel option: " + e);
                }
            } catch (Throwable t) {
                logger.warn("Failed to set a channel option: " + channel, t);
            }
        }
    }

    final Map<AttributeKey<?>, Object> attrs = attrs0();
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
        }
    }
}

上面的代码将handler() 返回的ChannelHandler 添加到Pipeline 中,而handler() 返回的是handler其实就是我们在初始化Bootstrap 调用handler 设置的ChannelInitializer实例,因此这里就是将ChannelInitializer 插入到了Pipeline 的末端.
此时Pipeline 的结构如下图所示:

image-20200818165548308

我们刚才提到,在Bootstrap. init中会调用p. addLast()方法,将ChannelInitializer 插入到链表末端:

	@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 the registered is false it means that the channel was not registered on an eventloop yet.
            // In this case we add the context to the pipeline and add a task that will call
            // ChannelHandler.handlerAdded(...) once the channel is registered.
            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;
    }
private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
    return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler);
}

addLast有很多重载的方法,我们关注这个比较重要的方法就可以了。
上面的addLast 方法中,首先检查这个ChannelHandler 的名字是否是重复的,如果不重复的话,则调用newContext方法为这个Handler 创建一个对应的Defaul tChanne lHandl erContext实例,并与之关联起来(Context中有一个handler 属性保存着对应的Handler 实例).
为了添加一个handler 到pipeline 中,必须把此handler 包装成Channe lHandl erContext.
因此在,上面的代码中我们可以看到新实例化了一个newCtx 对象,并将handler作为参数传递到构造方法中.那么我们来看一下实例化的DefaultChanne lHandlerContext到底有什么玄机吧.
首先看它的构造器:

DefaultChannelHandlerContext(
        DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
    super(pipeline, executor, name, isInbound(handler), isOutbound(handler));
    if (handler == null) {
        throw new NullPointerException("handler");
    }
    this.handler = handler;
}

Defaul tChannelHandlerContext的构造器中,调用了两个很有意思的方法: isInbound 与isOutbound,这两个方法是做什么的呢?

private static boolean isInbound(ChannelHandler handler) {
    return handler instanceof ChannelInboundHandler;
}

private static boolean isOutbound(ChannelHandler handler) {
    return handler instanceof ChannelOutboundHandler;
}

从源码中可以看到,当一个handler 实现了Channel InboundHandler接口,则isInbound 返回真;相似地,当一个handler 实现了Channe l0utboundHandler 接口,则isOutbound 就返回真.
而这两个boolean 变量会传递到父类AbstractChanne lHandlerContext中,并初始化父类的两个字段: inbound 与outbound.
那么这里的ChannelInitializer 所对应的Defaul tChanne lHandlerContext的inbound 与inbound字段分别是什么呢?那就看一下ChannelInitializer到底实现了哪个接口不就行了?
如下是ChannelInitializer的类层次结构图:

image-20200818174802199

可以清楚地看到,ChannelInitializer 仅仅实现了Channel InboundHandler接口,因此这里实例化的Defaul tChanne lHandlerContext的inbound = true, outbound = false.不就是inbound 和outbound 两个字段嘛,为什么需要这么大费周章地分析一番?其实这两个字段关系到pipeline的事件的流向与分类求因此是十分关键的,不过我在这里先卖个关子,后面我们再来详细分析这两个字段所起的作用.在这里,我暂且只需要记住,ChannelInitializer所对应的Defaul tChanne lHandlerContext的inbound = true, outbound= false 即可.
当创建好Context后,就将这个Context插入到Pipeline 的双向链表中:

private void addLast0(AbstractChannelHandlerContext newCtx) {
    AbstractChannelHandlerContext prev = tail.prev;
    newCtx.prev = prev;
    newCtx.next = tail;
    prev.next = newCtx;
    tail.prev = newCtx;
}
2.1.4自定义ChannelHandler 的添加过程

前面我们已经分析了一个ChannelInitializer 如何插入到Pipeline 中的,接下来就来探讨一下ChannelInitializer 在哪里被调用,ChannelInitializer 的作用,以及我们自定义的ChannelHandler是如何插入到Pipeline 中的.
现在我们再简单地复习一下Channel的注册过程:
1、首先在AbstractBootstrap. ini tAndRegister中,通过group(). regi ster (channel),调用Multi threadEventLoopGroup. register方法
2、在MultithreadEventLoopGroup. register 中,通过next()获取一个可用的SingleThreadEventLoop,然后调用它的register

3、在SingleThreadEventLoop. register 中,通过channel. unsafe (). register(this,promise)来获取channel 的unsafe() 底层操作对象,然后调用它的register.
4、在AbstractUnsafe. register方法中,调用register0 方法注册Channel
5、在. AbstractUnsafe. register0中,调用AbstractNi oChannel#doRegister方法
6、AbstractNi oChannel. doRegister方法通过javaChannel (). register (eventLoop (). selector, 0,this) 将Channel 对应的Java NIO
SockerChannel注册到一个eventLoop 的Selector 中,并且将当前Channel作为attachment.
而我们自定义ChannelHandler 的添加过程,发生在AbstractUnsafe. register0中,在这个方法中调用了pipeline.fireChannelRegistered()方法,其实现如下:

@Override
public final ChannelPipeline fireChannelRegistered() {
    AbstractChannelHandlerContext.invokeChannelRegistered(head);
    return this;
}

再看AbstractChannelHandl erContext. invokeChannelRegistered方法:

static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelRegistered();
    } else {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                next.invokeChannelRegistered();
            }
        });
    }
}

很显然,这个代码会从head开始遍历Pipelhe 的双向链表,然后找到第一个属性inbound为true的 ChannelHandlerContext实例.想起来了没?我们在前面分析ChannelInitializer时,花了大量的笔墨来分析了inbound和outbound属性,你看现在这里就用上了.回想一下,ChannelInitializer实现了ChannelInboudllandler,因此它所对应的 ChannelllandlerContext的inbound属性就是true,因此这里返回就是 ChannelInitializer实例所对应的ChannelHandlerContext.即:

image-20200818201108886

当获取到inbound的 Context后,就调用它的invokeChannelRegistered方法:

private void invokeChannelRegistered() {
    if (invokeHandler()) {
        try {
            ((ChannelInboundHandler) handler()).channelRegistered(this);
        } catch (Throwable t) {
            notifyHandlerException(t);
        }
    } else {
        fireChannelRegistered();
    }
}

我们已经强调过了,每个 ChannelHandler都与一个ChannelHandlerContext 关联,我们可以通过ChannelHandlerContext获取到对应的ChannelHandler.因此很显然了,这里handler()返回的,其实就是一开始我们实例化的 ChannelInitializer对象,并接着调用了ChannelInitializer.channelRegistered方法.看到这里,是否会觉得有点眼熟呢?ChannelInitializer.channelRegistered这个方法我们在一开始的时候已经大量地接触了,但是我们并没有深入地分析这个方法的调用过程,那么在这里读者朋友应该对它的调用有了更加深入的了解了吧

那么这个方法中又有什么玄机呢?继续看代码:

@Override
@SuppressWarnings("unchecked")
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    // Normally this method will never be called as handlerAdded(...) should call initChannel(...) and remove
    // the handler.
    if (initChannel(ctx)) {
        // we called initChannel(...) so we need to call now pipeline.fireChannelRegistered() to ensure we not
        // miss an event.
        ctx.pipeline().fireChannelRegistered();
    } else {
        // Called initChannel(...) before which is the expected behavior, so just forward the event.
        ctx.fireChannelRegistered();
    }
}
@SuppressWarnings("unchecked")
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
    if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
        try {
            initChannel((C) ctx.channel());
        } catch (Throwable cause) {
            // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...).
            // We do so to prevent multiple calls to initChannel(...).
            exceptionCaught(ctx, cause);
        } finally {
            remove(ctx);
        }
        return true;
    }
    return false;
}

initchannel(©ctx.channel());这个方法我们很熟悉了吧,它就是我们在初始化Bootstrap时,调用handler方法传入的匿名内部类所实现的方法:

bootstrap.handler(new ChannelInitializer<SocketChannel>() {
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new IMDecoder());
        socketChannel.pipeline().addLast(new IMEncoder());
        socketChannel.pipeline().addLast(chatClientHandler);
    }
});

因此当调用了这个方法后,我们自定义的 Channelllandler就插入到Pipeline 了,此时的Pipeline如下图所示:

image-20200818210454994

当添加了自定义的Channellandler后,会删除 ChannelInitializer这个ChannelHandler,即"ctx.pipeline().remove(this)",因此最后的 Pipeline如下:

image-20200818210733963

好了,到了这里,我们的自定义 ChannelHandler的添加过程也分析的查不多了

2.1.5 ChannelHandler的名字

我们注意到,pipeline.addXXX都有一个重载的方法,例如addLast,它有一个重载的版本是:

ChannelPipeline addLast(String name,ChannelHandler handler);

第一个参数指定了所添加的handler的名字(更准确地说是 ChannelllandlerContext的名字,不过我们通常是以 handler作为叙述的对象,因此说成handler的名字便于理解).那么handler的名字有什么用呢?如果我们不设置name,那么handler会有怎样的名字?

为了解答这些疑惑,老规矩,依然是从源码中找到答案.

我们还是以addLast 方法为例:

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

这个方法会调用重载的addLast 方法:

@Override
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
    if (handlers == null) {
        throw new NullPointerException("handlers");
    }

    for (ChannelHandler h: handlers) {
        if (h == null) {
            break;
        }
        addLast(executor, null, h);
    }

    return this;
}

第一个参数被设置为null,我们不关心它.第二参数就是这个handler的名字.看代码可知,在添加一个handler之前,需要调用checkMultiplicity方法来确定此 handler和已添加的handler的名字重复.

2.1.6自动生成handler的名字

如果我们调用的是如下的 addLast方法
ChannelPipeline addLast(ChannelHandler… handlers);
那么Netty会调用generateName为我们的handler自动生成一个名字:

private String filterName(String name, ChannelHandler handler) {
    if (name == null) {
        return generateName(handler);
    }
    checkDuplicateName(name);
    return name;
}
private String generateName(ChannelHandler handler) {
    Map<Class<?>, String> cache = nameCaches.get();
    Class<?> handlerType = handler.getClass();
    String name = cache.get(handlerType);
    if (name == null) {
        name = generateName0(handlerType);
        cache.put(handlerType, name);
    }

    // It's not very likely for a user to put more than one handler of the same type, but make sure to avoid
    // any name conflicts.  Note that we don't cache the names generated here.
    if (context0(name) != null) {
        String baseName = name.substring(0, name.length() - 1); // Strip the trailing '0'.
        for (int i = 1;; i ++) {
            String newName = baseName + i;
            if (context0(newName) == null) {
                name = newName;
                break;
            }
        }
    }
    return name;
}

而generateName会接着调用generateNameO来实际产生一个 handler的名字:

	private static String generateName0(Class<?> handlerType) {
        return StringUtil.simpleClassName(handlerType) + "#0";
    }

自动生成的名字的规则很简单,就是handler的简单类名加上“#O",因此我们的ChatClientHandler的名字就是"ChatClientHandler#O"

2.1.7关于Pipeline的事件传输机制

前面章节中,我们知道AbstractChannelHandlerContext中有inbound和outbound两个

boolean变量,分别用于标识Context所对应的 handler的类型,即:

1、inbound为真时,表示对应的Channelllandler实现了ChannelInboundllandler方法.
2、outbound为真时,表示对应的ChannelHlandler实现了ChannelOutboundHlandler方法.

这里大家肯定很疑惑了吧:那究竟这两个字段有什么作用呢?其实这还要从ChannelPipeline的传输的事件类型说起.
Netty的事件可以分为Inbound和Outbound事件.
inbound 事件和outbound事件的流向是不一样的,inbound 事件的流行是从下至上,而outbound刚好相反,是从上到下.并且inbound 的传递方式是通过调用相应的ChannelHandlerContext.fireIN_EVT()方法,而outbound方法的的传递方式是通过调用ChannelHandlerContext.OUT_EVT() 例如ChannellandlerContext.fireChannelRegistered()调用会发送一个 ChannelRegistered 的inbound给下一个ChannellandlerContext,而 ChannelHlandlerContext.bind调用会发送一个bind的outbound事件给下一个ChannelHandlerContext.

注意,如果我们捕获了一个事件,并且想让这个事件继续传递下去,那么需要调用Context相应的传播方法.

例如:

public void channelActive(ChannelHandlerContext ctx) throws Exception {
    this.ctx = ctx;
    IMMessage message = new IMMessage(IMP.LOGIN.getName(),System.currentTimeMillis(),this.nickName);
    sendMsg(message);
    logger.info("成功连接服务器,已执行登录动作");
    session();
}

上面的例子中,MyInboundHandler收到了一个 channelActive事件,它在处理后,如果希望将事件继续传播下去,那么需要接着调用ctx.fireChannelActive().

2.1.8 Outbound的操作(outbound operations of a channel)

Outbound事件都是请求事件(request event),即请求某件事情的发生,然后通过Outbound事件进行通知.
Outbound事件的传播方向是tail -> customContext ->head.
我们接下来以 connect事件为例,分析一下 Outbound事件的传播机制.

首先,当用户调用了Bootstrap.connect方法时,就会触发一个Connect请求事件,此调用会触发如下调用链:
Bootstrap.connect->Bootstrap. doResolveAndConnect->Bootstrap.doResolveAndConnectO->Bootstrap.doConnect->AbstractChannel.connect
继续跟踪的话,我们就发现,AbstractChannel.connect其实由调用了
DefaultChannelPipeline.connect方法:

@Override
public final ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
    return tail.connect(remoteAddress, localAddress);
}

可以看到,当outbound事件(这里是connect事件)传递到Pipeline后,它其实是以tail为起点开始传播的.

而tail.connect其实调用的是 AbstractChannelHandlerContext.connect方法:

@Override
public ChannelFuture connect(
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {

    if (remoteAddress == null) {
        throw new NullPointerException("remoteAddress");
    }
    if (!validatePromise(promise, false)) {
        // cancelled
        return promise;
    }

    final AbstractChannelHandlerContext next = findContextOutbound();
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeConnect(remoteAddress, localAddress, promise);
    } else {
        safeExecute(executor, new Runnable() {
            @Override
            public void run() {
                next.invokeConnect(remoteAddress, localAddress, promise);
            }
        }, promise, null);
    }
    return promise;
}

findContextOutbound()顾名思义,它的作用是以当前Context为起点,向Pipeline中的Context双向链表的前端寻找第一个outbound属性为真的Context(即关联着ChannelOutboundHandler的 Context),然后返回.

它的实现如下:

private AbstractChannelHandlerContext findContextOutbound() {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.prev;
    } while (!ctx.outbound);
    return ctx;
}

当我们找到了一个 outbound 的Context后,就调用它的invokeConnect方法,这个方法中会调用 Context所关联着的 ChannelHandler的connect方法:

private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
    if (invokeHandler()) {
        try {
            ((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    } else {
        connect(remoteAddress, localAddress, promise);
    }
}

如果用户没有重写ChannelHandler的connect方法,那么会调用ChannelOutboundHandlerAdapter所实现的方法:

@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
        SocketAddress localAddress, ChannelPromise promise) throws Exception {
    ctx.connect(remoteAddress, localAddress, promise);
}

我们看到,ChannelOutboundllandlerAdapter.connect仅仅调用了ctx.connect,而这个调用又回到了:

Context.connect ->Connect.findContextOutbound ->next.invokeConnect ->handler.connect ->Context.connect

这样的循环中,直到connect事件传递到DefaultChannelPipeline的双向链表的头节点,即head中.为什么会传递到head中呢?回想一下,head 实现了ChannelOutboundHlandler,因此它的 outbound属性是true.

因此当connect消息传递到head后,会将消息转递到对应的 ChannelHandler中处理,而恰因此当connect消息传递到head后,会将消息转递到对应的 ChannelHandler中处理,而恰好,head的handler()返回的就是head本身:

@Override
public ChannelHandler handler() {
    return this;
}

因此最终connect事件是在 head中处理的. head的connect事件处理方法如下:

@Override
public void connect(
        ChannelHandlerContext ctx,
        SocketAddress remoteAddress, SocketAddress localAddress,
        ChannelPromise promise) throws Exception {
    unsafe.connect(remoteAddress, localAddress, promise);
}

到这里,整个Connect请求事件就结束了.
下面以一幅图来描述一个整个Connect请求事件的处理过程:

我们仅仅以Connect请求事件为例,分析了Outbound 事件的传播过程,但是其实所有的outbound 的事件传播都遵循着一样的传播规律,同学们可以试着分析一下其他的outbound 事件,体会一下它们的传播过程.

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值