Android常用开源组件探索-OkHttp(2)原理详解

1、概述

OKHTTP请求流程

OkHttp 是 Square 公司开源的一款网络框架,一般和 Retrofit、RxJava 或协程一起使用。OkHttp 支持发起同步请求和异步请求,同步请求对应类的是 RealCall ,异步请求对应的是 AsyncCall ,AsynCall 是 RealCall 的内部类。RealCall 和 AsyncCall 可以理解为同步请求操作和异步请求操作。

当用 RealCall 的 execute() 方法发起同步请求时,请求会被请求分发器 Dispatcher 放到同步请求操作队列中,然后直接用 Dispatcher 的 executed() 方法执行请求。

当用 RealCall 的 enqueue() 方法发起异步请求时,RealCall 会创建一个 AsyncCall 并传到 Dispatcher 的 enqueue() 方法中。Dispatcher 会把异步请求放到异步请求操作队列,然后后把异步请求提交到线程池中执行,当异步请求被执行时,RealCall 会通过拦截器链 发起请求,拦截器链中各个拦截器处理请求的顺序为:自定义拦截器—重试与重定向拦截器—首部填充拦截器—缓存拦截器—连接拦截器 ——自定义网络拦截器—数据交换拦截器。
Dispatcher 的实现比较简单,主要是做一些请求数量判断,比如同一主机的最大请求数量默认为 5, 同时进行的异步请求数量最大为 64 ,超过这这两个值时异步请求就不会立刻提交到线程池中。

OkHttp 允许我们自定义拦截器和自定义网络拦截器,自定义拦截器是最先执行的拦截器,而网络拦截器是连接建立后才会处理请求的拦截器,而且网络拦截器不会处理 WebSocket 连接。

拦截器链 RealInterceptorChain 中有一个 proceed() 方法,各个拦截器对在处理完自己的逻辑后,就要调用 proceed() 方法让下一个拦截器处理请求。

在重试与重定向拦截器 中,proceed() 方法是放在一个 while 循环中执行的,而且还用 try-catch 代码块包住了,这样其他拦截器抛出异常时,重试与重定向拦截器才能处理这些路线异常和 IO 异常。RetryAndFollowUpInterceptor 中的重定向逻辑主要是在重试与重定向拦截器的 followUpRequest() 方法中,在这个方法中会根据不同的响应状态码构建重定向请求,比如状态码为 407 时,就会返回一个包含认证挑战的请求。

重试与重定向拦截器的下一个拦截器是 BridgeInterceptor ,这个拦截器负责给填写一些请求首部,比如把请求地址的 host 组件拿出来,放到 host 首部中。当 BridgeInterceptor 把默认需要填写信息的首部的信息填写完后,就会把请求交给缓存拦截器 CacheInterceptor 处理。
OkHttp 默认是不进行缓存的,如果如果想要缓存请求和响应的话,就要用缓存目录和缓存大小创建一个 Cache ,Cache 中使用了一个 DiskLruCachec ,也就是 Cache 使用了最近最少使用缓存算法来缓存请求和响应数据,创建好 Cache 后设置给 OkHttpClient 就可以缓存响应数据了。设置了 Cache 后,默认情况下缓存拦截器只会缓存 GET 和 HEAD 等获取资源的方法的请求,如果想对 POST 或 PUT 等修改资源的方法,就要自定义缓存拦截器。

OkHttp 的连接机制是从连接拦截器 ConnectInterceptor 的 intercept() 方法开始的,连接机制可以分为 HTTP 连接机制,HTTPS 连接机制和 HTTP/2 连接机制。

ConnectInterceptor 的 intercept() 方法主要是调用 RealCall 的 initExchange() 方法复用或建立新的连接。在 initExchange() 方法中,会调用 ExchangeFinder 的 find() 方法查找可重用的连接或创建新连接,find() 方法会通过 findHealthyConnection() 方法间接调用到连接查找方法 findConnection() 。

在 findConnection() 方法中,首先会尝试复用 RealCall 已有的连接,当 RealCall 没有连接,也就是 RealCall 的 connection 字段为空的话,就会尝试从连接池中获取连接,连接池中也没有连接的话,就会创建一个新的连接 RealConnection,并调用 RealConnection 的 connect() 方法建立连接。在 findConnection() 方法返回 RealConnection 后,find() 方法就会调用 RealConnection 的 newCodec() 方法获取并返回一个数据编译码器 ExchangeCodec 。RealCall 的 initExchange() 方法获取到 ExchangeCodec 后,会用它来创建一个数据交换器 Exchange 。

RealConnection 的 connect() 方法的核心逻辑是放在 while 循环中执行的,如果需要用到隧道(tunnel)的话,就调用 connectTunnel() 方法透传客户端和服务器的数据。否则调用 connectSocket() 方法与服务端 Socket 建立连接,再调用 establishProtocol() 方法建立协议,这个方法中涉及了 HTTPS 连接和 HTTPS/2 连接的逻辑。

在 RealConnection 的 connectSocket() 方法中,首先会用 SocketFactory 创建一个 Socket,然后会调用 Platform 的 connectSocket() 方法建立与服务端 Socket 的连接,然后把服务端 Socket 的输入流和输出流初始化自己的 source 和 sink 字段,到这里连接就建立完成了,连接建立完成后,RealCall 的 initExchange() 方法就会把 RealConnection 返回给 ConnectInterceptor,然后 ConnectInterceptor 就会把请求交给下一个拦截器 CallServerInterceptor 处理。

在 RealConnection 的 establishProtocol() 方法中,首先会判断当前请求是否有对应的 SSLSocketFactory ,也就是当前请求是否为 HTTPS 请求,如果不是的话,就会遍历请求地址 address 的协议列表 protocols ,如果协议列表中有 H2_PRIOR_KNOWLEDGE 协议的话,则调用 startHttp2() 方法发送一个 Upgrade 报文,看下服务器支不支持 HTTP/2。establishProtocol() 方法做的第二件事就是调用 connecTls() 开始 SSL/TLS 握手。 establishProtocol() 做的第三件事就是看下 RealConnection 的 protocol 是不是 HTTP/2 协议,是的话就调用 startHttp2() 建立 HTTP/2 连接。

在 startHttp2() 方法中做了两件事,一是创建一个 Http2Connection,二是调用 Http2Connection 的 start() 方法发送前奏消息、SETTINGS 帧以及 WINDOW_UPGRADE 帧。在 Http2Connection 的 start() 方法中,首先会用 Http2Writer 的 connectionPreface() 和 settings() 方法写入前奏消息和 SETTING 帧,然后调用 Http2Writer 的 windowUpdate() 方法发送窗口更新(WINDOW_UPDATE)帧,最后把流读取任务 ReaderRunnable 提交到队列中执行。

而在 connectTls() 方法中,首先会调用 SSLSocketFactory 的 createSocket() 方法创建一个 SSLSocket ,然后用 ConnectionSpecSelector 的 configureSecureSocket() 方法获取 SSLSocket 的连接规格 ConnectionSpec ,如果 ConnectionSpec 支持 TLS 扩展,就调用 Platform 的 configureTlsExtensions() 方法配置 TLS 扩展,然后再调用 SSLSocket 的 startHandshake() 方法开始 TLS 握手,然后再用 HostNameVerifier 验证 host 是否合法,然后再用 CertificatePinner 检查 host 的证书是否合法,然后把连接成功的 SSLSocket 中的输入流和输出流用来初始化 source 和 sink,最后调用 Platform 的 afterHandshake() 方法结束握手。

当 数据交换拦截器 CallServerInterceptor 接收到请求时,就会用 数据交换器 Exchange 写入请求头和请求体,而 Exchange 会通过 Socket 提供的的输出流写入请求信息,通过输入流读取响应信息,当 CallServerInterceptor 读取完了响应信息后,就会往上传递,直到把响应信息返回给最开始发起请求的地方。

2、 相关问题

2.1 OKHttp分发器是怎样工作的?

分发器的主要作用是维护请求队列与线程池,比如我们有100个异步请求,肯定不能把它们同时请求,而是应该把它们排队分个类,分为正在请求中的列表和正在等待的列表,
等请求完成后,即可从等待中的列表中取出等待的请求,从而完成所有的请求
而这里同步请求各异步请求又略有不同:

同步请求:

synchronized void executed(RealCall call) {
	runningSyncCalls.add(call);
}

因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。后续按照加入队列的顺序同步请求即可。

异步请求:

synchronized void enqueue(AsyncCall call) {
	//请求数最大不超过64,同一Host请求不能超过5个
	if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) 	  {
		runningAsyncCalls.add(call);
		executorService().execute(call);
	} else {
		readyAsyncCalls.add(call);
	}
}

当正在执行的任务未超过最大限制64,同时同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。

每个任务完成后,都会调用分发器的finished方法,这里面会取出等待队列中的任务继续执行。

2.2 OKHttp拦截器是怎样工作的?

经过上面分发器的任务分发,下面就要利用拦截器开始一系列配置了

# RealCall
  override fun execute(): Response {
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)
    }
  }

我们再来看下RealCall的execute方法,可以看出,最后返回了getResponseWithInterceptorChain,责任链的构建与处理其实就是在这个方法里面

internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        call = this,interceptors = interceptors,index = 0
    )
    val response = chain.proceed(originalRequest)
  }

如上所示,构建了一个OkHttp拦截器的责任链。

责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。

如上所示责任链添加的顺序及作用如下表所示:

拦截器作用
应用拦截器拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor处理错误重试和重定向BridgeInterceptor应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
CacheInterceptor缓存拦截器,如果命中缓存则不会发起网络请求。
ConnectInterceptor连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
networkInterceptors(网络拦截器)用户自定义拦截器,通常用于监控网络层的数据传输。CallServerInterceptor

我们的网络请求就是这样经过责任链一级一级的递推下去,最终会执行到CallServerInterceptor的intercept方法,此方法会将网络响应的结果封装成一个Response对象并return。之后沿着责任链一级一级的回溯,最终就回到getResponseWithInterceptorChain方法的返回。

2.3 应用拦截器和网络拦截器有什么区别?

从整个责任链路来看,应用拦截器是最先执行的拦截器,也就是用户自己设置request属性后的原始请求,而网络拦截器位于ConnectInterceptor和CallServerInterceptor之间,此时网络链路已经准备好,只等待发送请求数据。它们主要有以下区别:

  1. 首先,应用拦截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦发生错误重试或者网络重定向,网络拦截器可能执行多次,因为相当于进行了二次请求,但是应用拦截器永远只会触发一次。另外如果在CacheInterceptor中命中了缓存就不需要走网络请求了,因此会存在短路网络拦截器的情况。
  2. ,除了CallServerInterceptor之外,每个拦截器都应该至少调用一次realChain.proceed方法。实际上在应用拦截器这层可以多次调用proceed方法(本地异常重试)或者不调用proceed方法(中断),但是网络拦截器这层连接已经准备好,可且仅可调用一次proceed方法。
  3. 最后,从使用场景看,应用拦截器因为只会调用一次,通常用于统计客户端的网络请求发起情况;而网络拦截器一次调用代表了一定会发起一次网络通信,因此通常可用于统计网络链路上传输的数据。

2.4 OKHttp如何复用TCP连接?

ConnectInterceptor的主要工作就是负责建立TCP连接,建立TCP连接需要经历三次握手四次挥手等操作,如果每个HTTP请求都要新建一个TCP消耗资源比较多,而Http1.1已经支持keep-alive,即多个Http请求复用一个TCP连接,OKHttp也做了相应的优化,下面我们来看下OKHttp是怎么复用TCP连接的。

ConnectInterceptor中查找连接的代码会最终会调用到ExchangeFinder.findConnection方法,具体如下:

# ExchangeFinder
//为承载新的数据流 寻找 连接。寻找顺序是 已分配的连接、连接池、新建连接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  synchronized (connectionPool) {
    // 1.尝试使用 已给数据流分配的连接.(例如重定向请求时,可以复用上次请求的连接)
    releasedConnection = transmitter.connection;
    result = transmitter.connection;

    if (result == null) {
      // 2. 没有已分配的可用连接,就尝试从连接池获取。(连接池稍后详细讲解)
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
        result = transmitter.connection;
      }
    }
  }

  synchronized (connectionPool) {
    if (newRouteSelection) {
      //3. 现在有了IP地址,再次尝试从连接池获取。可能会因为连接合并而匹配。(这里传入了routes,上面的传的null)
      routes = routeSelection.getAll();
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
        foundPooledConnection = true;
        result = transmitter.connection;
      }
    }

  // 4.第二次没成功,就把新建的连接,进行TCP + TLS 握手,与服务端建立连接. 是阻塞操作
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);

  synchronized (connectionPool) {
    // 5. 最后一次尝试从连接池获取,注意最后一个参数为true,即要求 多路复用(http2.0)
    //意思是,如果本次是http2.0,那么为了保证 多路复用性,(因为上面的握手操作不是线程安全)会再次确认连接池中此时是否已有同样连接
    if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
      // 如果获取到,就关闭我们创建里的连接,返回获取的连接
      result = transmitter.connection;
    } else {
      //最后一次尝试也没有的话,就把刚刚新建的连接存入连接池
      connectionPool.put(result);
    }
  }
 
  return result;
}

上面精简了部分代码,可以看出,连接拦截器使用了5种方法查找连接:

  1. 首先会尝试使用 已给请求分配的连接。(已分配连接的情况例如重定向时的再次请求,说明上次已经有了连接)。
  2. 若没有 已分配的可用连接,就尝试从连接池中 匹配获取。因为此时没有路由信息,所以匹配条件:address一致——host、port、代理等一致,且匹配的连接可以接受新的请求。
    若从连接池没有获取到,则传入routes再次尝试获取,这主要是针对Http2.0的一个操作,Http2.0可以复用square.com与square.ca的连接
  3. 若第二次也没有获取到,就创建RealConnection实例,进行TCP + TLS握手,与服务端建立连接。
  4. 此时为了确保Http2.0连接的多路复用性,会第三次从连接池匹配。因为新建立的连接的握手过程是非线程安全的,所以此时可能连接池新存入了相同的连接。
  5. 第三次若匹配到,就使用已有连接,释放刚刚新建的连接;若未匹配到,则把新连接存入连接池并返回。

2.5 OKHttp空闲连接如何清除?

上面说到我们会建立一个TCP连接池,但如果没有任务了,空闲的连接也应该及时清除,OKHttp是如何做到的呢?

 # RealConnectionPool
  private val cleanupQueue: TaskQueue = taskRunner.newQueue()
  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
    override fun runOnce(): Long = cleanup(System.nanoTime())
  }

  long cleanup(long now) {
    int inUseConnectionCount = 0;//正在使用的连接数
    int idleConnectionCount = 0;//空闲连接数
    RealConnection longestIdleConnection = null;//空闲时间最长的连接
    long longestIdleDurationNs = Long.MIN_VALUE;//最长的空闲时间

    //遍历连接:找到待清理的连接, 找到下一次要清理的时间(还未到最大空闲时间)
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //若连接正在使用,continue,正在使用连接数+1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }
		//空闲连接数+1
        idleConnectionCount++;

        // 赋值最长的空闲时间和对应连接
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
	  //若最长的空闲时间大于5分钟 或 空闲数 大于5,就移除并关闭这个连接
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // else,就返回 还剩多久到达5分钟,然后wait这个时间再来清理
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        //连接没有空闲的,就5分钟后再尝试清理.
        return keepAliveDurationNs;
      } else {
        // 没有连接,不清理
        cleanupRunning = false;
        return -1;
      }
    }
	//关闭移除的连接
    closeQuietly(longestIdleConnection.socket());

    //关闭移除后 立刻 进行下一次的 尝试清理
    return 0;
  }

思路还是很清晰的:

  1. 在将连接加入连接池时就会启动定时任务。
  2. 有空闲连接的话,如果最长的空闲时间大于5分钟 或 空闲数 大于5,就移除关闭这个最长空闲连接;如果 空闲数 不大于5 且 最长的空闲时间不大于5分钟,就返回到5分钟的剩余时间,然后等待这个时间再来清理。
  3. 没有空闲连接就等5分钟后再尝试清理。
  4. 没有连接不清理。

2.6 OKHttp有哪些优点?

  1. 使用简单,在设计时使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。
  2. 扩展性强,可以通过自定义应用拦截器与网络拦截器,完成用户各种自定义的需求。
  3. 功能强大,支持Spdy、Http1.X、Http2、以及WebSocket等多种协议。
  4. 通过连接池复用底层TCP(Socket),减少请求延时。
  5. 无缝的支持GZIP减少数据流量。
  6. 支持数据缓存,减少重复的网络请求。
  7. 支持请求失败自动重试主机的其他ip,自动重定向。

2.7 OKHttp框架中用到了哪些设计模式?

  • 构建者模式:OkHttpClient与Request的构建都用到了构建者模式。
  • 外观模式: OkHttp使用了外观模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。
  • 责任链模式: OKHttp的核心就是责任链模式,通过5个默认拦截器构成的责任链完成请求的配置。
  • 享元模式: 享元模式的核心即池中复用,OKHttp复用TCP连接时用到了连接池,同时在异步请求中也用到了线程池。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值