【es笔记三】Java Low Level REST Client

翻译:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-low.html

0 综述

Java REST Client 有两种风格:

  • Java Low Level REST ClientElasticSearch官方的低级客户端。它允许通过http与一个Elasticsearch集群通信。将请求的编组和响应的反编组工作留给用户自己处理。它兼容所有的Elasticsearch的版本。
  • Java High Level REST ClientElasticsearch官方的高级客户端。它基于低级客户端,它暴露了API特定的方法,并负责处理请求编组和响应非编组。

1 Java Low Level REST Client

低级客户端的功能包括:

  • 最小的依赖
  • 跨所有可用节点的负载平衡
  • 节点故障和特定响应代码时的故障转移
  • 连接惩罚失败(是否重试失败的节点取决于它失败的连续次数;失败次数越多,客户端在再次尝试同一节点之前等待的时间越长)
  • 持久连接
  • 跟踪请求和响应的日志记录
  • 可选自动发现集群节点

1.1 开始

本节介绍如何开始使用Java Low Level REST Client

1.1.1 Javadoc文档

Java Low Level REST ClientJavadoc文档可以在这里找到。

1.1.2 Maven仓库

Java Low Level REST Client所需要的最低Java版本是1.7

1.1.2.1 Maven配置

以下是如何使用maven作为依赖关系管理器来配置依赖关系。将以下内容添加到您的pom.xml文件中:

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>6.5.1</version>
</dependency>
1.1.2.2 Gradle配置

以下是使用gradle作为依赖关系管理器配置依赖关系的方法。将以下内容添加到您的build.gradle文件中:

dependencies {
    compile 'org.elasticsearch.client:elasticsearch-rest-client:6.5.1'
}

1.1.3 依赖

Java Low Level REST Client内部使用 Apache Http Async Client 发送http请求。它取决于以下几个部分,

异步客户端以及它自己的依赖:

  • org.apache.httpcomponents:httpasyncclient
  • org.apache.httpcomponents:httpcore-nio
  • org.apache.httpcomponents:httpclient
  • org.apache.httpcomponents:httpcore
  • commons-codec:commons-codec
  • commons-logging:commons-logging

1.1.4 初始化

通过RestClient#builder(HttpHost...)静态方法可以构建一个RestClent实例。唯一需要的参数是客户端与之通信的一个或多个服务器主机。

RestClient restClient = RestClient.builder(
    new HttpHost("localhost", 9200, "http"),
    new HttpHost("localhost", 9201, "http")).build();

RestClient 类是线程安全的。理想情况下具有和使用它的应用程序相同的生命周期。需要在它完全不需要的时候进行关闭,以便它使用的所有资源以及底层的http客户端实例及其线程得到正确释放:

restClient.close();

RestClientBuilder还允许在构建RestClient实例时可选择的设置以下配置参数:

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "http"));
Header[] defaultHeaders = new Header[]{new BasicHeader("header", "value")};
builder.setDefaultHeaders(defaultHeaders); // (1)

(1)设置需要随每个请求一起发送的默认Headers,以防止必须为每个请求指定它们。

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "http"));
builder.setMaxRetryTimeoutMillis(10000);  // (1)

(1)设置在多次尝试同一请求时应该遵守的超时时间。默认值为30秒,与默认套接字超时相同。如果自定义套接字超时,则应相应地调整最大重试超时。

RestClientBuilder builder = RestClient.builder(
        new HttpHost("localhost", 9200, "http"));
builder.setFailureListener(new RestClient.FailureListener() {
    @Override
    public void onFailure(Node node) {
        // (1)
    }
});

(1)设置一个侦听器,每次节点出现故障时都会收到通知,以防需要采取措施。启用嗅探失败时在内部使用。

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "http"));
builder.setNodeSelector(NodeSelector.SKIP_DEDICATED_MASTERS);  // (1)

(1)设置节点选择器以用于过滤客户端将请求发送到客户端本身的节点之间的节点。这有助于防止在启用嗅探时向专用主节点发送请求。默认情况下,客户端向每个配置的节点发送请求。

RestClientBuilder builder = RestClient.builder(
        new HttpHost("localhost", 9200, "http"));
builder.setRequestConfigCallback(
    new RestClientBuilder.RequestConfigCallback() {
        @Override
        public RequestConfig.Builder customizeRequestConfig(
                RequestConfig.Builder requestConfigBuilder) {
            return requestConfigBuilder.setSocketTimeout(10000); // (1) 
        }
    });

(1)设置一个回调,来允许修改默认请求配置(例如,请求超时,身份验证或org.apache.http.client.config.RequestConfig.Builder 允许设置的任何内容 ) 。

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "http"));
builder.setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder.setProxy(
                new HttpHost("proxy", 9000, "http")); // (1)
        }
    });

(1)设置一个回调,来允许修改http客户端配置(例如,通过ssl进行加密通信,或org.apache.http.impl.nio.client.HttpAsyncClientBuilder 允许设置的任何内容 ) 。

1.1.5 如何发送请求

一旦RestClient被创建,可以通过调用 performRequestperformRequestAsync来发送请求。performRequest是同步的,它将会阻塞调用线程,并在请求成功时或者由于失败而抛出异常时返回Response对象。performRequestAsync是异步的,它接受一个ResponseListener参数,并在请求成功时或者由于失败而抛出异常时调用对应的方法。

下面是同步方法performRequest的使用:

// 创建请求对象
Request request = new Request(
    "GET",  // (1)
    "/");   // (2)
// 使用同步方法发送请求
Response response = restClient.performRequest(request);

(1)这个参数需要填写HTTP方法(GETPOSTHEAD、等等)。

(2)服务器上的url路径。

下面是异步方法performRequestAsync的使用:

Request request = new Request(
    "GET",  // (1)
    "/");   // (2)
restClient.performRequestAsync(request, new ResponseListener() {
    @Override
    public void onSuccess(Response response) {
        // (3)
    }

    @Override
    public void onFailure(Exception exception) {
        // (4)
    }
});

(1)这个参数需要填写HTTP方法(GETPOSTHEAD、等等)。

(2)服务器上的url路径。

(3)处理响应的回调。

(4)处理请求失败的回调。

你可以向Request请求对象中添加请求参数:

request.addParameter("pretty", "true");

你还可以将请求的body部分设置为任何HttpEntity对象:

request.setEntity(new NStringEntity(
        "{\"json\":\"text\"}",
        ContentType.APPLICATION_JSON));

注意:

HttpEntity设置ContentType非常重要,因为它将会设置对应的http请求的Content-Type请求头,以便Elasticsearch能够正确的解析请求内容。

你也可以直接传入一个json字符串,它将默认设置为application/json

request.setJsonEntity("{\"json\":\"text\"}");
1.1.5.1 请求选项

RequestOptions类保存了应该在同一个应用程序中的多个请求之间共享的那部分请求数据。您可以创建一个单例实例,并在所有请求之间共享它:

// 单例(静态final对象)
private static final RequestOptions COMMON_OPTIONS;
// 静态初始化
static {
    RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
    builder.addHeader("Authorization", "Bearer " + TOKEN); // (1)
    builder.setHttpAsyncResponseConsumerFactory(           // (2)
        new HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
    // 调用build()方法创建对象
    COMMON_OPTIONS = builder.build();
}

(1)添加所有请求都需要的headers信息。

(2)定制响应的消费者。

addHeader方法是用来设置那些和认证以及代理相关的headers,没有必要在这里设置Content-Type,因为客户端会自动的将HttpEntity中设置的ContentType绑定到headers中去。

你可以通过设置NodeSelector来控制哪些节点将会收到请求。通常NodeSelector.NOT_MASTER_ONLY是一个好的选择。

你还可以自定义响应的消费者来缓冲异步响应。默认消费者将在JVM堆上缓冲最多100MB的响应。如果响应更大,则请求将失败。

一旦你创建了一个 RequestOptions的单例对象,你可以在创建Request对象的时候使用它:

request.setOptions(COMMON_OPTIONS);

你还可以为每个请求定制化自己的RequestOptions对象:

RequestOptions.Builder options = COMMON_OPTIONS.toBuilder();
options.addHeader("cats", "knock things off of other things");
request.setOptions(options);
1.1.5.2 多个并行异步操作

客户端很乐意并行执行许多操作。以下示例展示了并行索引许多文档。在现实世界中,您可能希望使用_bulkAPI,但是这个例子很有启发性。

final CountDownLatch latch = new CountDownLatch(documents.length);
for (int i = 0; i < documents.length; i++) {
    // 为每一个document创建一个request对象
    Request request = new Request("PUT", "/posts/doc/" + i);
    // 假设documents被保存在一个HttpEntity数组中
    request.setEntity(documents[i]);
    restClient.performRequestAsync(
            request,
            new ResponseListener() {
                @Override
                public void onSuccess(Response response) {
                    // (1)
                    latch.countDown();
                }

                @Override
                public void onFailure(Exception exception) {
                    // (2)
                    latch.countDown();
                }
            }
    );
}
latch.await();

(1)处理返回的响应。

(2)处理返回的异常,这是由于通信错误或者反回了一个response,它的status code标识了错误。

1.1.6 如何处理响应

Response对象,要么是通过调用同步方法performRequest返回,要么是作为ResponseListener#onSuccess(Response)的参数。

Response response = restClient.performRequest(new Request("GET", "/"));
RequestLine requestLine = response.getRequestLine(); // (1)
HttpHost host = response.getHost(); // (2)
int statusCode = response.getStatusLine().getStatusCode(); // (3)
Header[] headers = response.getHeaders(); // (4)
String responseBody = EntityUtils.toString(response.getEntity()); // (5)

(1)获取请求行的信息。

(2)获取返回响应的主机信息对象。

(3)响应状态行,你可以获取响应状态码。

(4)获取响应的headers,也可以通过getHeader(String)获取具体的某个header

(5) 获取org.apache.http.HttpEntity 对象的字符串形式。

在执行请求时,会在以下场景中抛出异常:

  • IOException

    通信问题(比如,SocketTimeoutException

  • ResponseException

    返回了一个响应,但是它的状态码表明发生了错误(不是2xx

注意:

返回404HEAD请求不会抛出一个ResponseException,因为它是一个预期的正确的HEAD响应,只是没有找到资源而已。所有其他的HTTP方法(例如GET)抛出ResponseException404回应,除非该ignore参数包含404ignore是一个特殊的客户端参数,不会发送到Elasticsearch并包含逗号分隔的错误状态代码列表。它允许控制是否应将某些错误状态代码视为预期响应而不是异常。这对于例如get api是有用的,因为它可以返回404 当文档丢失时,在这种情况下,响应正文将不包含错误,而是通常的get api响应,只是没有找到未找到的文档。

请注意Java Low Level REST Client不会提供json序列化和反序列化的任何帮助,用户可以选择任何喜欢的第三方库。

底层的Apache Http Client提供了不同的 org.apache.http.HttpEntity 实现,允许以不同的格式(流,字节数组,字符串等)提供请求主体。至于读取响应体, HttpEntity#getContent方法很方便,它返回InputStream 来自先前缓冲的响应体的读数。作为替代方案,可以提供org.apache.http.nio.protocol.HttpAsyncResponseConsumer 控制字节读取和缓冲方式的自定义 。

1.1.7 记录日志

Java Low Level REST ClientApache Async Http Client使用的是相同的日志库: Apache Commons Logging

1.2 常见配置

初始化中所述,RestClientBuilder 同时支持提供 RequestConfigCallbackHttpClientConfigCallback,允许任何Apache Async Http Client暴露的自定义配置。这些回调使得可以修改客户端的某些特定行为,而不会覆盖RestClient 初始化的所有其他默认配置。本节介绍一些需要为Java Low Level REST Client进行其他配置的常见方案。

1.2.1 超时时间

在通过构建器构建RestClient时,可以通过提供RequestConfigCallback实例来配置请求超时。该接口有一个方法,它接收org.apache.http.client.config.RequestConfig.Builder 实例作为参数,并具有相同的返回类型。可以修改请求配置生成器,然后返回它。在下面的示例中,我们将增大连接超时时间(默认为1秒)和套接字超时时间(默认为30秒)。我们还相应地调整最大重试超时时间(默认也是30秒)。

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setRequestConfigCallback(
        new RestClientBuilder.RequestConfigCallback() {
            @Override
            public RequestConfig.Builder customizeRequestConfig(
                    RequestConfig.Builder requestConfigBuilder) {
                return requestConfigBuilder
                    .setConnectTimeout(5000)
                    .setSocketTimeout(60000);
            }
        })
    .setMaxRetryTimeoutMillis(60000);

1.2.2 线程数量

Apache Http Async Client默认启动一个调度线程(dispatcher thread),一些连接管理器需要使用的工作线程(worker threads ),它们和本地检测到的处理器数量(取决于Runtime.getRuntime().availableProcessors()的返回值)一样多。线程数可以通过以下方式修改:

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder.setDefaultIOReactorConfig(
                IOReactorConfig.custom()
                    .setIoThreadCount(1)
                    .build());
        }
    });

1.2.3 基本认证

在通过构建器构建RestClient时,可以通过提供HttpClientConfigCallback来配置基本身份验证。该接口有一个方法,它接收org.apache.http.impl.nio.client.HttpAsyncClientBuilder 作为参数,并具有相同的返回类型。可以修改http client builder,然后返回它。在下面的示例中,我们设置了一个需要基本身份验证的默认凭据提供程序:

final CredentialsProvider credentialsProvider =
    new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
    new UsernamePasswordCredentials("user", "password"));

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder
                .setDefaultCredentialsProvider(credentialsProvider);
        }
    });

可以禁用身份验证,这意味着每个请求都将在没有携带包含认证的headers的情况下发送,以查看是否接受该请求,并且在接收到HTTP 401响应后,将重新发送与基本身份验证标头完全相同的请求。如果你想这样做,你可以通过HttpAsyncClientBuilder禁用它:

final CredentialsProvider credentialsProvider =
    new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
    new UsernamePasswordCredentials("user", "password"));

RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            httpClientBuilder.disableAuthCaching(); // (1)
            return httpClientBuilder
                .setDefaultCredentialsProvider(credentialsProvider);
        }
    });

(1)禁用身份验证

1.2.4 加密通信

对通信进行加密也可以通过HttpClientConfigCallback配置。org.apache.http.impl.nio.client.HttpAsyncClientBuilder 作为参数,它公开了配置加密通信的多个方法:setSSLContext, setSSLSessionStrategy 以及 setConnectionManager ,按照从最不重要的优先级排序。下面是一个例子:

KeyStore truststore = KeyStore.getInstance("jks");
try (InputStream is = Files.newInputStream(keyStorePath)) {
    truststore.load(is, keyStorePass.toCharArray());
}
SSLContextBuilder sslBuilder = SSLContexts.custom()
    .loadTrustMaterial(truststore, null);
final SSLContext sslContext = sslBuilder.build();
RestClientBuilder builder = RestClient.builder(
    new HttpHost("localhost", 9200, "https"))
    .setHttpClientConfigCallback(new HttpClientConfigCallback() {
        @Override
        public HttpAsyncClientBuilder customizeHttpClient(
                HttpAsyncClientBuilder httpClientBuilder) {
            return httpClientBuilder.setSSLContext(sslContext);
        }
    });

如果没有提供显示配置,将使用系统默认配置

1.2.5 其他

对于其他需要的配置,应该参考Apache HttpAsyncClient文档:https://hc.apache.org/httpcomponents-asyncclient-4.1.x/

1.2.6 节点选择器

客户端以循环方式将每个请求发送到配置的节点之一。在初始化客户机时,可以通过节点选择器筛选节点。这在启用sniffing时非常有用,以防只有专用的主节点应该被HTTP请求命中。对于每个请求,客户机将运行最终配置的节点选择器来筛选候选节点,然后从剩余的节点选择列表中选择下一个。

RestClientBuilder builder = RestClient.builder(
        new HttpHost("localhost", 9200, "http"));
builder.setNodeSelector(new NodeSelector() { // (1)
    @Override
    public void select(Iterable<Node> nodes) {
        /*
         * Prefer any node that belongs to rack_one. If none is around
         * we will go to another rack till it's time to try and revive
         * some of the nodes that belong to rack_one.
         */
        boolean foundOne = false;
        for (Node node : nodes) {
            String rackId = node.getAttributes().get("rack_id").get(0);
            if ("rack_one".equals(rackId)) {
                foundOne = true;
                break;
            }
        }
        if (foundOne) {
            Iterator<Node> nodesIt = nodes.iterator();
            while (nodesIt.hasNext()) {
                Node node = nodesIt.next();
                String rackId = node.getAttributes().get("rack_id").get(0);
                if ("rack_one".equals(rackId) == false) {
                    nodesIt.remove();
                }
            }
        }
    }
});

1.3 嗅探器

最小库允许自动从运行中的Elasticsearch集群中发现节点,并将节点设置到存在的RestClient实例中。默认情况下,它使用Nodes Info api检索属于集群的节点,并使用jackson解析获得的json响应。

Elasticsearch 2.x开始兼容。

1.3.1 Javadoc文档

REST client snifferJavadoc文档可以在这里找到:https://artifacts.elastic.co/javadoc/org/elasticsearch/client/elasticsearch-rest-client-sniffer/6.5.2/index.html

1.3.2 Maven仓库

1.3.2.1 Maven配置

下面是如何使用maven作为依赖项管理器来配置依赖项。在pom.xml中添加以下内容:

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client-sniffer</artifactId>
    <version>6.5.2</version>
</dependency>
1.3.2.2 Gradle配置

下面是如何使用gradle作为依赖项管理器来配置依赖项。将以下内容添加到build.gradle文件:

dependencies {
    compile 'org.elasticsearch.client:elasticsearch-rest-client-sniffer:6.5.2'
}

1.3.3 使用

一旦一个RestClient实例被创建,一个Sniffer可以与其关联。Sniffer将利用所提供的RestClient 定期(默认情况下每5分钟)从集群获取当前节点列表,并通过调用RestClient#setNodes更新它们。

RestClient restClient = RestClient.builder(
    new HttpHost("localhost", 9200, "http"))
    .build();
Sniffer sniffer = Sniffer.builder(restClient).build();

不使用的时候,显式关闭Sniffer是非常重要的,这将使其后台线程正确关闭并释放其所有资源。Sniffer 对象应具有与RestClient客户端相同的生命周期,并在RestClient客户端之前关闭:

sniffer.close();
restClient.close();

默认情况下,Sniffer每5分钟更新一次节点。可以通过提供(以毫秒为单位)来定制此间隔,如下所示:

RestClient restClient = RestClient.builder(
    new HttpHost("localhost", 9200, "http"))
    .build();
Sniffer sniffer = Sniffer.builder(restClient)
    .setSniffIntervalMillis(60000).build();

也可以在失败时启用嗅探,这意味着在每次失败之后,节点列表会立即更新,而不是在接下来的普通嗅探轮次中。在这种情况下,SniffOnFailureListener需要首先被创建并在RestClient创建时提供。此外,一旦Sniffer稍后被创建,它需要与同一个SniffOnFailureListener实例相关联,该 实例将在每次失败时得到通知,并使用它关联的Sniffer来执行所述的额外嗅探轮询。

SniffOnFailureListener sniffOnFailureListener =
    new SniffOnFailureListener();
RestClient restClient = RestClient.builder(
    new HttpHost("localhost", 9200))
    .setFailureListener(sniffOnFailureListener) // (1)
    .build();
Sniffer sniffer = Sniffer.builder(restClient)
    .setSniffAfterFailureDelayMillis(30000) // (2)
    .build();
sniffOnFailureListener.setSniffer(sniffer); // (3)

(1)RestClient 实例设置失败监听器

(2)当失败时启动嗅探,不仅节点在每次失败后都会更新,而且在失败后的一分钟内,还会比平时更快地安排额外的嗅探轮次,假设一切都会恢复正常我们希望尽快检测到。该间隔可以通过setSniffAfterFailureDelayMillis 方法在嗅探器创建时定制。注意,如果没有像前面解释的那样启用失败时的嗅探,则最后一个配置参数没有任何效果。

(3)Sniffer 实例关联到失败监听器。

Elasticsearch Nodes Info api不会返回连接到节点时使用的协议,而只返回它们的host:port密钥对,因此默认使用http 。如果想要使用https,则 ElasticsearchNodesSniffer实例必须手动被创建:

RestClient restClient = RestClient.builder(
        new HttpHost("localhost", 9200, "http"))
        .build();
NodesSniffer nodesSniffer = new ElasticsearchNodesSniffer(
        restClient,
        ElasticsearchNodesSniffer.DEFAULT_SNIFF_REQUEST_TIMEOUT,
        ElasticsearchNodesSniffer.Scheme.HTTPS);
Sniffer sniffer = Sniffer.builder(restClient)
        .setNodesSniffer(nodesSniffer).build();

以同样的方式,也可以自定义sniffRequestTimeout,默认为一秒。这是timeout在调用Nodes Info api时作为查询字符串参数提供的参数,因此当超时在服务器端到期时,仍然会返回有效响应,尽管它可能只包含属于集群的节点的子集,那些在那之前做出回应的人。

RestClient restClient = RestClient.builder(
    new HttpHost("localhost", 9200, "http"))
    .build();
NodesSniffer nodesSniffer = new ElasticsearchNodesSniffer(
    restClient,
    TimeUnit.SECONDS.toMillis(5),
    ElasticsearchNodesSniffer.Scheme.HTTP);
Sniffer sniffer = Sniffer.builder(restClient)
    .setNodesSniffer(nodesSniffer).build();

此外,NodesSniffer可以为高级用例提供自定义实现,可能需要从外部源而不是从Elasticsearch获取Nodes:

RestClient restClient = RestClient.builder(
    new HttpHost("localhost", 9200, "http"))
    .build();
NodesSniffer nodesSniffer = new NodesSniffer() {
        @Override
        public List<Node> sniff() throws IOException {
            return null; 
        }
    };
Sniffer sniffer = Sniffer.builder(restClient)
    .setNodesSniffer(nodesSniffer).build();
  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值