dubbo服务者源码分期

 

开局一张图,内容全靠编
总的概括
è¿éåå¾çæè¿°
先说一下dubbo的服务端初始化过程
1.在serviceConfig里面组装配置参数;
2.获取到对外提供服务的接口,实现类以及注册url交给ProcxyFactory生成本地代理invoker;当消费者请求过来的时候,最后都是交给invoker去执行,然后invoker通过反射调用真正的实例;
3.生成完本地代理Invoker后,在DubboProtocol中对invoker进行暴露,先将invoker包装生成exporter,再将export作为value,服务接口和端口号组合成key存入到DubboProtocol的map中,这个可以看成是本地注册。
4.在DubboProtocol中生存开启netty通信生存ExchangeServer,ExchangeServer是负责底层通信的,将ip加端口号作为key,ExchangeServer为value存入到map中。ExchangeServer负责相同的ip端口号服务通信。
5.通过invoker里面的url信息获取获取注册中心,将服务者信息注册到注册中心,此节点存储了服务提供方ip、端口、group、接口名称、版本、应用名称,这样可以让消费者从注册中心获取到服务者的信息。
6.为了感知注册中心的一些配置变化,提供者会监听注册中心路径/dubbo/${interfaceClass}/configurators的节点,监听该节点在注册中心的一些配置信息变更。zookeeper注册中心通过zookeeper框架的监听回调接口进行监听(redis注册中心通过订阅命令(subscribe)监听),服务器缓存注册中心的配置,当配置发生变更时,服务会刷新本地缓存,代码在ZookeeperRegistry的doSubscribe方法。
 

源码分析
下面是dubbo服务端的配置文件

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans  
        http://www.springframework.org/schema/beans/spring-beans.xsd  
        http://code.alibabatech.com/schema/dubbo  
        http://code.alibabatech.com/schema/dubbo/dubbo.xsd ">   
    <!-- 具体的实现bean -->  
    <bean id="testService" class="com.ts.services.impl.TestServiceImpl" />  
    <!-- 提供方应用信息,用于计算依赖关系 -->  
    <dubbo:application name="provider"  />    
    <!-- 使用zookeeper注册中心暴露服务地址 -->  
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />
    <!-- 用dubbo协议在20880端口暴露服务 -->  
    <dubbo:protocol name="dubbo" port="29014" />  
    <!-- 声明需要暴露的服务接口 -->  
    <dubbo:service interface="com.ts.service.TestService" ref="testService" timeout="300"/>
</beans> 

spring会对dubbo:service标签进行解析生成serviceBean,serviceBean实现了ApplicationListener,重写了onApplicationEvent方法,当所有bean刷新完毕后,会调用onApplicationEvent方法,在onApplicationEvent方法中会调用ServiceConfig的export方法
ServiceConfig主要做了三件事
1.组装和检查配置文件里面配置的注册信息,接口信息和协议信息
2.让proxyFactory对要暴露的服务生成Invoker
3.将Invoker交给proctol暴露,开启netty通信,将invoker信息注册到注册中心方便消费者调用,并监听注册中心服务者的配置,
当服务者的配置改变后能动态刷新本地缓存

if (! Constants.SCOPE_NONE.toString().equalsIgnoreCase(scope)) {

            //配置不是remote的情况下做本地暴露 (配置为remote,则表示只暴露远程服务)
            if (!Constants.SCOPE_REMOTE.toString().equalsIgnoreCase(scope)) {
                exportLocal(url);
            }
            //如果配置不是local则暴露为远程服务.(配置为local,则表示只暴露远程服务)
            if (! Constants.SCOPE_LOCAL.toString().equalsIgnoreCase(scope) ){
                if (logger.isInfoEnabled()) {
                    logger.info("Export dubbo service " + interfaceClass.getName() + " to url " + url);
                }
                //当配置的有注册中心的时候
                if (registryURLs != null && registryURLs.size() > 0
                        && url.getParameter("register", true)) {
                    for (URL registryURL : registryURLs) {
                        url = url.addParameterIfAbsent("dynamic", registryURL.getParameter("dynamic"));
                        URL monitorUrl = loadMonitor(registryURL);
                        if (monitorUrl != null) {
                            url = url.addParameterAndEncoded(Constants.MONITOR_KEY, monitorUrl.toFullString());
                        }
                        if (logger.isInfoEnabled()) {
                            logger.info("Register dubbo service " + interfaceClass.getName() + " url " + url + " to registry " + registryURL);
                        }
                        //生成本地代理
                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));
                        //进行服务暴露和注册
                        Exporter<?> exporter = protocol.export(invoker);
                        exporters.add(exporter);
                    }
                } else {
                    //没有注册中心,直接连接消费者端
                    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);

                    Exporter<?> exporter = protocol.export(invoker);
                    exporters.add(exporter);
                }
            }
        }

1.生成本地代理Invoker 

Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);

上面是通过proxyFactory对ref,也就是接口的实现类生成代理invoker,proxyFactory默认的是JavassistProxyFactory

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper类不能正确处理带$的类名
        final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName, 
                                      Class<?>[] parameterTypes, 
                                      Object[] arguments) throws Throwable {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }

可看到getInvoker方法返回的是一个重写了AbstractProxyInvoker doInvoker方法的实例,在doInvoker方法里调用了wapper.invoker方法,这里wapper.invoker方法运用javassist动态编码技术形成新的代理类。记住这里生成的invoker就是对服务端要暴露的服务的一种代理,这样是为了解耦dubbo和要暴露的服务,让所有客户端的请求先交给Invoker处理,invoker再根据客户端的请求信息利用反射调用真正的服务。
 

2.对代理进行暴露

 Exporter<?> exporter = protocol.export(invoker);

这里的protocol是通过spi机制动态生成的,它会返回ProtocolListenerWrapper->ListenerExporterWrapper->RegistryProtocol对象链,对于registry协议,两个Wrapper都不会做任何处理,会直接调到RegistryProtocol.export方法。

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        //export invoker,进行暴露,也就是开启了netty通信
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
        //registry provider   获取注册中心
        final Registry registry = getRegistry(originInvoker);
        final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
        //注册服务者节点
        registry.register(registedProviderUrl);
        // 订阅override数据
        // FIXME 提供者订阅时,会影响同一JVM即暴露服务,又引用同一服务的的场景,因为subscribed以服务名为缓存的key,导致订阅信息覆盖。
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        //监听服务者在注册中心的配置,当配置改变的时候能刷新本地缓存
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        //保证每次export都返回一个新的exporter实例
        return new Exporter<T>() {
            public Invoker<T> getInvoker() {
                return exporter.getInvoker();
            }
            public void unexport() {
            	try {
            		exporter.unexport();
            	} catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	registry.unregister(registedProviderUrl);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	overrideListeners.remove(overrideSubscribeUrl);
                	registry.unsubscribe(overrideSubscribeUrl, overrideSubscribeListener);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
            }
        };
    }

doLocalExport方法

private <T> ExporterChangeableWrapper<T>  doLocalExport(final Invoker<T> originInvoker){
        String key = getCacheKey(originInvoker);
        //exporter代理,建立返回的exporter与protocol export出的exporter的对应关系,在override时可以进行关系修改.
        ExporterChangeableWrapper<T> exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
        if (exporter == null) {
            synchronized (bounds) {
                exporter = (ExporterChangeableWrapper<T>) bounds.get(key);
                if (exporter == null) {
                    //声明一个invoker的代理
                    final Invoker<?> invokerDelegete = new InvokerDelegete<T>(originInvoker, getProviderUrl(originInvoker));
                    //利用dubboProtocol对invoker进行暴露
                    exporter = new ExporterChangeableWrapper<T>((Exporter<T>)protocol.export(invokerDelegete), originInvoker);
                    bounds.put(key, exporter);
                }
            }
        }
        return (ExporterChangeableWrapper<T>) exporter;
    }

进入dubboproctol的export方法

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        //dubbo://202.106.199.34:29014/com.ts.service.TestService?anyhost=true&application=provider&dubbo=2.5.3&interface=com.ts.service.TestService&methods=getName&pid=59736&side=provider&timeout=300&timestamp=1563845476832
        URL url = invoker.getUrl();
        // export service.  key的值:com.ts.service.TestService:29014
        //key其实包括接口名,端口号,以及服务的版本号,分组名
        String key = serviceKey(url);
        DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);
        //将key和export存入map中,这样可以根据客户端发送过来的invocation解析得到取出哪一个export
        exporterMap.put(key, exporter);
        
        //export an stub service for dispaching event
        Boolean isStubSupportEvent = url.getParameter(Constants.STUB_EVENT_KEY,Constants.DEFAULT_STUB_EVENT);
        Boolean isCallbackservice = url.getParameter(Constants.IS_CALLBACK_SERVICE, false);
        if (isStubSupportEvent && !isCallbackservice){
            String stubServiceMethods = url.getParameter(Constants.STUB_EVENT_METHODS_KEY);
            if (stubServiceMethods == null || stubServiceMethods.length() == 0 ){
                if (logger.isWarnEnabled()){
                    logger.warn(new IllegalStateException("consumer [" +url.getParameter(Constants.INTERFACE_KEY) +
                            "], has set stubproxy support event ,but no stub methods founded."));
                }
            } else {
                stubServiceMethodsMap.put(url.getServiceKey(), stubServiceMethods);
            }
        }

        openServer(url);
        
        return exporter;
    }

openServer方法,生成一个ExchangeServer,ExchangeServer负责底层通信

private void openServer(URL url) {
        // find server.  202.106.199.34:29014
        String key = url.getAddress();
        //client 也可以暴露一个只有server可以调用的服务。
        boolean isServer = url.getParameter(Constants.IS_SERVER_KEY,true);
        if (isServer) {
            //同一个ip和端口号的服务都交给同一个ExchangeServer处理
        	ExchangeServer server = serverMap.get(key);
        	if (server == null) {
        		serverMap.put(key, createServer(url));
        	} else {
        		//server支持reset,配合override功能使用
        		server.reset(url);
        	}
        }
    }

createServer(url)方法

private ExchangeServer createServer(URL url) {
        //默认开启server关闭时发送readonly事件
        url = url.addParameterIfAbsent(Constants.CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString());
        //默认开启heartbeat
        url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
        String str = url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_SERVER);

        if (str != null && str.length() > 0 && ! ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str))
            throw new RpcException("Unsupported server type: " + str + ", url: " + url);

        url = url.addParameter(Constants.CODEC_KEY, Version.isCompatibleVersion() ? COMPATIBLE_CODEC_NAME : DubboCodec.NAME);
        ExchangeServer server;
        try {
            //开启netty通信
            server = Exchangers.bind(url, requestHandler);
        } catch (RemotingException e) {
            throw new RpcException("Fail to start server(url: " + url + ") " + e.getMessage(), e);
        }
        str = url.getParameter(Constants.CLIENT_KEY);
        if (str != null && str.length() > 0) {
            Set<String> supportedTypes = ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions();
            if (!supportedTypes.contains(str)) {
                throw new RpcException("Unsupported client type: " + str);
            }
        }
        return server;
    }

先看看requestHandler这个DubboProcto里面的内部类,这个类是处理接受从客户端来的请求,并通过invocation里面所带的参数组长key取出对应的export,再利用export里面的invoker处理请求
 

private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
        
        public Object reply(ExchangeChannel channel, Object message) throws RemotingException {
            if (message instanceof Invocation) {
                Invocation inv = (Invocation) message;
                //通过invocation获取invoker
                Invoker<?> invoker = getInvoker(channel, inv);
                //如果是callback 需要处理高版本调用低版本的问题
                if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))){
                    String methodsStr = invoker.getUrl().getParameters().get("methods");
                    boolean hasMethod = false;
                    if (methodsStr == null || methodsStr.indexOf(",") == -1){
                        hasMethod = inv.getMethodName().equals(methodsStr);
                    } else {
                        String[] methods = methodsStr.split(",");
                        for (String method : methods){
                            if (inv.getMethodName().equals(method)){
                                hasMethod = true;
                                break;
                            }
                        }
                    }
                    if (!hasMethod){
                        logger.warn(new IllegalStateException("The methodName "+inv.getMethodName()+" not found in callback service interface ,invoke will be ignored. please update the api interface. url is:" + invoker.getUrl()) +" ,invocation is :"+inv );
                        return null;
                    }
                }
                RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
                //执行具体的请求
                return invoker.invoke(inv);
            }
            throw new RemotingException(channel, "Unsupported request: " + message == null ? null : (message.getClass().getName() + ": " + message) + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
        }

.......
}
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException{
        boolean isCallBackServiceInvoke = false;
        boolean isStubServiceInvoke = false;
        int port = channel.getLocalAddress().getPort();
        String path = inv.getAttachments().get(Constants.PATH_KEY);
        //如果是客户端的回调服务.
        isStubServiceInvoke = Boolean.TRUE.toString().equals(inv.getAttachments().get(Constants.STUB_EVENT_KEY));
        if (isStubServiceInvoke){
            port = channel.getRemoteAddress().getPort();
        }
        //callback
        isCallBackServiceInvoke = isClientSide(channel) && !isStubServiceInvoke;
        if(isCallBackServiceInvoke){
            path = inv.getAttachments().get(Constants.PATH_KEY)+"."+inv.getAttachments().get(Constants.CALLBACK_SERVICE_KEY);
            inv.getAttachments().put(IS_CALLBACK_SERVICE_INVOKE, Boolean.TRUE.toString());
        }
        //端口号,接口服务,版本号,分组组合成一个key
        String serviceKey = serviceKey(port, path, inv.getAttachments().get(Constants.VERSION_KEY), inv.getAttachments().get(Constants.GROUP_KEY));
        //通过key获取到对应的export
        DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
        
        if (exporter == null)
            throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + inv);

        return exporter.getInvoker();
    }

下面看一下dubbo服务端是如何开启netty通信的

server = Exchangers.bind(url, requestHandler);
public static ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
        if (handler == null) {
            throw new IllegalArgumentException("handler == null");
        }
        url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
        //getExchanger(url) 获取到HeaderExchanger   
        //url的值是:dubbo://202.106.199.34:29014/com.ts.service.TestService?anyhost=true&application=provider&channel.readonly.sent=true&codec=dubbo&dubbo=2.5.3&heartbeat=60000&interface=com.ts.service.TestService&methods=getName&pid=59736&side=provider&timeout=300&timestamp=1563845476832
        return getExchanger(url).bind(url, handler);
    }

HeaderExchanger的bind方法

public ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException {
        return new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler))));
    }

可以看到这里是一个装饰模式的调用链,HeaderExchangeServer--->NettyServer--->DecodeHandler--->HeaderExchangeHandler-->ExchangeHandler。

Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))

 返回的是一个nettyservice,这个地方的调用链比想象的要复杂,只能浅尝辄止了。

在开启netty通信后,下面再看服务的注册和监听,RegistryProcotol的export方法

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {
        //export invoker
        final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker);
        //registry provider
        final Registry registry = getRegistry(originInvoker);
        final URL registedProviderUrl = getRegistedProviderUrl(originInvoker);
        //将服务提供者信息注册到注册中心,方便消费者能在注册中心订阅到服务
        registry.register(registedProviderUrl);
        // 订阅override数据
        // FIXME 提供者订阅时,会影响同一JVM即暴露服务,又引用同一服务的的场景,因为subscribed以服务名为缓存的key,导致订阅信息覆盖。
        final URL overrideSubscribeUrl = getSubscribedOverrideUrl(registedProviderUrl);
        final OverrideListener overrideSubscribeListener = new OverrideListener(overrideSubscribeUrl);
        overrideListeners.put(overrideSubscribeUrl, overrideSubscribeListener);
        //订阅服务者配置信息,这样当服务者配置信息发生改变的时候,能刷新本地缓存,重新生成export
        registry.subscribe(overrideSubscribeUrl, overrideSubscribeListener);
        //保证每次export都返回一个新的exporter实例
        return new Exporter<T>() {
            public Invoker<T> getInvoker() {
                return exporter.getInvoker();
            }
            public void unexport() {
            	try {
            		exporter.unexport();
            	} catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	registry.unregister(registedProviderUrl);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
                try {
                	overrideListeners.remove(overrideSubscribeUrl);
                	registry.unsubscribe(overrideSubscribeUrl, overrideSubscribeListener);
                } catch (Throwable t) {
                	logger.warn(t.getMessage(), t);
                }
            }
        };
    }



最后总结一下dubbo里面比较重要的组建

Invoker
Invoker起到代理处理器的作用,在消费者端,Invoker负责和远程服务端通信
在服务者端,Invoker服务利用反射调用具体的服务实力处理客户端的请求

ProxyFactory
从JavassistProxyFactory就可以看出来,在客户端ProxyFactory会将invoker代理成一个符合客户端要求的实例,比如,客户端要调用远程服务DemoService,那ProxyFactory就会将invoker作为一个被代理的目标生成DemoService代理对象。
在服务端,ProxyFactory会将具体的服务,例如DemoServiceImpl代理成invoker

public class JavassistProxyFactory extends AbstractProxyFactory {

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.getProxy(interfaces).newInstance(new InvokerInvocationHandler(invoker));
    }

    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper类不能正确处理带$的类名
        final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName, 
                                      Class<?>[] parameterTypes, 
                                      Object[] arguments) throws Throwable {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }

}

Procotol

@SPI("dubbo")
public interface Protocol {
    
    /**
     * 获取缺省端口,当用户没有配置端口时使用。
     * 
     * @return 缺省端口
     */
    int getDefaultPort();

    /**
     * 暴露远程服务:<br>
     * 1. 协议在接收请求时,应记录请求来源方地址信息:RpcContext.getContext().setRemoteAddress();<br>
     * 2. export()必须是幂等的,也就是暴露同一个URL的Invoker两次,和暴露一次没有区别。<br>
     * 3. export()传入的Invoker由框架实现并传入,协议不需要关心。<br>
     * 
     * @param <T> 服务的类型
     * @param invoker 服务的执行体
     * @return exporter 暴露服务的引用,用于取消暴露
     * @throws RpcException 当暴露服务出错时抛出,比如端口已占用
     */
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    /**
     * 引用远程服务:<br>
     * 1. 当用户调用refer()所返回的Invoker对象的invoke()方法时,协议需相应执行同URL远端export()传入的Invoker对象的invoke()方法。<br>
     * 2. refer()返回的Invoker由协议实现,协议通常需要在此Invoker中发送远程请求。<br>
     * 3. 当url中有设置check=false时,连接失败不能抛出异常,并内部自动恢复。<br>
     * 
     * @param <T> 服务的类型
     * @param type 服务的类型
     * @param url 远程服务的URL地址
     * @return invoker 服务的本地代理
     * @throws RpcException 当连接服务提供方失败时抛出
     */
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;

    /**
     * 释放协议:<br>
     * 1. 取消该协议所有已经暴露和引用的服务。<br>
     * 2. 释放协议所占用的所有资源,比如连接和端口。<br>
     * 3. 协议在释放后,依然能暴露和引用新的服务。<br>
     */
    void destroy();

}

procotol通常就是暴露远程服务和引用远程服务,但在RegistryProcotol中的代码,可以看到除了暴露远程服务和引用远程服务外还有将服务注册到注册中心方便订阅,然后监听注册中心,当注册中心里面有什么变化的时候可以刷新本地缓存。
在DubboProctol里面负责客户端底层通信的是ExchangeClient,在服务端负责底层通信的是ExchangeServer

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值