Nacos服务注册与发现源码(一)之gRPC协议的实例注册

Nacos核心功能点

  • 服务注册:Nacos Client会通过发送请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
  • 服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
  • 服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
  • 服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存
  • 服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。

Nacos服务端核心模块

在这里插入图片描述

Nacos客户端原理

在这里插入图片描述
从以上的两张图中我们就能够找到突破口了,其实核心内容就集中在nacos-console、nacos-naming、nacos-config这几个模块。

客户端源码

NacosFactory
创建配置服务、创建名称服务、创建维护服务。

NamingFactory

/**
 * Create a new naming service.
 *
 * @param serverList server list
 * @return new naming service
 * @throws NacosException nacos exception
 */
public static NamingService createNamingService(String serverList) throws NacosException {
    try {
        Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.naming.NacosNamingService");
        Constructor constructor = driverImplClass.getConstructor(String.class);
        return (NamingService) constructor.newInstance(serverList);
    } catch (Throwable e) {
        throw new NacosException(NacosException.CLIENT_INVALID_PARAM, e);
    }
}

NamingService
服务注册与发现接口:实例注册、批量实例注册、实例注销、获取所有实例、选择实例、选择一个健康实例、订阅服务(接收实例变更事件)、取消订阅服务、获取来自服务器的全部服务、获取服务器的健康状态、关闭资源服务
NacosNamingService
NamingService接口的实现类:实现上述接口的所有方法

private void init(Properties properties) throws NacosException {
    ValidatorUtils.checkInitParam(properties);
    this.namespace = InitUtils.initNamespaceForNaming(properties);
    InitUtils.initSerialization();
    InitUtils.initWebRootContext(properties);
    initLogName(properties);

    this.notifierEventScope = UUID.randomUUID().tostring();//通知器的事件范围
    this.changeNotifier = new InstancesChangeNotifier(this.notifierEventScope);//创建实例变更通知器
    NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);//注册实例变更事件的发布器
    NotifyCenter.registerSubscriber(changeNotifier);//注册实例变更事件的订阅器
    this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, properties);//服务信息容器
    this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, 
        changeNotifier);
}

@Override
public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
    NamingUtils.checkInstanceIsLegal(instance);    //检查实例关于保活的参数
    clientProxy.registerService(serviceName, groupName, instance);    //通过客户端代理委托类注册实例
}

NamingUtils

/**
 * <p>Check instance param about keep alive.</p>
 *
 * <pre>
 * heart beat timeout must > heart beat interval
 * ip delete timeout must  > heart beat interval
 * </pre>
 *
 * @param instance need checked instance
 * @throws NacosException if check failed, throw exception
 */
public static void checkInstanceIsLegal(Instance instance) throws NacosException {
    if (instance.getInstanceHeartBeatTimeOut() < instance.getInstanceHeartBeatInterval()
            || instance.getIpDeleteTimeout() < instance.getInstanceHeartBeatInterval()) {
        throw new NacosException(NacosException.INVALID_PARAM,
                "Instance 'heart beat interval' must less than 'heart beat timeout' and 'ip delete timeout'.");
    }
    if (!StringUtils.isEmpty(instance.getClusterName()) && 
        !CLUSTER_NAME_PATTERN.matcher(instance.getClusterName()).matches()) {
        throw new NacosException(NacosException.INVALID_PARAM,
                String.format("Instance 'clusterName' should be characters with only 0-9a-zA-Z-. (current: %s)",
                        instance.getClusterName()));
    }
}

NamingClientProxyDelegate
客户端代理委托类

@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    getExecuteClientProxy(instance).registerService(serviceName, groupName, instance);
}

//如果是瞬时实例则使用grpc客户端代理,否则使用Http客户端代理,进行实例注册
private NamingClientProxy getExecuteClientProxy(Instance instance) {
    return instance.isEphemeral() ? grpcClientProxy : httpClientProxy;
}

NamingGrpcClientProxy

@Override
public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
    NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance {}", namespaceId, serviceName,
            instance);
    //缓存当前注册的实例信息用于恢复
    redoService.cacheInstanceForRedo(serviceName, groupName, instance);
    //封装参数,基于grpc进行调用和结果处理
    doRegisterService(serviceName, groupName, instance);
}

/**
 * Execute register operation.
 *
 * @param serviceName name of service
 * @param groupName   group of service
 * @param instance    instance to register
 * @throws NacosException nacos exception
 */
public void doRegisterService(String serviceName, String groupName, Instance instance) throws NacosException {
    InstanceRequest request = new InstanceRequest(namespaceId, serviceName, groupName,
            NamingRemoteConstants.REGISTER_INSTANCE, instance);
    requestToServer(request, Response.class);
     redoService.instanceRegistered(serviceName, groupName);
}

private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T> responseClass)
        throws NacosException {
    try {
        request.putAllHeader(
                getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
        Response response =
                requestTimeout < 0 ? rpcClient.request(request) : rpcClient.request(request, requestTimeout);
        if (ResponseCode.SUCCESS.getCode() != response.getResultCode()) {
            throw new NacosException(response.getErrorCode(), response.getMessage());
        }
        if (responseClass.isAssignableFrom(response.getClass())) {
            return (T) response;
        }
        NAMING_LOGGER.error("Server return unexpected response '{}', expected response should be '{}'",
                response.getClass().getName(), responseClass.getName());
    } catch (NacosException e) {
        throw e;
    } catch (Exception e) {
        throw new NacosException(NacosException.SERVER_ERROR, "Request nacos server failed: ", e);
    }
    throw new NacosException(NacosException.SERVER_ERROR, "Server return invalid response");
}

NamingGrpcRedoService

/**
 * Cache registered instance for redo.
 *
 * @param serviceName service name
 * @param groupName   group name
 * @param instance    registered instance
 */
public void cacheInstanceForRedo(String serviceName, String groupName, Instance instance) {
    String key = NamingUtils.getGroupedName(serviceName, groupName);
    InstanceRedoData redoData = InstanceRedoData.build(serviceName, groupName, instance);
    synchronized (registeredInstances) {
        registeredInstances.put(key, redoData);
    }
}

/**
 * Instance register successfully, mark registered status as {@code true}.
 *
 * @param serviceName service name
 * @param groupName   group name
 */
public void instanceRegistered(String serviceName, String groupName) {
    String key = NamingUtils.getGroupedName(serviceName, groupName);
    synchronized (registeredInstances) {
        InstanceRedoData redoData = registeredInstances.get(key);
        if (null != redoData) {
            redoData.setRegistered(true);
        }
    }
}

Instance
定义实例的各种属性:IP、Port、权重、健康标识、接收请求标识、瞬时标识、集群名称、服务名称、元数据信息(Map类型)
定义各种方法:获取实例的心跳间隔(默认5s)、获取实例的心跳超时(默认15s)、获取实例的删除超时(默认30s)、获取实例Id的生成器(simple、snowflake)

spring-cloud-starter-alibaba-nacos-discovery
spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.alibaba.cloud.nacos.discovery.NacosDiscoveryAutoConfiguration,\
  com.alibaba.cloud.nacos.ribbon.RibbonNacosAutoConfiguration,\
  com.alibaba.cloud.nacos.endpoint.NacosDiscoveryEndpointAutoConfiguration,\
  com.alibaba.cloud.nacos.registry.NacosServiceRegistryAutoConfiguration,\
  com.alibaba.cloud.nacos.discovery.NacosDiscoveryClientConfiguration,\
  com.alibaba.cloud.nacos.discovery.reactive.NacosReactiveDiscoveryClientConfiguration,\
  com.alibaba.cloud.nacos.discovery.configclient.NacosConfigServerAutoConfiguration,\
  com.alibaba.cloud.nacos.NacosServiceAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
  com.alibaba.cloud.nacos.discovery.configclient.NacosDiscoveryClientConfigServiceBootstrapConfiguration

NacosServiceRegistryAutoConfiguration

@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
public NacosAutoServiceRegistration nacosAutoServiceRegistration(
      NacosServiceRegistry registry,
      AutoServiceRegistrationProperties autoServiceRegistrationProperties,
      NacosRegistration registration) {
   return new NacosAutoServiceRegistration(registry,
         autoServiceRegistrationProperties, registration);
}

AbstractAutoServiceRegistration

@Override
@SuppressWarnings("deprecation")
public void onApplicationEvent(WebServerInitializedEvent event) {
   bind(event);
}

public void bind(WebServerInitializedEvent event) {
   ApplicationContext context = event.getApplicationContext();
   if (context instanceof ConfigurableWebServerApplicationContext) {
      if ("management".equals(((ConfigurableWebServerApplicationContext) context)
            .getServerNamespace())) {
         return;
      }
   }
   this.port.compareAndSet(0, event.getWebServer().getPort());
   this.start();
}

public void start() {
   if (!isEnabled()) {
      if (logger.isDebugEnabled()) {
         logger.debug("Discovery Lifecycle disabled. Not starting");
      }
      return;
   }

   // only initialize if nonSecurePort is greater than 0 and it isn't already running
   // because of containerPortInitializer below
   if (!this.running.get()) {
      this.context.publishEvent(
            new InstancePreRegisteredEvent(this, getRegistration()));
      register();
      if (shouldRegisterManagement()) {
         registerManagement();
      }
      this.context.publishEvent(
            new InstanceRegisteredEvent<>(this, getConfiguration()));
      this.running.compareAndSet(false, true);
   }

}

/**
 * Register the local service with the {@link ServiceRegistry}.
 */
protected void register() {
   this.serviceRegistry.register(getRegistration());
}

NacosServiceRegistry implements ServiceRegistry

@Override
public void register(Registration registration) {

   if (StringUtils.isEmpty(registration.getServiceId())) {
      log.warn("No service to register for nacos client...");
      return;
   }

   NamingService namingService = namingService();
   String serviceId = registration.getServiceId();
   String group = nacosDiscoveryProperties.getGroup();

   Instance instance = getNacosInstanceFromRegistration(registration);

   try {
      namingService.registerInstance(serviceId, group, instance);
      log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
            instance.getIp(), instance.getPort());
   }
   catch (Exception e) {
      log.error("nacos registry, {} register failed...{},", serviceId,
            registration.toString(), e);
      // rethrow a RuntimeException if the registration is failed.
      // issue : https://github.com/alibaba/spring-cloud-alibaba/issues/1132
      rethrowRuntimeException(e);
   }
}

private NamingService namingService() {
   return nacosServiceManager
         .getNamingService(nacosDiscoveryProperties.getNacosProperties());
}
NacosServiceManager
public NamingService getNamingService(Properties properties) {
   if (Objects.isNull(this.namingService)) {
      buildNamingService(properties);
   }
   return namingService;
}

private NamingService buildNamingService(Properties properties) {
   if (Objects.isNull(namingService)) {
      synchronized (NacosServiceManager.class) {
         if (Objects.isNull(namingService)) {
            namingService = createNewNamingService(properties);
         }
      }
   }
   return namingService;
}

private NamingService createNewNamingService(Properties properties) {
   try {
      return createNamingService(properties);
   }
   catch (NacosException e) {
      throw new RuntimeException(e);
   }
}

/**
 * Create naming service.
 *
 * @param properties init param
 * @return Naming
 * @throws NacosException Exception
 */
public static NamingService createNamingService(Properties properties) throws NacosException {
    return NamingFactory.createNamingService(properties);
}

服务端源码

RequestGrpc
根据nacos_grpc_service.proto编译得到的类,业务处理类必须继承子类RequestImplBase,并且覆盖已定义但未实现的方法,比如request()

/**
 */
public static abstract class RequestImplBase implements io.grpc.BindableService {

  /**
   * <pre>
   * Sends a commonRequest
   * </pre>
   */
  public void request(com.alibaba.nacos.api.grpc.auto.Payload request,
      io.grpc.stub.StreamObserver<com.alibaba.nacos.api.grpc.auto.Payload> responseObserver) {
    asyncUnimplementedUnaryCall(getRequestMethod(), responseObserver);
  }

  @Override public final io.grpc.ServerServiceDefinition bindService() {
    return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
        .addMethod(
          getRequestMethod(),
          asyncUnaryCall(    //ServerCalls.asyncUnaryCall,涉及到grpc的调用机制
            new MethodHandlers<
              com.alibaba.nacos.api.grpc.auto.Payload,
              com.alibaba.nacos.api.grpc.auto.Payload>(
                this, METHODID_REQUEST)))    //通过方法处理器找到被调用的服务实现类的方法,this就是RequestImplBase的子类
        .build();
  }
}

GrpcRequestAcceptor extends RequestGrpc.RequestImplBase
业务处理类是核心,覆盖已定义但未实现的方法,比如request

@Override
public void request(Payload grpcRequest, StreamObserver<Payload> responseObserver) {
    
    traceIfNecessary(grpcRequest, true);
    String type = grpcRequest.getMetadata().getType();
    
    //server is on starting.
    if (!ApplicationUtils.isStarted()) {
        Payload payloadResponse = GrpcUtils.convert(
                ErrorResponse.build(NacosException.INVALID_SERVER_STATUS, 
                "Server is starting,please try later."));
        traceIfNecessary(payloadResponse, false);
        responseObserver.onNext(payloadResponse);
        
        responseObserver.onCompleted();
        return;
    }
    
    // server check.
    if (ServerCheckRequest.class.getSimpleName().equals(type)) {
        Payload serverCheckResponseP = GrpcUtils.convert(new ServerCheckResponse(CONTEXT_KEY_CONN_ID.get()));
        traceIfNecessary(serverCheckResponseP, false);
        responseObserver.onNext(serverCheckResponseP);
        responseObserver.onCompleted();
        return;
    }
    
    RequestHandler requestHandler = requestHandlerRegistry.getByRequestType(type);
    //no handler found.
    if (requestHandler == null) {
        Loggers.REMOTE_DIGEST.warn(String.format("[%s] No handler for request type : %s :", "grpc", type));
        Payload payloadResponse = GrpcUtils
                .convert(ErrorResponse.build(NacosException.NO_HANDLER, "RequestHandler Not Found"));
        traceIfNecessary(payloadResponse, false);
        responseObserver.onNext(payloadResponse);
        responseObserver.onCompleted();
        return;
    }
    
    //check connection status.
    String connectionId = CONTEXT_KEY_CONN_ID.get();
    boolean requestValid = connectionManager.checkValid(connectionId);
    if (!requestValid) {
        Loggers.REMOTE_DIGEST
                .warn("[{}] Invalid connection Id ,connection [{}] is un registered ,", "grpc", connectionId);
        Payload payloadResponse = GrpcUtils
                .convert(ErrorResponse.build(NacosException.UN_REGISTER, "Connection is unregistered."));
        traceIfNecessary(payloadResponse, false);
        responseObserver.onNext(payloadResponse);
        responseObserver.onCompleted();
        return;
    }
    
    Object parseObj = null;
    try {
        parseObj = GrpcUtils.parse(grpcRequest);
    } catch (Exception e) {
        Loggers.REMOTE_DIGEST
                .warn("[{}] Invalid request receive from connection [{}] ,error={}", "grpc", connectionId, e);
        Payload payloadResponse = 
            GrpcUtils.convert(ErrorResponse.build(NacosException.BAD_GATEWAY, e.getMessage()));
        traceIfNecessary(payloadResponse, false);
        responseObserver.onNext(payloadResponse);
        responseObserver.onCompleted();
        return;
    }
    
    if (parseObj == null) {
        Loggers.REMOTE_DIGEST.warn("[{}] Invalid request receive  ,parse request is null", connectionId);
        Payload payloadResponse = GrpcUtils
                .convert(ErrorResponse.build(NacosException.BAD_GATEWAY, "Invalid request"));
        traceIfNecessary(payloadResponse, false);
        responseObserver.onNext(payloadResponse);
        responseObserver.onCompleted();
        return;
    }
    
    if (!(parseObj instanceof Request)) {
        Loggers.REMOTE_DIGEST
                .warn("[{}] Invalid request receive  ,parsed payload is not a request,parseObj={}", connectionId,
                        parseObj);
        Payload payloadResponse = GrpcUtils
                .convert(ErrorResponse.build(NacosException.BAD_GATEWAY, "Invalid request"));
        traceIfNecessary(payloadResponse, false);
        responseObserver.onNext(payloadResponse);
        responseObserver.onCompleted();
        return;
    }
    
    Request request = (Request) parseObj;
    try {
        Connection connection = connectionManager.getConnection(CONTEXT_KEY_CONN_ID.get());
        RequestMeta requestMeta = new RequestMeta();
        requestMeta.setClientIp(connection.getMetaInfo().getClientIp());
        requestMeta.setConnectionId(CONTEXT_KEY_CONN_ID.get());
        requestMeta.setClientVersion(connection.getMetaInfo().getVersion());
        requestMeta.setLabels(connection.getMetaInfo().getLabels());
        connectionManager.refreshActiveTime(requestMeta.getConnectionId());
        Response response = requestHandler.handleRequest(request, requestMeta);    //调用具体的请求处理器
        Payload payloadResponse = GrpcUtils.convert(response);
        traceIfNecessary(payloadResponse, false);
        responseObserver.onNext(payloadResponse);
        responseObserver.onCompleted();
    } catch (Throwable e) {
        Loggers.REMOTE_DIGEST
        .error("[{}] Fail to handle request from connection [{}] ,error message :{}", "grpc", connectionId, e);
        Payload payloadResponse = GrpcUtils.convert(ErrorResponse.build(e));
        traceIfNecessary(payloadResponse, false);
        responseObserver.onNext(payloadResponse);
        responseObserver.onCompleted();
    }
    
}

RequestHandler
抽象类、核心方法handleRequest

public Response handleRequest(T request, RequestMeta meta) throws NacosException {
    for (AbstractRequestFilter filter : requestFilters.filters) {
        try {
            Response filterResult = filter.filter(request, meta, this.getClass());
            if (filterResult != null && !filterResult.isSuccess()) {
                return filterResult;
            }
        } catch (Throwable throwable) {
            Loggers.REMOTE.error("filter error", throwable);
        }
        
    }
    return handle(request, meta);    //调用子类的方法,在抽象类中是抽象方法
}

InstanceRequestHandler extends RequestHandler

@Override
@Secured(action = ActionTypes.WRITE)
public InstanceResponse handle(InstanceRequest request, RequestMeta meta) throws NacosException {
    Service service = Service
            .newService(request.getNamespace(), request.getGroupName(), request.getServiceName(), true);
    switch (request.getType()) {
        case NamingRemoteConstants.REGISTER_INSTANCE:
            return registerInstance(service, request, meta);
        case NamingRemoteConstants.DE_REGISTER_INSTANCE:
            return deregisterInstance(service, request, meta);
        default:
            throw new NacosException(NacosException.INVALID_PARAM,
                    String.format("Unsupported request type %s", request.getType()));
    }
}

private InstanceResponse registerInstance(Service service, InstanceRequest request, RequestMeta meta)
        throws NacosException {
    //调用客户端操作服务的方法
    clientOperationService.registerInstance(service, request.getInstance(), meta.getConnectionId());
    //发布注册实例追踪事件
    NotifyCenter.publishEvent(new RegisterInstanceTraceEvent(System.currentTimeMillis(),
            meta.getClientIp(), true, service.getNamespace(), service.getGroup(), service.getName(),
            request.getInstance().getIp(), request.getInstance().getPort()));
    return new InstanceResponse(NamingRemoteConstants.REGISTER_INSTANCE);    //构造响应结果
}

EphemeralClientOperationServiceImpl implements ClientOperationService

@Override
public void registerInstance(Service service, Instance instance, String clientId) throws NacosException {
    //检查实例是否合法:心跳超时时间、IP删除时间等是否小于心跳间隔时间、集群名称是否符合命名规范
    NamingUtils.checkInstanceIsLegal(instance);
    //缓存服务、缓存命名空间与服务的关系
    Service singleton = ServiceManager.getInstance().getSingleton(service);
    if (!singleton.isEphemeral()) {
        throw new NacosRuntimeException(NacosException.INVALID_PARAM,
                String.format("Current service %s is persistent service, can't register ephemeral instance.",
                        singleton.getGroupedServiceName()));
    }
    Client client = clientManager.getClient(clientId);    //连接Id作为客户端Id,获取客户端
    if (!clientIsLegal(client, clientId)) {    //检查客户端是否合法:客户端是否存在、客户端是否瞬时
        return;
    }
    InstancePublishInfo instanceInfo = getPublishInfo(instance);    //构建实例的发布信息
    client.addServiceInstance(singleton, instanceInfo);    //在客户端添加服务与实例的关系信息
    client.setLastUpdatedTime();
    client.recalculateRevision();
    //发布客户端注册服务事件
    NotifyCenter.publishEvent(new ClientOperationEvent.ClientRegisterServiceEvent(singleton, clientId));
    //发布实例元数据事件
    NotifyCenter
            .publishEvent(new MetadataEvent.InstanceMetadataEvent(singleton, instanceInfo.getMetadataId(), 
            false));
}

IpPortBasedClient extends AbstractClient

protected final ConcurrentHashMap<Service, InstancePublishInfo> publishers = 
    new ConcurrentHashMap<>(16, 0.75f, 1);	//存储当前客户端发布的服务的实例信息

protected final ConcurrentHashMap<Service, Subscriber> subscribers = new ConcurrentHashMap<>(16, 0.75f, 1);	//存储当前客户端订阅服务的订阅器

@Override
public boolean addServiceInstance(Service service, InstancePublishInfo instancePublishInfo) {
    //缓存服务与实例映射关系到客户端的容器、一个客户端对应一个连接
    if (null == publishers.put(service, instancePublishInfo)) {
        if (instancePublishInfo instanceof BatchInstancePublishInfo) {
            MetricsMonitor.incrementIpCountWithBatchRegister(instancePublishInfo);
        } else {
            MetricsMonitor.incrementInstanceCount();
        }
    }
    //发布客户端变更事件
    NotifyCenter.publishEvent(new ClientEvent.ClientChangedEvent(this));
    Loggers.SRV_LOG.info("Client change for service {}, {}", service, getClientId());
    return true;
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值