由一次NoHttpResponseException异常,追究到Http长连接和短连接

 一片春愁待酒浇,江上舟摇,楼上帘招。秋娘渡与泰娘桥,风又漂漂,雨又潇潇。何日归家洗客袍?银字笙调,心字香烧。流光容易把人抛,红了樱桃,绿了芭蕉。

——蒋捷《一剪梅.舟过吴江》

一、异常描述

调用smk短信出现NoHttpResponseException异常:

用的是公司配置好的RestTemplate对象。 

原因分析:公司配置好的RestTemplate自带链接池,下次请求时时,连接池里有的连接已经断开了;所以会出现上面的错误。

用李哥的原话:这个问题应该是对方超时把连接关了,池子里的还认为是有效的。(要确认smk那边每个请求的keepalived设置多久的,他们响应的请求头上有带,然后在设置我们这边的会比较准确)

二、解决方法

解决方法有两种,一种是将RestTemplate设置成长连接(RestTemplate配置了连接池管理器,同时也要配置keepAlive);另一种是将RestTemplate设置成短连接。

2.1 RestTemplate设置成长连接

长连接以保证高性能,RestTemplate 本身也是一个 wrapper 其底层默认是 SimpleClientHttpRequestFactory ,如果要保证长连接HttpComponentsClientHttpRequestFactory 是个更好的选择,它不仅可以控制能够建立的连接数还能细粒度的控制到某个 server 的连接数,非常方便。在默认情况下,RestTemplate 到某个 server 的最大连接数只有 2, 一般需要调的更高些,最好等于 server 的 CPU 个数,所以通常下建议使用连接池的方式处理RestTeamplate;

 

 

先来看下原来的配置:

package com.hfi.health.starters.web;

import com.hfi.health.starters.common.util.HfiLogger;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.net.ssl.SSLContext;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
@EnableConfigurationProperties({HttpClientProperties.class})
public class RestTemplateAutoConfiguration
{
  private static final Logger logger = HfiLogger.create(RestTemplateAutoConfiguration.class);
  @Autowired
  private HttpClientProperties httpClientProperties;
  
  @Bean
  public RestTemplate restTemplate()
  {
    RestTemplate template = new RestTemplate(httpRequestFactory());
    template.getInterceptors().add(new LoggingReqRespInterceptor());
    
    return template;
  }
  
  @Bean
  public ClientHttpRequestFactory httpRequestFactory()
  {
    return new HttpComponentsClientHttpRequestFactory(httpClient());
  }
  
  @Bean
  public HttpClient httpClient()
  {
    Registry<ConnectionSocketFactory> registry = RegistryBuilder.create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", SSLConnectionSocketFactory.getSocketFactory()).build();
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
    connectionManager.setMaxTotal(this.httpClientProperties.getMaxTotal());
    connectionManager.setDefaultMaxPerRoute(this.httpClientProperties.getDefaultMaxPerRoute());
    connectionManager.setValidateAfterInactivity(this.httpClientProperties.getValidateAfterInactivity());    

    RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(this.httpClientProperties.getSocketTimeout()).setConnectTimeout(this.httpClientProperties.getConnectTimeout()).setConnectionRequestTimeout(this.httpClientProperties.getConnectionRequestTimeout()).build();
    
    HttpClientBuilder clientBuilder = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).setConnectionManager(connectionManager);
    try
    {
      SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy()
      {
        public boolean isTrusted(X509Certificate[] arg0, String arg1)
          throws CertificateException
        {
          return true;
        }
      }).build();
      SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, new String[] { "TLSv1" }, null, NoopHostnameVerifier.INSTANCE);
      
      clientBuilder.setConnectionManager(connectionManager).setSSLSocketFactory(csf);
    }
    catch (NoSuchAlgorithmException|KeyStoreException|KeyManagementException e)
    {
      logger.error("SSL context configuring failed, HTTPS cannot be used in RestTemplate.", e);
    }
    return clientBuilder.build();
  }
}

 改后的配置:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.hfi.health.starters.web;

import com.alibaba.fastjson.JSON;
import com.hfi.health.starters.common.util.HfiLogger;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import javax.net.ssl.SSLContext;
import org.apache.http.HeaderElement;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
@EnableConfigurationProperties({HttpClientProperties.class})
public class RestTemplateAutoConfiguration {
    private static final Logger logger = HfiLogger.create(RestTemplateAutoConfiguration.class);
    @Autowired
    private HttpClientProperties httpClientProperties;

    public RestTemplateAutoConfiguration() {
    }

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate template = new RestTemplate(this.httpRequestFactory());
        template.getInterceptors().add(new LoggingReqRespInterceptor());
        return template;
    }

    @Bean
    public ClientHttpRequestFactory httpRequestFactory() {
        return new HttpComponentsClientHttpRequestFactory(this.httpClient());
    }

    @Bean
    public HttpClient httpClient() {
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", SSLConnectionSocketFactory.getSocketFactory()).build();
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
        connectionManager.setMaxTotal(this.httpClientProperties.getMaxTotal());
        connectionManager.setDefaultMaxPerRoute(this.httpClientProperties.getDefaultMaxPerRoute());
        connectionManager.setValidateAfterInactivity(this.httpClientProperties.getValidateAfterInactivity());
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(this.httpClientProperties.getSocketTimeout()).setConnectTimeout(this.httpClientProperties.getConnectTimeout()).setConnectionRequestTimeout(this.httpClientProperties.getConnectionRequestTimeout()).build();
        HttpClientBuilder clientBuilder = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).setConnectionManager(connectionManager);

        try {
            SSLContext sslContext = (new SSLContextBuilder()).loadTrustMaterial((KeyStore)null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                    return true;
                }
            }).build();
            SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1"}, (String[])null, NoopHostnameVerifier.INSTANCE);
            clientBuilder.setConnectionManager(connectionManager).setSSLSocketFactory(csf);
            clientBuilder.setKeepAliveStrategy(this.connectionKeepAliveStrategy());
        } catch (KeyStoreException | KeyManagementException | NoSuchAlgorithmException var7) {
            logger.error("SSL context configuring failed, HTTPS cannot be used in RestTemplate.", var7);
        }

        return clientBuilder.build();
    }

    public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
        return (response, context) -> {
            BasicHeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator("Keep-Alive"));

            while(true) {
                String param;
                String value;
                do {
                    do {
                        if (!it.hasNext()) {
                            HttpHost target = (HttpHost)context.getAttribute("http.target_host");
                            Optional<Entry<String, Integer>> any = ((Map)Optional.ofNullable(this.httpClientProperties.getKeepAliveTargetHost()).orElseGet(HashMap::new)).entrySet().stream().filter((e) -> {
                                return ((String)e.getKey()).equalsIgnoreCase(target.getHostName());
                            }).findAny();
                            return (Long)any.map((en) -> {
                                return (long)(Integer)en.getValue() * 1000L;
                            }).orElse(this.httpClientProperties.getKeepAliveTime() * 1000L);
                        }

                        HeaderElement he = it.nextElement();
                        logger.info("HeaderElement:{}", JSON.toJSONString(he));
                        param = he.getName();
                        value = he.getValue();
                    } while(value == null);
                } while(!"timeout".equalsIgnoreCase(param));

                try {
                    return Long.parseLong(value) * 1000L;
                } catch (NumberFormatException var8) {
                    logger.error("Error occurs while parsing timeout settings of keep-alived connection.", var8);
                }
            }
        };
    }
}

 关键代码在这里:

红框的代码是设置http的keep-alive的策略的方法;
keep-alive的作用:使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。Web服务器,基本上都支持HTTP Keep-Alive。

keep-alive使用场景:我们相当于有少数固定客户端,长时间极高频次的访问服务器,启用keep-alive非常合适。

2.2 RestTemplate设置成短连接

 

二、HTTP协议中的长连接和短连接(keep-alive状态)

2.1 HTTP Keep-Alive

在http早期,每个http请求都要求打开一个tpc socket连接,并且使用一次之后就断开这个tcp连接。

使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数,也意味着可以减少TIME_WAIT状态连接,以此提高性能和提高httpd服务器的吞吐率(更少的tcp连接意味着更少的系统内核调用,socket的accept()和close()调用)。

但是,keep-alive并不是免费的午餐,长时间的tcp连接容易导致系统资源无效占用。配置不当的keep-alive,有时比重复利用连接带来的损失还更大。所以,正确地设置keep-alive timeout时间非常重要。

2.2 keepalvie timeout

Httpd守护进程,一般都提供了keep-alive timeout时间设置参数。比如nginx的keepalive_timeout,和Apache的KeepAliveTimeout。这个keepalive_timout时间值意味着:一个http产生的tcp连接在传送完最后一个响应后,还需要hold住keepalive_timeout秒后,才开始关闭这个连接。

 此处参考文章:HTTP Keep-Alive是什么?如何工作?

2.3 长连接与短连接

  • 长连接:client方与server方先建立连接,连接建立后不断开,然后再进行报文发送和接收。这种方式下由于通讯连接一直存在。此种方式常用于P2P通信。
  • 短连接:Client方与server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此方式常用于一点对多点通讯。C/S通信。

2.4 长连接与短连接的操作过程

短连接的操作步骤是:
建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接

长连接的操作步骤是:
建立连接——数据传输...(保持连接)...数据传输——关闭连接

 2.5 长连接与短连接的使用时机

短连接多用于操作频繁,点对点的通讯,而且连接数不能太多的情况。每个TCP连接的建立都需要三次握手,每个TCP连接的断开要四次握手。

如果每次操作都要建立连接然后再操作的话处理速度会降低,所以每次操作后,下次操作时直接发送数据就可以了,不用再建立TCP连接。例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,频繁的socket创建也是对资源的浪费。

Web网站的http服务一般都用短连接,因为长连接对于服务器来说要耗费一定的资源。像web网站这么频繁的成千上万甚至上亿客户端的连接用短连接更省一些资源。试想如果都用长连接,而且同时用成千上万的用户,每个用户都占有一个连接的话,可想而知服务器的压力有多大。所以并发量大,但是每个用户又不需频繁操作的情况下需要短连接。

总之:长连接和短连接的选择要根据需求而定。
长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。

2.6 HTTP协议长连接、短连接总结

长连接与短连接的不同主要在于client和server采取的关闭策略不同。短连接在建立连接以后只进行一次数据传输就关闭连接,而长连接在建立连接以后会进行多次数据数据传输直至关闭连接(长连接中关闭连接通过Connection:closed头部字段)。

2.7 二者关闭策略的不同,就产生了长连接的优点

  • 通过开启、关闭更少的TCP连接,节约CPU时间和内存
  • 通过减少TCP开启引起的包的数目,降低网络阻塞。

二者所应用的具体场景不同。短连接多用于操作频繁、点对点的通讯,且连接数不能太多的情况。数据库的连接则采用长连接。

此小节参考文章:HTTP协议中的长连接与短连接

三、TCP的keepalive和HTTP的keepalive之间的区别

两者是完全不同的概念,只是凑巧名字相同。
tcp的keepalive指的是:周期性的去检查链接是否有效(working),经过一个时钟周期(keepalive_timeout)之后发送一个空的探测报文来检查;
HTTP的keepalive 表示:是否允许在同一次TCP链接中进行多次的HTTP请求,从而减少tcp链接建立和断开造成的开销

参考文章:HTTP协议中的长连接和短连接(keep-alive状态)

四、如何配置HTTP自定义KeepAlive策略

4.1RestTemlate自定义KeepAlive策略示例

/**
 * SpringBoot启动类
 */
@SpringBootApplication
@Slf4j
public class CustomerServiceApplication implements ApplicationRunner {
   @Autowired
   private RestTemplate restTemplate;
 
   public static void main(String[] args) {
      new SpringApplicationBuilder()
            .sources(CustomerServiceApplication.class)
            .bannerMode(Banner.Mode.OFF)
            .web(WebApplicationType.NONE)
            .run(args);
   }
 
   @Bean
   public HttpComponentsClientHttpRequestFactory requestFactory() {
      PoolingHttpClientConnectionManager connectionManager =
            new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
      connectionManager.setMaxTotal(200);
      connectionManager.setDefaultMaxPerRoute(20);
 
      CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .evictIdleConnections(30, TimeUnit.SECONDS)
            .disableAutomaticRetries()
            // 有 Keep-Alive 认里面的值,没有的话永久有效
            //.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
            // 换成自定义的
            .setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
            .build();
 
      HttpComponentsClientHttpRequestFactory requestFactory =
            new HttpComponentsClientHttpRequestFactory(httpClient);
 
      return requestFactory;
   }
 
   @Bean
   public RestTemplate restTemplate(RestTemplateBuilder builder) {
//    return new RestTemplate();
 
      return builder
            .setConnectTimeout(Duration.ofMillis(100))
            .setReadTimeout(Duration.ofMillis(500))
            .requestFactory(this::requestFactory)
            .build();
   }
 
   @Override
   public void run(ApplicationArguments args) throws Exception {
      URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:8080/coffee/?name={name}")
            .build("mocha");
      RequestEntity<Void> req = RequestEntity.get(uri)
//          .accept(MediaType.APPLICATION_XML)
            .build();
      ResponseEntity<String> resp = restTemplate.exchange(req, String.class);
      log.info("Response Status: {}, Response Headers: {}", resp.getStatusCode(), resp.getHeaders().toString());
      log.info("Coffee: {}", resp.getBody());
   }
}
 
 
/**
 * KeepAlive策略
 */
public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
    private final long DEFAULT_SECONDS = 30;
 
    @Override
    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        return Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE))
                .stream()
                .filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout")
                        && StringUtils.isNumeric(h.getValue()))
                .findFirst()
                .map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS))
                .orElse(DEFAULT_SECONDS) * 1000;
    }
}

 

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
org.apache.http.ConnectionClosedException.class org.apache.http.ConnectionReuseStrategy.class org.apache.http.Consts.class org.apache.http.ContentTooLongException.class org.apache.http.ExceptionLogger.class org.apache.http.FormattedHeader.class org.apache.http.Header.class org.apache.http.HeaderElement.class org.apache.http.HeaderElementIterator.class org.apache.http.HeaderIterator.class org.apache.http.HttpClientConnection.class org.apache.http.HttpConnection.class org.apache.http.HttpConnectionFactory.class org.apache.http.HttpConnectionMetrics.class org.apache.http.HttpEntity.class org.apache.http.HttpEntityEnclosingRequest.class org.apache.http.HttpException.class org.apache.http.HttpHeaders.class org.apache.http.HttpHost.class org.apache.http.HttpInetConnection.class org.apache.http.HttpMessage.class org.apache.http.HttpRequest.class org.apache.http.HttpRequestFactory.class org.apache.http.HttpRequestInterceptor.class org.apache.http.HttpResponse.class org.apache.http.HttpResponseFactory.class org.apache.http.HttpResponseInterceptor.class org.apache.http.HttpServerConnection.class org.apache.http.HttpStatus.class org.apache.http.HttpVersion.class org.apache.http.MalformedChunkCodingException.class org.apache.http.MessageConstraintException.class org.apache.http.MethodNotSupportedException.class org.apache.http.NameValuePair.class org.apache.http.NoHttpResponseException.class org.apache.http.ParseException.class org.apache.http.ProtocolException.class org.apache.http.ProtocolVersion.class org.apache.http.ReasonPhraseCatalog.class org.apache.http.RequestLine.class org.apache.http.StatusLine.class org.apache.http.TokenIterator.class org.apache.http.TruncatedChunkException.class org.apache.http.UnsupportedHttpVersionException.class org.apache.http.annotation.Contract.class org.apache.http.annotation.Experimental.class org.apache.http.annotation.Obsolete.class org.apache.http.annotation.ThreadingBehavior.class org.apache.http.annotation.package-info.class org.apache.http.concurrent.BasicFuture.class org.apache.http.concurrent.Cancellable.class org.apache.http.concurrent.FutureCallback.class org.apache.http.concurrent.package-info.class org.apache.http.config.ConnectionConfig.class org.apache.http.config.Lookup.class org.apache.http.config.MessageConstraints.class org.apache.http.config.Registry.class org.apache.http.config.RegistryBuilder.class org.apache.http.config.SocketConfig.class org.apache.http.config.package-info.class org.apache.http.entity.AbstractHttpEntity.class org.apache.http.entity.BasicHttpEntity.class org.apache.http.entity.BufferedHttpEntity.class org.apache.http.entity.ByteArrayEntity.class org.apache.http.entity.ContentLengthStrategy.class org.apache.http.entity.ContentProducer.class org.apache.http.entity.ContentType.class org.apache.http.entity.EntityTemplate.class org.apache.http.entity.FileEntity.class org.apache.http.entity.HttpEntityWrapper.class org.apache.http.entity.InputStreamEntity.class org.apache.http.entity.SerializableEntity.class org.apache.http.entity.StringEntity.class org.apache.http.entity.package-info.class org.apache.http.impl.AbstractHttpClientConnection.class org.apache.http.impl.AbstractHttpServerConnection.class org.apache.http.impl.BHttpConnectionBase.class org.apache.http.impl.ConnSupport.class org.apache.http.impl.DefaultBHttpClientConnection.class org.apache.http.impl.DefaultBHttpClientConnectionFactory.class org.apache.http.impl.DefaultBHttpServerConnection.class org.apache.http.impl.DefaultBHttpServerConnectionFactory.class org.apache.http.impl.DefaultConnectionReuseStrategy.class org.apache.http.impl.DefaultHttpClientConnection.class org.apache.http.impl.DefaultHttpRequestFactory.class org.apache.http.impl.DefaultHttpResponseFactory.class org.apache.http.impl.DefaultHttpServerConnection.class org.apache.http.impl.EnglishReasonPhraseCatalog.class org.apache.http.impl.HttpConnectionMetricsImpl.class org.apache.http.impl.NoConnectionReuseStrategy.class org.apache.http.impl.SocketHttpClientConnection.class org.apache.http.impl.SocketHttpServerConnection.class org.apache.http.impl.bootstrap.HttpServer.class org.apache.http.impl.bootstrap.RequestListener.class org.apache.http.impl.bootstrap.SSLServerSetupHandler.class org.apache.http.impl.bootstrap.ServerBootstrap.class org.apache.http.impl.bootstrap.ThreadFactoryImpl.class

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值