最近工作比较忙,文章更新出现了延时。虽说写技术博客最初主要是写给自己,但随着文章越写越多,现在更多的是写给关注我技术文章的小伙伴们。最近一段时间没有更新文章,虽有工作生活孩子占用了大部分时间的原因,但也有自身的懒惰,这里向小伙伴们也向自己说一声抱歉…

OkHttp 是这几年比较流行的 Http 客户端实现方案,其支持HTTP/2、支持同一Host 连接池复用、支持Http缓存、支持自动重定向 等等,有太多的优点。
一直想找时间了解一下 OkHttp 的实现原理 和 具体源码实现,不过还是推荐在使用 和 了解其原理之前,先通读一遍 OkHttp 的官方文档,由于官方文档为英文,我在通读的时候,顺便翻译了一下,如翻译有误,请帮忙指正

OkHttp官方API地址:
https://square.github.io/okhttp/

一、概述

Http是现在流行的应用程序请求方法。Http帮助我们交换数据和多媒体内容。有效地执行HTTP可以使您的内容加载更快,并节省带宽。

OkHttp 是一个执行效率比较高的Http客户端:

  • 支持HTTP/2 ,当多个请求对应同一host地址时,可共用同一个socket;
  • 连接池可减少请求延迟(如果HTTP/2不可用);
  • 支持GZIP压缩,减少网络传输的数据大小;
  • 支持Response数据缓存,避免重复网络请求;

当网络出现问题时,OkHttp会不断重试:
OkHttp将从常见的连接问题中静默恢复您的网络请求;如果您的服务具有多个IP地址,则在第一次连接失败时,OkHttp将尝试使用备用地址,这对于IPv4 + IPv6、减少服务器的数据驻留是必需的;OkHttp支持TLS功能(TLS 1.3, ALPN, certificate pinning),OkHttp可将其配置回退以获得广泛的连接性。

使用OkHttp很容易,OkHttp的请求/响应API使用builder方式构建,OkHttp支持同步阻塞调用、异步回调调用。

1.1、Get a URL

Get方法请求一个url地址,并将response结果打印出来:完整的Http Get请求举例

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

1.2、Post to a Server

向服务器发起Post请求;完整的Http Post请求举例

public static final MediaType JSON
    = MediaType.get("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(json, JSON);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

1.3、Requirements

OkHttp运行环境为 Android 5.0+ (API level 21+) 、Java 8+
OkHttp 3.12.x分支运行环境为Android 2.3+(API level 9+)、Java 7+

OkHttp依赖高性能I/O库Okio,依赖Kotlin library使用Kotlin开发语言;这两个依赖库很小,并有很强的向后兼容;

我们强烈建议你保持使用OkHttp最新版本。与自动更新的Web浏览器一样,保持HTTPS客户端的最新状态是防范潜在安全问题的重要防御措施。我们跟踪有危险的TLS生态系统并调整OkHttp以改善连接性和安全性。

OkHttp当前使用平台的内置TLS实现。 在Java平台上,OkHttp还支持Conscrypt,它将BoringSSL与Java集成在一起。如果Conscrypt是最安全的SSL提供程序,OkHttp将使用Conscrypt。

OkHttp 3.12.x分支运行环境为Android 2.3+(API level 9+)、Java 7+。OkHttp 3.12.x不支持TLS 1.2,因此不推荐使用。因为升级困难,我们将在2021年12月31日之前,向3.12.x分支添加向后兼容的补丁程序。

Security.insertProviderAt(Conscrypt.newProvider(), 1);

1.4、Releases

release history可参考 https://square.github.io/okhttp/changelog/

最新坂本已上传 Maven Central。

implementation("com.squareup.okhttp3:okhttp:4.8.0")

OkHttp支持R8压缩、混淆规则: https://square.github.io/okhttp/r8_proguard/

在一个使用了OkHttp依赖包的Android工程中,如果你使用了默认的R8压缩算法,你不用为引入Okhttp而多做任何事情。特定的规则已经集成到OkHttp提供的JAR包中,这些规则支持R8自动解析。

但是,如果你使用的不是R8,则必须应用以下混淆规则;你可能还需要添加Okio相关混淆规则,因为OkHttp使用了Okio依赖库。

# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**

# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase

# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
-dontwarn org.codehaus.mojo.animal_sniffer.*

# OkHttp platform used only on JVM and when Conscrypt dependency is available.
-dontwarn okhttp3.internal.platform.ConscryptPlatform

1.5、MockWebServer

OkHttp includes a library for testing HTTP, HTTPS, and HTTP/2 clients.

OkHttp包含一个测试HTTP、HTTPS、HTTP/2的客户端工程。

最新测试工程已上传 Maven Central

testImplementation("com.squareup.okhttp3:mockwebserver:4.8.0")

1.6、 软件开源许可协议

详细了解 Apache License 2.0许可协议,可参考我的文章:https://blog.csdn.net/xiaxl/article/details/106137088

Copyright 2019 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

二、Calls

HTTP客户端用于接收Http request请求,并响应Http response数据。理论原理并不复杂,但在是实现上确实比较难。

2.1、Requests

每一个 HTTP 请求包含一个请求地址URL请求方法(如GET或POST)请求Headers,很多请求可能包含请求 Body (特定内容类型的数据流);

2.2、Responses

响应数据包含 响应状态码(如200表示成功或404表示未找到)、响应Headers可选的响应Body

2.3、Rewriting Requests

当你使用OkHttp发起HTTP请求时,你的思想处于这样一个高度 “使用这个URL地址 和这些请求Headers获取数据”。为了保证效率和正确性,OkHttp在传输请求之前会先对其进行重写。

OkHttp可以添加原始请求中缺少的Header,包括Content-Length、Transfer-Encoding、User-Agent、Host、Connection、Content-Type;如Accept-Encoding不存在,OkHttp会添加Header用于压缩传输的Accept-Encoding;如果你获取过cookies,OkHttp将自动将cookies添加到header中。

一些请求支持response数据缓存。当缓存的response数据过期时,OkHttp在一定条件下可以发起一个GET请求,以获取新的response数据。这类请求的headers,例如If-Modified-SinceIf-None-Match会被添加。

2.4、Rewriting Responses

如果数据传输中使用压缩算法,OkHttp将删除相应的响应Header 如Content-Encoding、Content-Length,因为它们不适用于解压缩的response body。

如果conditional GET 成功,则会按照规范将 network与cache 的响应数据进行合并。

2.5、Follow-up Requests

如果你请求的 URL 已被重定向,在发生网络请求时,webserver将会返回一个 302 响应码,此302响应码用来标识重定向后新的 URL 请求地址。OkHttp将会自动重定向,并获取最终的response数据。

如果请求的响应信息提示要求进行 authorization 授权挑战,OkHttp将会安全的完成授权挑战(如果此时挑战信息已配置);如果认证器支持证书,OkHttp将使用内置证书进行重试;

2.6、Retrying Requests

有时连接失败:
无论是本地连接池原因,还是网络原因造成webserver不可达,当网络条件可用时,OkHttp将会重试

2.7、Calls

通过 rewrites、redirects、follow-ups、retries,你的请求可能会产生许多中间请求和响应数据。OkHttp使用API Call来建立Request请求,为了保证请求的安全性,许多中间请求和响应是必不可少的。通常,中间请求不多!但是,值得高兴的是,如果你的URLs被重定向,或你的服务器出现故障,该请求将会被重试。

Calls 会以以下两种方式执行:

  • 同步执行:执行线程会被阻塞,直到response数据返回;
  • 异步执行:你可以将请求放在任何线程上,响应的回调数据将在另一个线程中获取。

Calls 可以在任何线程中被取消掉,这将倒是请求失败,如果请求尚未完成。当取消请求时,写request body或读response body位置处将抛出IOException异常。

2.8、Dispatch

对于同步调用,您需要自己创建执行线程,并负责控制您发出的请求数量, 同时连接过多会浪费资源, 太少会造成延迟;

对于异步调用,Dispatcher默认请求策略为: 每个webserver服务器默认最大请求数量默认为5,整体的最大请求数量为64,并且这两个值用户可自行定义。

三、Caching

OkHttp网络缓存默认是关闭的,用户可以选择开启。OkHttp实现网络缓存功能依赖的是RFC标准,存在模糊定义的情况下,以当前比较流行的浏览器软件Firefox/Chrome为准。

3.1、Basic Usage

  private val client: OkHttpClient = OkHttpClient.Builder()
      .cache(Cache(
          directory = File(application.cacheDir, "http_cache"),
          // $0.05 worth of phone storage in 2020
          maxSize = 50L * 1024L * 1024L // 10 MiB
      ))
      .build()

3.2、EventListener events

缓存的回调事件API为EventListener,典型场景如下:

3.2.1、Cache Hit

在理想情况下,缓存数据可以完全满足对应的request请求,而无需发起任何网络请求。应用Http网络缓存数据后,将跳过常规网络请求事件,如DNS解析、连接到网络以及下载response数据。

根据HTTP RFC的建议,基于“Last-Modified”,文档的最长过期时间默认为文档计划服务时间的10%。默认过期日期不应用于查询的URI。

  • CallStart
  • CacheHit
  • CallEnd

3.2.2、Cache Miss

缓存未命中时,可以看到正常的网络请求,但回调事件显示缓存存在。根据响应headers,如果数据未从网络中获取、不可缓存、缓存过期,则缓存未命中很常见。

  • CallStart
  • CacheMiss
  • ProxySelectStart
  • … Standard Events …
  • CallEnd

3.2.3、Conditional Cache Hit

当需要检查缓存结果仍然有效时,跟随在cachehitmiss后,会收到一个cacheConditionalHit事件。然后是缓存命中或未命中。 至关重要的是,在缓存命中的情况下,服务器不会发送响应正文。重要的是,在缓存命中的情况下,不会 server 不会发送response body 数据。

The response will have non-null cacheResponse and networkResponse. The cacheResponse will be used as the top level response only if the response code is HTTP/1.1 304 Not Modified.

在 HTTP/1.1 服务器返回的Response 为 304 Not Modified情况下,请求的response将返回非空的 cacheResponsenetworkResponse。cacheResponse 的优先级最高。

  • CallStart
  • CacheConditionalHit
  • ConnectionAcquired
  • … Standard Events…
  • ResponseBodyEnd (0 bytes)
  • CacheHit
  • ConnectionReleased
  • CallEnd

3.2.4、Cache directory

缓存目录必须有且仅有一个单例类持有。

可以在不再需要缓存时删除它,但这可能会删除App重启之前保持不变的缓存。(Deleting the cache when it is no longer needed can be done. However this may delete the purpose of the cache which is designed to persist between app restarts.)

cache.delete()

3.2.5、Pruning the Cache

可以使用 evictAll 删除整个缓存

cache.evictAll()

可以使用 url 迭代方式,删除某个单独的Item。典型的应用场景是,用户通过 下拉刷新(pull to refresh)强制启动一个刷新动作。

val urlIterator = cache.urls()
while (urlIterator.hasNext()) {
  if (urlIterator.next().startsWith("https://www.google.com/")) {
    urlIterator.remove()
  }
}

3.2.6、Troubleshooting

1、有效的,可缓存的 responses 数据,未被缓存(Valid cacheable responses are not being cached)

确保完全读取 responses 响应数据,除非完全读取响应数据 或 请求被取消。

3.2.7、Overriding normal cache behaviour

See Cache documentation. https://square.github.io/okhttp/4.x/okhttp/okhttp3/-cache/

四、Connections

尽管你只提供了 URL ,但 OkHttp 规划与对应服务器(webserver)的网络连接(connection)时,使用以下三种类型:URL、Address、and Route.

4.1、URLs

URL( 如 https://github.com/square/okhttp )是 HTTP 和 Internet 的基础。除了针对网络上所有内容的通用,还规定了如何访问 web 资源。

URLs are abstract:

  • 指定请求(call)可以是明文 ( http ) 或加密 ( https ),但不指定应该使用哪种加密算法。也没有指定如何验证对等方的证书(HostnameVerifier)或哪些证书可以信任(SSLSocketFactory);
  • 没有指定是否应使用特定的代理服务器或如何向该代理服务器进行身份验证;

每个 URL 标识一个特定的路径( 如 /square/okhttp )和 查询(如 ?q=sharks&lang= en),每个 webserver 管理许多URL。

4.2、Addresses

Addresses 指定一个 webserver (如 github.com) 和 连接到该服务器所需的所有静态配置:端口号、HTTPS设置、首选网络协议(如 HTTP/2、SPDY)。

多个 URL 共享同一个 address,也可能共享相同的 TCP socket 连接;共享连接具有显着的性能优势:更低的延迟、更高的吞吐量(由于 TCP 连接建立缓慢)、节省电量。 OkHttp使用一个 ConnectionPool,它可以自动重用 HTTP / 1.x 连接并多路复用 HTTP/2、SPDY连接。

在OkHttp中,地址的某些字段来自URL(scheme、hostname、port),其余部分来自OkHttpClient

4.3、Routes

Routes 提供实际连接到 webserver 所需的动态信息。特定IP地址(由 DNS 查询获取)、要使用的确切代理服务器(如果正在使用 ProxySelector )、要协商的TLS版本(用于HTTPS连接)。

一个 address 可能有很多 routes。 例如,一个 webserver 可以托管在多个数据中心中,DNS查询时可以产生多个IP地址。

4.4、Connections

当你使用 OkHttp 向某个 URL 发起一个 Request 网络请求时,OkHttp做了以下几件事:

  • OkHttp 使用URL,并配置 OkHttpClient 来创建一个 address。 此 address 指定了我们如何连接到 webserver ;
  • OkHttp 尝试从连接池中检索该 address 的连接;
  • 如果在池中找不到对应的连接,则选择一个路由进行尝试。 这通常意味着,创建一个DNS请求,以获取服务器的IP地址; 然后根据需要选择TLS版本和代理服务器;
  • 如果是新建一个路由,则可以通过建立 socket 连接、TLS隧道(基于HTTP代理的HTTPS),直接通过 TLS 建立隧道进行连接。如果有必要会进行TLS握手;
  • 发送Http请求 和 读取 Response 数据;

如果连接出现问题,OkHttp 将选择其他路由进行重试, 这样当一部分服务器无法访问时,OkHttp可以恢复使用; 当共用连接失效 或 TLS版本不受支持时,此功能也很有用。

一旦请求的 response 数据返回到客户端,这个connection 将被释放返回到连接池中,以保证该 这个connection 可以被其他请求复用。闲置一段时间后,连接将从池中退出。

五、Events

Events 可以让你获取应用程序运行中HTTP的状态,使用 Events 来监听状态变化:

  • 应用程序发出的HTTP请求的的数量和频率。 如果您发起了太多的Http请求,或者您的请求的内容太大,那么您应该知道这些回调数据!
  • 网络请求的性能。 如果网络的性能不足,则需要改善网络或减少使用。

5.1、EventListener

Subclass EventListener and override methods for the events you are interested in. In a successful HTTP call with no redirects or retries the sequence of events is described by this flow.

重写接口 EventListener 中,你感兴趣的的方法。在一次成功的Http请求获取中,没有重定向或重试的前提下,事件的调用顺序如下图所示:

执行顺序

以下为 EventListener 使用举例:

class PrintingEventListener extends EventListener {
  private long callStartNanos;

  private void printEvent(String name) {
    long nowNanos = System.nanoTime();
    if (name.equals("callStart")) {
      callStartNanos = nowNanos;
    }
    long elapsedNanos = nowNanos - callStartNanos;
    System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  @Override public void dnsStart(Call call, String domainName) {
    printEvent("dnsStart");
  }

  @Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
    printEvent("dnsEnd");
  }

  ...
}

创建两个Http请求:

Request request = new Request.Builder()
    .url("https://publicobject.com/helloworld.txt")
    .build();

System.out.println("REQUEST 1 (new connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

System.out.println("REQUEST 2 (pooled connection)");
try (Response response = client.newCall(request).execute()) {
  // Consume and discard the response body.
  response.body().source().readByteString();
}

运行以上两个请求时,PrintingEventListener打印的调用日志如下:

REQUEST 1 (new connection)
0.000 callStart
0.010 dnsStart
0.017 dnsEnd
0.025 connectStart
0.117 secureConnectStart
0.586 secureConnectEnd
0.586 connectEnd
0.587 connectionAcquired
0.588 requestHeadersStart
0.590 requestHeadersEnd
0.591 responseHeadersStart
0.675 responseHeadersEnd
0.676 responseBodyStart
0.679 responseBodyEnd
0.679 connectionReleased
0.680 callEnd
REQUEST 2 (pooled connection)
0.000 callStart
0.001 connectionAcquired
0.001 requestHeadersStart
0.001 requestHeadersEnd
0.002 responseHeadersStart
0.082 responseHeadersEnd
0.082 responseBodyStart
0.082 responseBodyEnd
0.083 connectionReleased
0.083 callEnd

注意:为什么第二次呼叫没有触发connect事件? 它重用了第一个请求的连接,从而显着提高了性能。

5.2、EventListener.Factory

In the preceding example we used a field, callStartNanos, to track the elapsed time of each event. This is handy, but it won’t work if multiple calls are executing concurrently. To accommodate this, use a Factory to create a new EventListener instance for each Call. This allows each listener to keep call-specific state.

在前面的示例中,我们使用一个名为 callStartNanos 的字段来跟踪每个事件方法的执行时间。 这很方便,但是如果同时执行多个Http请求,它将不起作用。 为此,请使用 Factory 为每个 Call 请求创建一个新的 EventListener ,这会使每个 listener 保持良好的监听状态。

factory 为每一个Call 请求创建一个单独的ID,使用该ID区分log消息中不同的请求。

class PrintingEventListener extends EventListener {
  public static final Factory FACTORY = new Factory() {
    final AtomicLong nextCallId = new AtomicLong(1L);

    @Override public EventListener create(Call call) {
      long callId = nextCallId.getAndIncrement();
      System.out.printf("%04d %s%n", callId, call.request().url());
      return new PrintingEventListener(callId, System.nanoTime());
    }
  };

  final long callId;
  final long callStartNanos;

  public PrintingEventListener(long callId, long callStartNanos) {
    this.callId = callId;
    this.callStartNanos = callStartNanos;
  }

  private void printEvent(String name) {
    long elapsedNanos = System.nanoTime() - callStartNanos;
    System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  ...
}

我们可以以上PrintingEventListener 来监听一对并发的HTTP请求:

Request washingtonPostRequest = new Request.Builder()
    .url("https://www.washingtonpost.com/")
    .build();
client.newCall(washingtonPostRequest).enqueue(new Callback() {
  ...
});

Request newYorkTimesRequest = new Request.Builder()
    .url("https://www.nytimes.com/")
    .build();
client.newCall(newYorkTimesRequest).enqueue(new Callback() {
  ...
});

网络环境为家庭Wifi,执行效率上 00020001更早执行完成:

0001 https://www.washingtonpost.com/
0001 0.000 callStart
0002 https://www.nytimes.com/
0002 0.000 callStart
0002 0.010 dnsStart
0001 0.013 dnsStart
0001 0.022 dnsEnd
0002 0.019 dnsEnd
0001 0.028 connectStart
0002 0.025 connectStart
0002 0.072 secureConnectStart
0001 0.075 secureConnectStart
0001 0.386 secureConnectEnd
0002 0.390 secureConnectEnd
0002 0.400 connectEnd
0001 0.403 connectEnd
0002 0.401 connectionAcquired
0001 0.404 connectionAcquired
0001 0.406 requestHeadersStart
0002 0.403 requestHeadersStart
0001 0.414 requestHeadersEnd
0002 0.411 requestHeadersEnd
0002 0.412 responseHeadersStart
0001 0.415 responseHeadersStart
0002 0.474 responseHeadersEnd
0002 0.475 responseBodyStart
0001 0.554 responseHeadersEnd
0001 0.555 responseBodyStart
0002 0.554 responseBodyEnd
0002 0.554 connectionReleased
0002 0.554 callEnd
0001 0.624 responseBodyEnd
0001 0.624 connectionReleased
0001 0.624 callEnd

EventListener.Factory 还可以限行部分调用,以下随机了10%:

class MetricsEventListener extends EventListener {
  private static final Factory FACTORY = new Factory() {
    @Override public EventListener create(Call call) {
      if (Math.random() < 0.10) {
        return new MetricsEventListener(call);
      } else {
        return EventListener.NONE;
      }
    }
  };

  ...
}

5.3、Events with Failures

当请求失败时,请求失败的回调方法将会被调用。当与server建立连接失败时,调用 connectFailed();当HTTP 请求失败时,调用 callFailed()。当发生请求失败时,可能存在Start事件,但无End事件。

Events with Failures

5.4、Events with Retries and Follow-Ups

网络请求中,当发生网络连接错误时,OkHttp将自动重试。当这种场景出现时,connectFailed()callFailed()事件后,事件回调不会终止。网络请求重试时,将会收到很多其他重试事件。

单个HTTP请求,后续可能存在对个网络请求,比如 authentication鉴权挑战、重定向、HTTP网络层连接超时。这种场景下,会存在多个网络连接、requests、responses。因此后续,同一个类型的事件,可能存在很多的事件回调。

Events with Retries and Follow-Ups

5.5、Availability

Events is available as a public API in OkHttp 3.11. Future releases may introduce new event types; you will need to override the corresponding methods to handle them.

Events 相关API 在OkHttp 3.11为共有API,后续版本中可能会增加新的回调事件。使用Events事件时,你需要重写其中相应的方法。

wx_gzh.jpg

版权声明:本文为xiaxveliang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/xiaxveliang/p/13406804.html