【九】dubbo源码分析之服务调用方发起调用+发送请求(入口InvokerInvocationHandler.invoke)

一、简介

dubbo的服务调用主要包括几个大的步骤

1.发送请求

2.编解码

3.服务降级

4.过滤器链处理

5.序列化

6.线程派发以及响应请求

 Dubbo 服务调用过程 

 

dubbo中Exchange 层为框架引入 Request 和 Response 语义 

二、服务消费者发起调用

dubbo服务调用支持的方式

同步调用(默认)(本篇讲述这个)

异步调用

1.有返回值

2.无返回值(不关心结果。直接返回一个空的 RpcResult)

触发的调用链

 

能够看到先是通过ReferenceAnnotationBeanPostProcessor$ReferenceBeanInvocationHandler.invoke-------->反射调用-------->DelegatingMethodAccessorImpl.invoke-------->NativeMethodAccessorImpl.invoke-------->代理类proxy0.sayHello--------> InvokerInvocationHandler.invoke 

代理类的代码 

Dubbo 默认使用 Javassist 框架为服务接口生成动态代理类,因此我们需要先将代理类进行反编译才能看到源码 

反编译后看一下代理类的代码

(反编译的方式看这章:dubbo源码分析之服务调用方refer(服务引用、创建invoker、创建代理、查看动态生成的.class文件)


package com.alibaba.dubbo.common.bytecode;
 
import com.alibaba.dubbo.common.bytecode.ClassGenerator.DC;
import com.alibaba.dubbo.rpc.service.EchoService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import org.apache.dubbo.samples.api.client.HelloService;
 
public class proxy0 implements DC, HelloService, EchoService {

    // 方法数组
    public static Method[] methods;
    private InvocationHandler handler;
 
    public proxy0(InvocationHandler var1) {
        this.handler = var1;
    }
 
    public proxy0() {
    }
 
    public String sayHello(String var1) {

         // 将参数存储到 Object 数组中
        Object[] var2 = new Object[]{var1};

        // 调用 InvocationHandler 实现类的 invoke 方法得到调用结果
        Object var3 = this.handler.invoke(this, methods[0], var2);

        // 返回调用结果
        return (String)var3;
    }
 

     /** 回声测试方法 */
    public Object $echo(Object var1) {
        Object[] var2 = new Object[]{var1};
        Object var3 = this.handler.invoke(this, methods[1], var2);
        return (Object)var3;
    }

 主要做了几件事:

1.将运行时参数存储到数组中

2.调用 InvocationHandler 接口实现类的 invoke 方法,得到调用结果

3.将结果转型并返回给调用方。

跟进去看一下这个invoke方法

InvokerInvocationHandler.invoke 

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();

        // 拦截定义在 Object 类中的方法(未被子类重写),比如 wait/notify
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(invoker, args);
        }

        // 如果 toString、hashCode 和 equals 等方法被子类重写了,这里也直接调用
        if ("toString".equals(methodName) && parameterTypes.length == 0) {
            return invoker.toString();
        }
        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
            return invoker.hashCode();
        }
        if ("equals".equals(methodName) && parameterTypes.length == 1) {
            return invoker.equals(args[0]);
        }

        // 将 method 和 args 封装到 RpcInvocation 中,并执行后续的调用
        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }

主要做了几件事:

1.如果调用的方法是属于Object的,比如wait/notify,那么直接调用AnnotationHelloServiceConsumer类的该方法

2.如果调用的方法是toString  hashCode equals,那么调用invoker对应的该方法

此处的invoker是

3.如果调用的是服务提供方的方法,则invoker.invoke进行RPC远程调用

MockClusterInvoker.invoke

public Result invoke(Invocation invocation) throws RpcException {
        Result result = null;


         // 获取 mock 配置值
        String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
        if (value.length() == 0 || value.equalsIgnoreCase("false")) {

            //debug的时候走的这里
            // 无 mock 逻辑,直接调用其他 Invoker 对象的 invoke 方法,
            // 比如 FailoverClusterInvoker
            //no mock
            result = this.invoker.invoke(invocation);
        } else if (value.startsWith("force")) {
            if (logger.isWarnEnabled()) {
                logger.info("force-mock: " + invocation.getMethodName() + " force-mock enabled , url : " + directory.getUrl());
            }

            // force:xxx 直接执行 mock 逻辑,不发起远程调用
            //force:direct mock
            result = doMockInvoke(invocation, null);
        } else {

             // fail:xxx 表示消费方对调用服务失败后,再执行 mock 逻辑,不抛出异常
            //fail-mock
            try {

                 // 调用其他 Invoker 对象的 invoke 方法
                result = this.invoker.invoke(invocation);
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                } else {
                    if (logger.isWarnEnabled()) {
                        logger.warn("fail-mock: " + invocation.getMethodName() + " fail-mock enabled , url : " + directory.getUrl(), e);
                    }

                     // 调用失败,执行 mock 逻辑
                    result = doMockInvoke(invocation, e);
                }
            }
        }
        return result;
    }

 主要做了几件事:

1.获取 mock 配置值

2.如果无 mock 逻辑,直接调用其他 Invoker 对象的 invoke 方法,比如 FailoverClusterInvoker.invoke

3.如果mock配置是 force:xxx 直接调用doMockInvoke方法执行 mock 逻辑,不发起远程调用

4. 如果Mock配置是 fail:xxx 表示消费方使用Invoker.invoke调用服务失败后,再使用doMockInvoke执行 mock 逻辑,不抛出异常

其中 doMockInvoke跟服务降级相关,后面再专门写一篇说这个

AbstractClusterInvoker.invoke

    @Override
    public Result invoke(final Invocation invocation) throws RpcException {
        checkWhetherDestroyed();
        LoadBalance loadbalance = null;

        //将RpcInvocation与attachments绑定
        // binding attachments into invocation.
        Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
        if (contextAttachments != null && contextAttachments.size() != 0) {
            ((RpcInvocation) invocation).addAttachments(contextAttachments);
        }


       //调用Directory的list方法,得到符合路由条件的invoker
        List<Invoker<T>> invokers = list(invocation);

        //得到负载均衡器LoadBalance
        if (invokers != null && !invokers.isEmpty()) {
            loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
                    .getMethodParameter(RpcUtils.getMethodName(invocation), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
        }
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);

        //执行调用
        return doInvoke(invocation, invokers, loadbalance);
    }

做了几件事:

1.将RpcInvocation与attachments绑定

执行后

2.调用Directory的list方法,得到符合路由条件的invoker

3.得到负载均衡器LoadBalance

debug的时候得到的是

估计默认就是RandomLoadBalance

4.RpcUtils.attachInvocationIdIfAsync方法

执行后

看起来没什么区别

4.执行调用failoverClusterInvoker.doInvoke

 FailoverClusterInvoker.doInvoke

 这个类的这个方法上一章分析过了

这个方法里面再往后面调用,的调用栈是:

FailoverClusterInvoker.doInvoke-------->InvokerWrapper.invoke-------->ListenerInvoker.invoke-------->ProtocolFilterWrapper.invoke-------->ConsumerContextFilter.invoke-------->ProtocolFilterWrapper.invoke-------->FutureFilter.invoke-------->ProtocolFilter.invoke-------->MonitorFilter.invoke-------->AbstractInvoker.invoke

那下面我们来看 

AbstractInvoker.invoke

    public Result invoke(Invocation inv) throws RpcException {
        // if invoker is destroyed due to address refresh from registry, let's allow the current invoke to proceed
        if (destroyed.get()) {
            logger.warn("Invoker for service " + this + " on consumer " + NetUtils.getLocalHost() + " is destroyed, "
                    + ", dubbo version is " + Version.getVersion() + ", this invoker should not be used any longer");
        }

        RpcInvocation invocation = (RpcInvocation) inv;

         // 设置 Invoker
        invocation.setInvoker(this);
        if (attachment != null && attachment.size() > 0) {

            // 设置 attachment
            invocation.addAttachmentsIfAbsent(attachment);
        }
        Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
        if (contextAttachments != null && contextAttachments.size() != 0) {
            /**
             * invocation.addAttachmentsIfAbsent(context){@link RpcInvocation#addAttachmentsIfAbsent(Map)}should not be used here,
             * because the {@link RpcContext#setAttachment(String, String)} is passed in the Filter when the call is triggered
             * by the built-in retry mechanism of the Dubbo. The attachment to update RpcContext will no longer work, which is
             * a mistake in most cases (for example, through Filter to RpcContext output traceId and spanId and other information).
             */
            // 添加 contextAttachments 到 RpcInvocation#attachment 变量中
            invocation.addAttachments(contextAttachments);
        }
        if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)) {
             // 设置异步信息到 RpcInvocation#attachment 中
            invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
        }
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);


        try {

            // 抽象方法,由子类实现
            return doInvoke(invocation);
        } catch (InvocationTargetException e) { // biz exception
            Throwable te = e.getTargetException();
            if (te == null) {
                return new RpcResult(e);
            } else {
                if (te instanceof RpcException) {
                    ((RpcException) te).setCode(RpcException.BIZ_EXCEPTION);
                }
                return new RpcResult(te);
            }
        } catch (RpcException e) {
            if (e.isBiz()) {
                return new RpcResult(e);
            } else {
                throw e;
            }
        } catch (Throwable e) {
            return new RpcResult(e);
        }
    }

做了几件事: 

 1.添加信息到 RpcInvocation#attachment 变量中

添加完后

2.调用 子类doInvoke 执行后续的调用。debugg的时候就是调用的 DubboInvoker 类的doInvoke 

DubboInvoker.doInvoke

该方法是 Dubbo 对同步和异步调用的处理逻辑 

同步调用模式下,由框架自身调用 ResponseFuture 的 get 方法。

异步调用模式下,则由用户调用该方法。

protected Result doInvoke(final Invocation invocation) throws Throwable {
        RpcInvocation inv = (RpcInvocation) invocation;
        final String methodName = RpcUtils.getMethodName(invocation);

        // 设置 path 和 version 到 attachment 中
        inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
        inv.setAttachment(Constants.VERSION_KEY, version);

        ExchangeClient currentClient;
        if (clients.length == 1) {

            // 从 clients 数组中获取 ExchangeClient
            currentClient = clients[0];
        } else {
            currentClient = clients[index.getAndIncrement() % clients.length];
        }
        try {

            // 获取异步配置
            boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);

            // isOneway 为 true,表示“单向”通信
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);

             // 异步无返回值
            if (isOneway) {
                boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);

                 // 发送请求
                currentClient.send(inv, isSent);

                // 设置上下文中的 future 字段为 null
                RpcContext.getContext().setFuture(null);

                // 返回一个空的 RpcResult
                return new RpcResult();


             // 异步有返回值
            } else if (isAsync) {

                // 发送请求,并得到一个 ResponseFuture 实例
                ResponseFuture future = currentClient.request(inv, timeout);

                // 设置 future 到上下文中
                RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));

                // 暂时返回一个空结果
                return new RpcResult();

             // 同步调用
            } else {
                RpcContext.getContext().setFuture(null);

                // 发送请求,得到一个 ResponseFuture 实例,并调用该实例的 get 方法进行等待
                return (Result) currentClient.request(inv, timeout).get();
            }
        } catch (TimeoutException e) {
            throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        } catch (RemotingException e) {
            throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }

主要做了几件事:

1.设置attachment

2.得到Exchange(debug的时候这个Exchange是ReferenceCountExchangeClient,这个底层发送的逻辑后面专门写一篇来说),调用它的send或者request方法进行远程调用

它是做底层操作的,发起远程调用就是它负责

3.根据标识执行对应的调用方式

比如是,同步调用、异步有返回值调用、异步无返回值调用

默认是同步调用,debug的时候也是走的同步调用

 而debug的时候这个currentClient.request得到的值是DefaultFuture

DefaultFuture

public class DefaultFuture implements ResponseFuture {

    private static final Logger logger = LoggerFactory.getLogger(DefaultFuture.class);

    private static final Map<Long, Channel> CHANNELS = new ConcurrentHashMap<Long, Channel>();

    private static final Map<Long, DefaultFuture> FUTURES = new ConcurrentHashMap<Long, DefaultFuture>();

    static {
        Thread th = new Thread(new RemotingInvocationTimeoutScan(), "DubboResponseTimeoutScanTimer");
        th.setDaemon(true);
        th.start();
    }

    // invoke id.
    private final long id;
    private final Channel channel;
    private final Request request;
    private final int timeout;
    private final Lock lock = new ReentrantLock();
    private final Condition done = lock.newCondition();
    private final long start = System.currentTimeMillis();
    private volatile long sent;
    private volatile Response response;
    private volatile ResponseCallback callback;

    public DefaultFuture(Channel channel, Request request, int timeout) {
        this.channel = channel;
        this.request = request;

        // 获取请求 id,这个 id 很重要,后面还会见到
        this.id = request.getId();
        this.timeout = timeout > 0 ? timeout : channel.getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
        

        // put into waiting map.

        // 存储 <requestId, DefaultFuture> 映射关系到 FUTURES 中
        FUTURES.put(id, this);
        CHANNELS.put(id, channel);
    }

    @Override
    public Object get(int timeout) throws RemotingException {
        if (timeout <= 0) {
            timeout = Constants.DEFAULT_TIMEOUT;
        }

        // 检测服务提供方是否成功返回了调用结果
        if (!isDone()) {
            long start = System.currentTimeMillis();
            lock.lock();
            try {

                // 循环检测服务提供方是否成功返回了调用结果
                while (!isDone()) {

                    // 如果调用结果尚未返回,这里等待一段时间
                    done.await(timeout, TimeUnit.MILLISECONDS);

                    // 如果调用结果成功返回,或等待超时,此时跳出 while 循环,执行后续的逻辑
                    if (isDone() || System.currentTimeMillis() - start > timeout) {
                        break;
                    }
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }

           // 如果调用结果仍未返回,则抛出超时异常
            if (!isDone()) {
                throw new TimeoutException(sent > 0, channel, getTimeoutMessage(false));
            }
        }

        // 返回调用结果
        return returnFromResponse();
    }


    @Override
    public boolean isDone() {

        // 通过检测 response 字段为空与否,判断是否收到了调用结果
        return response != null;
    }


    private Object returnFromResponse() throws RemotingException {
        Response res = response;
        if (res == null) {
            throw new IllegalStateException("response cannot be null");
        }

        // 如果调用结果的状态为 Response.OK,则表示调用过程正常,服务提供方成功返回了调用结果
        if (res.getStatus() == Response.OK) {
            return res.getResult();
        }

        // 抛出异常
        if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
            throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
        }
        throw new RemotingException(channel, res.getErrorMessage());
    }

}

主要做了1件事

当服务消费者还未接收到调用结果时,用户线程调用 get 方法会被阻塞住。

同步调用模式下,框架获得 DefaultFuture 对象后,会立即调用 get 方法进行等待。

而异步模式下则是将该对象封装到 FutureAdapter 实例中,并将 FutureAdapter 实例设置到 RpcContext 中,供用户使用。

FutureAdapter 是一个适配器,用于将 Dubbo 中的 ResponseFuture 与 JDK 中的 Future 进行适配。

这样当用户线程调用 Future 的 get 方法时,经过 FutureAdapter 适配,最终会调用 ResponseFuture 实现类对象的 get 方法,也就是 DefaultFuture 的 get 方法。

三、服务调用方发送请求

dubbo中Exchange 层为框架引入 Request 和 Response 语义

ReferenceCountExchangeClient类

/**
 * dubbo protocol support class.
 */
@SuppressWarnings("deprecation")
final class ReferenceCountExchangeClient implements ExchangeClient {
 
    private final URL url;
    private final AtomicInteger refenceCount = new AtomicInteger(0);
 
    //    private final ExchangeHandler handler;
    private final ConcurrentMap<String, LazyConnectExchangeClient> ghostClientMap;
    private ExchangeClient client;
 
 
    public ReferenceCountExchangeClient(ExchangeClient client, ConcurrentMap<String, LazyConnectExchangeClient> ghostClientMap) {
        this.client = client;
        refenceCount.incrementAndGet();
        this.url = client.getUrl();
        if (ghostClientMap == null) {
            throw new IllegalStateException("ghostClientMap can not be null, url: " + url);
        }
        this.ghostClientMap = ghostClientMap;
    }
 
    @Override
    public ResponseFuture request(Object request) throws RemotingException {
        return client.request(request);
    }
 
    @Override
    public ResponseFuture request(Object request, int timeout) throws RemotingException {
        return client.request(request, timeout);
    }
 
 
    @Override
    public void send(Object message) throws RemotingException {
        client.send(message);
    }
 
    @Override
    public void send(Object message, boolean sent) throws RemotingException {
        client.send(message, sent);
    }
 
    public void incrementAndGetCount() {
        refenceCount.incrementAndGet();
    }
}

 ReferenceCountExchangeClient 内部仅实现了一个引用计数的功能,其他方法并无复杂逻辑,均是直接调用被装饰对象的相关方法。

每当该对象被引用一次 referenceCount 都会进行自增

每当 close 方法被调用时,referenceCount 进行自减。

debug的时候是该类的request方法被调用,该方法里面就是直接调用被装饰对象的request方法,这里的被装饰对象HeaderExchangeClient

HeaderExchangeClient.request

HeaderExchangeClient 中很多方法只有一行代码,即调用 HeaderExchangeChannel 对象的同签名方法。

HeaderExchangeClient 的用处是封装了一些关于心跳检测的逻辑。 

    @Override
    public ResponseFuture request(Object request, int timeout) throws RemotingException {
        return channel.request(request, timeout);
    }

HeaderExchangeChannel.request

@Override
    public ResponseFuture request(Object request, int timeout) throws RemotingException {
        if (closed) {
            throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
        }
        // create request.
        Request req = new Request();
        req.setVersion(Version.getProtocolVersion());
        req.setTwoWay(true);
        req.setData(request);
        DefaultFuture future = new DefaultFuture(channel, req, timeout);
        try {
            channel.send(req);
        } catch (RemotingException e) {
            future.cancel();
            throw e;
        }
        return future;
    }

 做了几件事

1.首先定义了一个 Request 对象

2.然后再将该对象传给 NettyClient 的 send 方法,进行后续的调用。

这里的channel.send方法,看一下channel是什么

注意:NettyClient 中并未实现 send 方法,该方法继承自父类 AbstractPeer。

 AbstractPeer.send

    @Override
    public void send(Object message) throws RemotingException {
        send(message, url.getParameter(Constants.SENT_KEY, false));
    }

调用子类的send方法,这里的子类是AbstractClient

AbstractClient.send

@Override
    public void send(Object message, boolean sent) throws RemotingException {
 
        if (send_reconnect && !isConnected()) {
           //第一次debug的时候没有走这里
            connect();
        }
 
        // 获取 Channel,getChannel 是一个抽象方法,具体由子类实现
        Channel channel = getChannel();
        //TODO Can the value returned by getChannel() be null? need improvement.
        if (channel == null || !channel.isConnected()) {
            throw new RemotingException(this, "message can not send, because channel is closed . url:" + getUrl());
        }
 
        // 继续向下调用
        channel.send(message, sent);
    }

 Dubbo 使用 Netty 作为底层的通信框架。跟一下这个getChannel方法

NettyClient.getChannel

    @Override
    protected com.alibaba.dubbo.remoting.Channel getChannel() {
        Channel c = channel;
        if (c == null || !c.isActive())
            return null;
 
        // 获取一个 NettyChannel 类型对象
        return NettyChannel.getOrAddChannel(c, getUrl(), this);
    }

看一下这个channel是什么

NettyChannel.getOrAddChannel

static NettyChannel getOrAddChannel(Channel ch, URL url, ChannelHandler handler) {
        if (ch == null) {
            return null;
        }
 
       // 尝试从集合中获取 NettyChannel 实例
        NettyChannel ret = channelMap.get(ch);
        if (ret == null) {
 
             // 如果 ret = null,则创建一个新的 NettyChannel 实例
            NettyChannel nettyChannel = new NettyChannel(ch, url, handler);
            if (ch.isActive()) {
 
               // 将 <Channel, NettyChannel> 键值对存入 channelMap 集合中
                ret = channelMap.putIfAbsent(ch, nettyChannel);
            }
            if (ret == null) {
                ret = nettyChannel;
            }
        }
        return ret;
    }

 NettyChannel.send

 @Override
    public void send(Object message, boolean sent) throws RemotingException {
        super.send(message, sent);
 
        boolean success = true;
        int timeout = 0;
        try {
 
            // 发送消息(包含请求和响应消息)
            ChannelFuture future = channel.writeAndFlush(message);
 
            // sent 的值源于 <dubbo:method sent="true/false" /> 中 sent 的配置值,有两种配置值:
        //   1. true: 等待消息发出,消息发送失败将抛出异常
        //   2. false: 不等待消息发出,将消息放入 IO 队列,即刻返回
        // 默认情况下 sent = false;
            if (sent) {
                timeout = getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
 
                // 等待消息发出,若在规定时间没能发出,success 会被置为 false
                success = future.await(timeout);
            }
            Throwable cause = future.cause();
            if (cause != null) {
                throw cause;
            }
        } catch (Throwable e) {
            throw new RemotingException(this, "Failed to send message " + message + " to " + getRemoteAddress() + ", cause: " + e.getMessage(), e);
        }
 
        // 若 success 为 false,这里抛出异常
        if (!success) {
            throw new RemotingException(this, "Failed to send message " + message + " to " + getRemoteAddress()
                    + "in timeout(" + timeout + "ms) limit");
        }
    }

这个channel.writeAndFlush(message);发送请求,再跟进去就进到netty中了。

在 Netty 中,出站数据在发出之前还需要进行编码操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值