OkHttp3源码分析(一)

okhttp3源码分析之AsyncTimeout 继承自Timeout, 使后台线程在发生超时时准确执行操作。用来实现本机不支持的超时。 公开方法: final void enter() 调用了private static synchronized void scheduleTimeout,确定链表关系(当前node上个的next和this的header前后关系)。 private static synchronized void s... 阅读详情

一、首先对Request类做一下分析:

Request就是组装Http发起的请求;Http发起的请求包含什么可以看一下HTTP协议格式和header
看一下源码:

public final class Request {
  private final HttpUrl url;
  private final String method;
  private final Headers headers;
  private final RequestBody body;
  private final Object tag;

  private volatile URI javaNetUri; // Lazily initialized.
  private volatile CacheControl cacheControl; // Lazily initialized.

  private Request(Builder builder) {
    this.url = builder.url;
    this.method = builder.method;
    this.headers = builder.headers.build();
    this.body = builder.body;
    this.tag = builder.tag != null ? builder.tag : this;
  }

看到没?其实就是url、method、headers、requestBody
实例:

Request request = new Request.Builder()
                .url("")
                .addHeader("", "")
                .addHeader("", "")
                .get()
                .build();

二、再对OkHttpClient类做下分析

OKHttpClinet里的内容其实就是Builder内的内容

public static final class Builder {
    Dispatcher dispatcher;
    Proxy proxy;
    List<Protocol> protocols;
    List<ConnectionSpec> connectionSpecs;
    final List<Interceptor> interceptors = new ArrayList<>();
    final List<Interceptor> networkInterceptors = new ArrayList<>();
    ProxySelector proxySelector;
    CookieJar cookieJar;
    Cache cache;
    InternalCache internalCache;
    SocketFactory socketFactory;
    SSLSocketFactory sslSocketFactory;
    TrustRootIndex trustRootIndex;
    HostnameVerifier hostnameVerifier;
    CertificatePinner certificatePinner;
    Authenticator proxyAuthenticator;
    Authenticator authenticator;
    ConnectionPool connectionPool;
    Dns dns;
    boolean followSslRedirects;
    boolean followRedirects;
    boolean retryOnConnectionFailure;
    int connectTimeout;
    int readTimeout;
    int writeTimeout;

    public Builder() {
      dispatcher = new Dispatcher();
      protocols = DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      proxySelector = ProxySelector.getDefault();
      cookieJar = CookieJar.NO_COOKIES;
      socketFactory = SocketFactory.getDefault();
      hostnameVerifier = OkHostnameVerifier.INSTANCE;
      certificatePinner = CertificatePinner.DEFAULT;
      proxyAuthenticator = Authenticator.NONE;
      authenticator = Authenticator.NONE;
      connectionPool = new ConnectionPool();
      dns = Dns.SYSTEM;
      followSslRedirects = true;
      followRedirects = true;
      retryOnConnectionFailure = true;
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
    }

对里面几个主要的解释一下:

  1. dispatcher:分发器,用于分发任务的。相当于服务器的nginx
  2. List interceptors = new ArrayList<>():应用拦截器
  3. List networkInterceptors = new ArrayList<>():网络拦截器
  4. Dns:用于解析dns的
  5. connectTimeout: socket的connectTimeOut
  6. readTimeout:socket的readTimeout
  7. writeTimeout:socket的writeTimeout

三、重点介绍RealCall类

构造函数

final class RealCall implements Call {
  private final OkHttpClient client;

  // Guarded by this.
  private boolean executed;
  volatile boolean canceled;

  /** The application's original request unadulterated by redirects or auth headers. */
  Request originalRequest;
  HttpEngine engine;

protected RealCall(OkHttpClient client, Request originalRequest) {
   this.client = client;
   this.originalRequest = originalRequest;
 }

可以看到把OkHttpClient和Request传过来了

再看一下enqueue方法,这个其实就是咱们发起的异步任务

@Override
public void enqueue(Callback responseCallback) {
  enqueue(responseCallback, false);
}

void enqueue(Callback responseCallback, boolean forWebSocket) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
}

client就是OkHttpClient,client.dispatcher()就是咱们在OkHttpClient里面初始化的dispatcher

所以下一步走到了Dispatcher.enqueue(new AsyncCall(responseCallback, forWebSocket));

先分析一下AsyncCall类

final class AsyncCall extends NamedRunnable {
  private final Callback responseCallback;
  private final boolean forWebSocket;

  private AsyncCall(Callback responseCallback, boolean forWebSocket) {
    super("OkHttp %s", originalRequest.url().toString());
    this.responseCallback = responseCallback;
    this.forWebSocket = forWebSocket;
  }

  String host() {
    return originalRequest.url().host();
  }

  Request request() {
    return originalRequest;
  }

  Object tag() {
    return originalRequest.tag();
  }

  void cancel() {
    RealCall.this.cancel();
  }

  RealCall get() {
    return RealCall.this;
  }

  @Override
  protected void execute() {
    boolean signalledCallback = false;
    try {
      Response response = getResponseWithInterceptorChain(forWebSocket);
      if (canceled) {
        signalledCallback = true;
        responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
      } else {
        signalledCallback = true;
        responseCallback.onResponse(RealCall.this, response);
      }
    } catch (IOException e) {
      if (signalledCallback) {
        // Do not signal the callback twice!
        logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
      } else {
        responseCallback.onFailure(RealCall.this, e);
      }
    } finally {
      client.dispatcher().finished(this);
    }
  }
}

发现AsyncCall继承了NamedRunnable;分析一下NamedRunnable的源码

public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = String.format(format, args);
  }

  @Override
  public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}

很简单,NamedRunnable就是一个任务,然后再run()里调用了execute();
所以最终AsyncCall也是一个任务,执行方法在execute();
分析一下execute方法:

try {
      Response response = getResponseWithInterceptorChain(forWebSocket);
      if (canceled) {
        signalledCallback = true;
        responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
      } else {
        signalledCallback = true;
        responseCallback.onResponse(RealCall.this, response);
      }
    } catch (IOException e) {
      if (signalledCallback) {
        // Do not signal the callback twice!
        logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
      } else {
        responseCallback.onFailure(RealCall.this, e);
      }
    } finally {
      client.dispatcher().finished(this);
    }
  1. 进行网络请求getResponseWithInterceptorChain()
  2. 是否取消了请求,取消了就回调onFailure
  3. 正常回调onResponse
  4. 出现异常也是回调onFailure
  5. finally移除任务
private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
    Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
    return chain.proceed(originalRequest);
  }

走到了应用拦截器里面ApplicationInterceptorChain

class ApplicationInterceptorChain implements Interceptor.Chain {
   private final int index;
   private final Request request;
   private final boolean forWebSocket;

   ApplicationInterceptorChain(int index, Request request, boolean forWebSocket) {
     this.index = index;
     this.request = request;
     this.forWebSocket = forWebSocket;
   }

   @Override
   public Connection connection() {
     return null;
   }

   @Override
   public Request request() {
     return request;
   }

   @Override
   public Response proceed(Request request) throws IOException {
     // If there's another interceptor in the chain, call that.
     if (index < client.interceptors().size()) {
       Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
       Interceptor interceptor = client.interceptors().get(index);
       Response interceptedResponse = interceptor.intercept(chain);

       if (interceptedResponse == null) {
         throw new NullPointerException("application interceptor " + interceptor
             + " returned null");
       }

       return interceptedResponse;
     }

     // No more interceptors. Do HTTP.
     return getResponse(request, forWebSocket);
   }
 }

分析proceed方法:其实相当于一个循环,OkHttpClient.Builder.addInterceptor(Interceptor interceptor) 添加的interceptors循环一个个对Request进行封装,然后把最后封装好的Request扔给getResponse(request, forWebSocket)

Response getResponse(Request request, boolean forWebSocket) throws IOException {
    // Copy body metadata to the appropriate request headers.
    RequestBody body = request.body();
    if (body != null) {
      Request.Builder requestBuilder = request.newBuilder();

      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }

      request = requestBuilder.build();
    }

    // Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
    engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);

    int followUpCount = 0;
    while (true) {
      if (canceled) {
        engine.releaseStreamAllocation();
        throw new IOException("Canceled");
      }

      boolean releaseConnection = true;
      try {
        engine.sendRequest();//真正的发送请求
        engine.readResponse();//读取response
        releaseConnection = false;
      } catch (RequestException e) {
        // The attempt to interpret the request failed. Give up.
        throw e.getCause();
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        //出现异常 重新再次请求
        HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null);
        if (retryEngine != null) {
          releaseConnection = false;
          engine = retryEngine;
          continue;
        }
        // Give up; recovery is not possible.
        throw e.getLastConnectException();
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        //出现异常 重新再次请求
        HttpEngine retryEngine = engine.recover(e, null);
        if (retryEngine != null) {
          releaseConnection = false;
          engine = retryEngine;
          continue;
        }

        // Give up; recovery is not possible.
        throw e;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        //没有重连就释放连接
        if (releaseConnection) {
          StreamAllocation streamAllocation = engine.close();
          streamAllocation.release();
        }
      }

      Response response = engine.getResponse();//获取response
      Request followUp = engine.followUpRequest();//是否有重定向

      if (followUp == null) {//没有重定向
        if (!forWebSocket) {
          engine.releaseStreamAllocation();//释放connect
        }
        return response;
      }

      StreamAllocation streamAllocation = engine.close();

      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      // 如果重定向的地址和当前的地址一样,则不需要释放连接
      if (!engine.sameConnection(followUp.url())) {
        streamAllocation.release();
        streamAllocation = null;
      }

      request = followUp;
      //根据重定向重新组装engine
      engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
          response);
    }
  }

分析一下getResponse()

  1. 组装request
  2. 初始化HttpEngine;engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);
  3. 发送请求:engine.sendRequest();
  4. 读取响应 :engine.readResponse();
  5. 是否有异常:是否用重连,发起重连
  6. 获取response:Response response = engine.getResponse();
  7. 是否有重定向:Request followUp = engine.followUpRequest();
  8. 没有重定向,释放connect,返回response:engine.releaseStreamAllocation();return response;
  9. 有重定向:组装engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
    response)循环执行
OKHttp3源码分析——学习笔记 文章目录1. HttpClient与HttpUrlConnection的区别2. OKHttp源码分析使用步骤:dispatcher任务调度器,Request请求RealCallAsyncCall3. OKHttp架构分析1. 异步请求线程池, Dispather2. 连接池清理线程池- ConnectionPool3. 缓存整理线程池 DisLruCache4. Http2异步事务线程池, ht... 阅读详情

相关推荐

微信小程序审核避坑指南:巧用 wx.getAccountInfoSync 实现审核与线上环境无感切换

本文详细介绍了如何利用`wx.getAccountInfoSync`API实现微信小程序审核与线上环境的无感切换。通过环境识别技术和动态渲染架构设计,开发者可以优雅解决审核资质门槛问题,同时保持生产环境的完整用户体验。文章包含组件化实现、全局状态管理等实用方案,帮助开发者提升审核通过率。

weixin_30527423的博客 260

OkHttp3源码详解

OkHttp3源码详解

QG 1915

Python脚本自动化芯片设计?PyAether实战教程带你玩转EDA

本文通过PyAether实战教程,详细介绍了如何利用Python脚本实现芯片设计自动化。内容涵盖环境搭建、核心对象模型解析,并重点演示了原理图自动生成、参数化单元创建及设计验证流程集成,帮助EDA工程师从重复性手动操作中解放,显著提升设计效率与一致性。

e4f5g6h的博客 1314

OkHttp3源码解析 - 请求流程

在Android蛮荒年代,我们做网络请求通常会选用HttpURLConnection或者Apache HTTP Client,这两者均支持HTTPS、流的上传和下载、配置超时和连接池等特性,但随着业务场景的复杂化以及对流量消耗的优化需求,OkHttp应运而生。现在谈起网络请求,大家肯定下意识想到的就是 okHttp 或者 retrofit 这样的三方请求库。

Android百晓生的博客 1072

飞书文档API深度使用指南:如何用n8n社区节点绕过HTTP Request的复杂配置

本文详细解析了如何利用n8n社区节点简化飞书文档API的集成过程,绕过复杂的HTTP Request配置。通过n8n-feishu-lite社区节点,开发者可以轻松实现飞书文档的创建、编辑和查询操作,大幅提升自动化工作流的效率。文章还提供了高级应用场景和性能优化建议,帮助开发者更好地利用飞书开放平台的API功能。

weixin_29227585的博客 83

AutoSAR RTE调度机制揭秘:从定时器事件到Runnable执行的完整流程

本文深入解析了AutoSAR RTE(运行时环境)的核心调度机制,详细阐述了从定时器事件触发到Runnable(可运行实体)执行的完整流程。通过剖析Rte_Start()函数、OS任务与警报的交互,以及事件驱动循环,揭示了RTE如何作为幕后指挥家,高效、可靠地协调软件组件间的周期性任务执行,是打通AutoSAR应用开发的关键。

weixin_29169899的博客 226

OkHttp3源码解析(高工视角)

本文解读的OkHttp版本是3.14.9,拉取自master分支的parent-3.14.9(tag);这是OkHttp团队将okhttp项目migrate to Kotlin前的最后一个版本。java是最...算了不拉仇恨了^_^。 1.http/1.1 2.http/2.0

m0_37840695的博客 842

STM32 RC522串口驱动程序 (简单直接)

rc522作为使用非常多的读写卡芯片,使用SPI芯片读写应用已经非常广了。但有些时候我们只有串口可以使用,或者我们觉得串口使用更加方便,那么就必须要编写串口的驱动程序。不过很难找到可以直接使用的串口程序。所以在此将我这边写好的串口驱动写好如下: 直接上代码 下面是RC522.C的驱动文件。支持读写卡号8位数据。rc522用的是stm32 的串口3,调试显示是串口1; #include "rc522.h" #include "delay.h" /////////////////////////////////

qq_42074368的博客 9001

OkHttp3源码解析--设计模式

看到这篇文章的人不知道有多少是和我一样的Android程序员。35岁,这是我们这个行业普遍的失业高发阶段,这种情况下如果还不提升自己的技能,进阶发展,我想,很可能就是本行业的职业生涯的终点了。我们要有危机意识,切莫等到一切都成定局时才开始追悔莫及。只要有规划的,有系统地学习,进阶提升自己并不难,给自己多充一点电,你才能走的更远。千里之行始于足下。这是上小学时,那种一元钱一个的日记本上每一页下面都印刷有的一句话,当时只觉得这句话很短,后来渐渐长大才慢慢明白这句话的真正的含义。

2301_79985012的博客 891

从Simulink到Amesim:一份FMU联合仿真的避坑指南(含UDP通讯完整配置)

本文详细解析了从Simulink到Amesim的FMU联合仿真全流程,重点介绍了UDP通讯配置中的常见陷阱与解决方案。通过实战案例,帮助工程师规避编译器不兼容、步长设置冲突等典型问题,提升复杂系统仿真的效率与准确性。

weixin_34046298的博客 336

SAP 系统间 SSO 的真正价值,从 SM59 到可信 RFC 的安全设计

SAP S/4HANA系统间Single Sign-On(SSO)的核心价值在于集中管理通信身份与信任关系,而非仅减少密码输入。传统固定账号密码方式在系统规模扩大后会面临密码管理、审计追踪等挑战。通过SM59配置可信RFC目标系统,结合SSL证书或SAP Trusted System等强认证机制,能有效降低安全凭据暴露风险。SSO配置需配合授权对象、角色管理等建立清晰的信任边界,实现系统间安全通信的同时保持可审计性。这种方案特别适用于Fiori前端与S/4HANA后端等场景,通过可信RFC传递用户身份,避免

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1062

OKHttp3 源码阅读 - Kotlin版本

OKHttp 是由 Square 公司开源的,广泛应用于 Android 开发中,并且是 Retrofit 的底层实现。它是一个高效的 HTTP 客户端,适用于 Android 和 Java 应用程序。它支持 HTTP/2、连接池、GZIP 压缩、缓存等功能,能够帮助开发者更高效地处理网络请求。从 Android 4.4 开始 HttpURLConnection 的底层实现采用的是OKHttp。OkHttp是一个功能强大且灵活的HTTP客户端库,适用于各种网络请求场景。

Bonnie_cat的博客 1158

大数据分析实验,包含五个子实验:wordCount实验,PageRank实验,关系挖掘实验,k-means算法,推荐系统算法。

大数据分析实验,包含五个子实验:wordCount实验,PageRank实验,关系挖掘实验,k-means算法,推荐系统算法。

805

okhttp3.7源码分析(一)

1.okhttp 特点: 1.支持HTTPS/HTTP2/WebSocket(在OkHttp3.7中已经剥离对Spdy的支持,转而大力支持HTTP2) 2.内部维护任务队列线程池,友好支持并发访问 3.内部维护连接池,支持多路复用,减少连接创建开销 4.socket创建支持最佳路由 5.提供拦截器链(InterceptorChain),实现request与response的分层处理(如透明GZIP...

weixin_43843686的博客 275

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

OkHttp3源码分析(一)-整体执行流程

对OkHttp介绍,我们分为使用篇和源码分析篇两个系列进行介绍。 源码分析篇目录如下: (一)-整体执行流程 在OkHttp使用篇当中,我们分几篇文章介绍了OkHttp框架的使用,但并没有去分析整个框架的源码和实现。这里单独开一个系列来捋一捋OkHttp3的源码,整个过程也是我自己学习的过程,所以有不好的地方还请见谅了。 下面就进入到本篇文章主题,从源码角度分析下整体的执行流程。在OkHttp3使...

VictorCatFish的博客 485

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

okhttp3简单实用及源码分析(一)

首先把项目跑起来,简单配置一下okhttp然后发起请求 //一、配置基本参数,用于发送http请求,并读取响应 OkHttpClient okHttpClient = new OkHttpClient.Builder() .readTimeout(5, TimeUnit.SECONDS) .build(); //二、配置请求参数 Request request = new Request...

楊帥 424

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

OKHttp3源码解析(一)同步请求的源码分析

OKHTTP是什么? 百度一搜:”一个处理网络请求的开源项目,是安卓端最火热的轻量级框架,由移动支付Square公司贡献(该公司还贡献了Picasso)用于替代HttpUrlConnection和Apache HttpClient(android API23 6.0里已移除HttpClient,现在已经打不出来)” 正常的Http协议 OKHTTP的开发 同步请求 OkHttpClient okHttpClient = new OkHttpClient.Builder().build();//.

万子的博客 567

java okhttp3源码,OkHttp源码分析(一) 请求和响应过程简单分析

这篇文章简要介绍OkHttp的请求和响应过程。0 前言​ OkHttp作为当下Java系编程的网络请求库,其热门程度自不必说了。网上有关OkHttp的使用、封装和源码分析的文章和代码也早已是百家齐放、甚至可以说是烂大街了。然而尽管如此,笔者还是希望能够将对OkHttp的学习和研究记录下来形成自己的内容,方便以后查看,于是开始写吧,好记性不如烂笔头。​ 那就从最简单的开始...

weixin_33329746的博客 454

OKHttp3--详细使用及源码分析系列之初步介绍【一】

说OHHttp是当下Android使用最频繁的网络请求框架应该是不过分的,是由Square公司贡献的一款开源工程;用于替代之前的HttpURLConnection(Google在Android4.4以后开始将源码中的HttpURLConnection替换成了OKHttp,但是HttpURLConnection的API还是在的,可以继续使用)和HttpClient(Google已经在Android6.0以后移除了HttpClient相关的API,已经不能继续使用);同时现在流行的Retrofit框架底层同样是

Mango先生的博客 1660

OkHttp 3.7源码分析(一)——整体架构

OkHttp3.7源码分析文章列表如下: OkHttp源码分析——整体架构 OkHttp源码分析——拦截器 OkHttp源码分析——任务队列 OkHttp源码分析——缓存策略 OkHttp源码分析——多路复用 OkHttp是一个处理网络请求的开源项目,是Android端最火热的轻量级框架,由移动支付Square公司贡献用于替代HttpUrlConnection和Apache HttpClie

贵公子丶笔记 238

Android OkHttp3源码分析

title: Android OkHttp3源码分析 date: 2019-06-03 21:16:50 tags: Android第三方框架 文章目录title: Android OkHttp3源码分析 date: 2019-06-03 21:16:50 tags: Android第三方框架Android OkHttp3源码分析介绍源码分析简单使用OkHttpClient的创建Request的...

hesheng1024的博客 2662

开源库源码分析:Okhttp源码分析(一)

接下来就要开始分析一些常用开源库的源码了,作为最常用的网络请求库,OkHttp以其强大的功能深受Android开发者的喜爱(比如说我),还有对该库进行二次封装而成的热门库,比如说Retrofit。本文我们将从源码入手看看OkHttp是如何运作的。注意本文解析的是OkHttp3库,该库是用Kotlin写的,需要大家有一些Kotlin基础。

Tai_Monster的博客 4525
上一篇: Android中布局中常用到的<include> <merge> <ViewStub>
下一篇: OkHttp3源码分析(二)
豪豪6
博客等级 码龄14年 6粉丝 35原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值