您的位置:首页 > 理论基础 > 计算机网络

Okhttp解析—Okhttp概览

2020-01-06 19:25 1176 查看

Okhttp解析—Okhttp概览

Okhttp作为目前Android使用最为广泛的网络框架之一,我们有必要去深入了解一下,本文是Okhttp解析的第一篇,主要是从宏观上认识Okhttp整个架构是如何实现的。

一、什么是Okhttp

HTTP是当今应用程序通过网络交换数据和媒体的方式。 有效地使用 HTTP 可以使应用加载得更快并节省带宽。
Okhttp是一个高效的HTTP Client,高效性体现在:

  • Http / 2支持允许对同一主机的所有请求共享一个套接字
  • 连接池减少了请求延迟
  • 透明 GZIP 缩小了下载大小
  • 对于重复请求,响应缓存可以完全避免网络请求

当网络出现问题时,OkHttp 不会立即结束: 它会默默地从常见的连接问题中恢复过来。 如果您的服务有多个 IP 地址,如果第一次连接失败,OkHttp 将尝试替代地址。 这对于 IPv4 + IPv6和承载于冗余数据中心的服务是必要的。 Okhttp 支持现代 TLS 特性(TLS 1.3、 ALPN、证书ping)。 它可以配置为回退到可用的连接。
并且Okhttp是易用的,其通过Builder模式设计请求 / 响应 API,支持同步阻塞调用和带回调的异步调用。

二、Okhttp的请求机制以及相关概念

首先我们来了解下HTTP client、request、response。
HTTP client的作用就是接受我们的request并返回response。
request通常包含一个 URL, 一个方法 (比如GET/POST), 以及一个headers列表还可能包含一个body(特定内容类型的数据流)。
response则通常用响应代码(比如200表示成功,404表示未找到)、headers和可选的body来回答request。

我们日常使用http都是按以下步骤:
1、创建httpClient
2、创建request
3、使用httpClient请求request然后获取respone

使用Okhttp也是如此,我们创建OkhttpClient然后把Reques交给它,最后拿到Respone,但是Okhttp在内部实际进行http请求时并不是这样简单的拿Request去请求然后获得Resopne返回。

下面就来看下Okhttp的请求机制,可以概括为以下流程:

  1. 当我们创建OkhttpClient然后把Reques交给它之后,Okhttp为了提高正确性和效率在传输请求之前会重写请求。
  2. 然后Okhttp会尝试连接webserver,我们知道request中是带有URL的但是Okhttp在连接webserver时不仅仅使用URL它还会用Address和Route。连接webserver成功后获取respone,连接webserver失败Okhttp会进行重试操作。
  3. 在把respone返回给client之前Okhttp一般还会重写respone以及缓存respone。还有就是如果请求过程中产生重定向Okhttp也会进行处理并返回最终的respone。
    上边就是Okhttp的请求以及返回的大致流程

Rewriting Requests

Okhttp 可以添加原始请求中缺少的headers,包括Content-Length,Transfer-Encoding,User-Agent ,Host ,Connection , 和Content-Type。 除非Accept-Encoding头已经存在,否则它将添加一个用于透明响应压缩的 Accept-Encoding 头。 如果你有 cookies,OkHttp 会添加一个 Cookie 头。

有些请求会有一个缓存response。 当这个缓存过期,OkHttp 可以执行一个有条件的 GET 来下载新的response,这需要添加如 If-Modified-Since 和 If-None-Match 这样的headers。

Connections

Okhttp连接webserver时使用了URL、Address和Route。

URL

Url是 HTTP 和互联网的基础,每个 URL 标识一个特定的路径。它是一个通用的,分散的网络命名方案,它指定了如何访问网络资源、指定调用是纯文本(http) 或加密(https)方式。它们没有指定是否应该使用特定的代理服务器或者如何通过该代理服务器的身份验证

Address

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

具有相同Address的 url 也可能有相同的底层 TCP 套接字连接。 共享一个连接有很大的性能优势比如更低的延迟,更高的吞吐量(由于 TCP 缓慢启动)和节省电池。 Okhttp 使用一个 ConnectionPool 自动重用 http / 1.x 连接以及多路传输 http / 2和 SPDY 连接。
在 OkHttp 中,Address的一些字段来自 URL (scheme, hostname, port) ,其余字段来自 OkHttpClient。

Route

Route提供实际连接到网络服务器所需的动态信息。 它会尝试的特定 IP 地址(由 DNS 查询发现)、使用的准确代理服务器(如果使用 ProxySelector)以及协商的 TLS 版本(用于 HTTPS 连接)。
一个地址可能有多条Route。 例如,承载于多个数据中心的 web 服务器在其 DNS 响应中可能会产生多个 IP 地址。

当你使用 OkHttp 请求一个 URL 时,它是这样做的:

  1. 它使用 URL 并配置 OkHttpClient 来创建一个address。 这个address指定了我们如何连接到网络服务器
  2. 它试图从连接池(connection pool)中检索具有该地址的连接
  3. 如果它没有在连接池中找到连接,它会选择一条route进行尝试。 这通常意味着发出 DNS 请求来获取服务器的 IP 地址。 然后,如果有需要,它会选择一个 TLS 版本和代理服务器
  4. 如果是一个新的route,则通过构建直接的套接字连接、 TLS 隧道(通过 HTTP 代理使用 HTTPS)或直接的 TLS 连接进行连接。 必要时,它会进行 TLS 握手
  5. 发送 HTTP 请求并读取respone

如果连接有问题,OkHttp 会选择另一条route,再试一次。 这让 OkHttp 在服务器地址的一个子集无法访问时从错误中恢复。 当池连接过时或者不支持当前使用的 TLS 版本时,它也很有用。
一旦接收到respone,连接将返回到池中,以便可以在将来的请求中重用它。 连接在一段时间的不活动会被从连接池中清除。

Rewriting Response

如果使用透明压缩,OkHttp 将删除相应的 Content-Encoding 和 Content-Length,因为它们不适用于解压缩的响应体。
如果条件 GET请求 成功,来自网络和缓存的respone将按照规范的指示进行合并。

Follow-up Requests

当你请求的 URL 被重定向,webserver 将返回一个响应代码,比如302来指示新 URL。Okhttp将会重定向检索最终的respone。
如果respone发出了一个授权验证,OkHttp 将要求 Authenticator (如果配置了一个)满足这个验证。 如果身份验证者提供了凭据,那么request会携带该凭据去重试。

Retrying Requests

有时候连接会失败(比如池连接过时并断开连接或无法连接到网络服务器本身)如果此时有一个可用的Route,OkHttp 会用该Route重试请求。

Okhttp在实现流程的时候还引入了一些概念比如Call、Interceptors、ConnectionSpec、Events等等。

Call

经过重写、重定向、重试等操作,简单请求可能会产生许多请求和响应。 Okhttp 使用 Call 来封装表示request,Call就是一个已经准备好可以执行的请求。如果 url 被重定向,或者故障转移到另一个 IP 地址,那么代码将继续工作,直至返回最终respone。

Call有两种调用方式:
同步:线程阻塞直到响应可读为止
异步: 可以在任何线程上对请求进行排队,当响应可读时在另一个线程上被调回
可以从任何线程取消Call调用, 这将导致调用失败。在写入请求body或读取响应body时调用cancel会抛出IOException。

dispatcher

Dispatcher调度器,它实际就是负责Okhttp请求策略。
对于同步调用,调用请求的线程自己负责管理同时发出的请求数量。但是要注意的是太多的并发请求会浪费资源; 太少也不好。
对于异步调用,Dispatcher 实现最大并发请求的策略。 它设置了每个 web 服务器的最大值并发请求数为5,总并发数为64。当然我们也可以自行设置并发数。

Interceptors

拦截器可以说是Okhttp的精髓之一,它是一种强大的机制,它可以监测、重写和重试Call调用。系统提供了5种已经定义好的拦截器,上面说的request/respone 重写,失败重试等都是在拦截器中完成的。
对 chain.proceed (request)的调用是每个拦截器实现的关键部分。 这个简单的外观方法是所有拦截器完成其功能的地方,并且还生成满足请求的响应。 注意如果 chain.proceed (request)被调用多次,则必须关闭以前的响应body。
实际使用过程是通过拦截器链把拦截器链起来然后按顺序调用拦截器。

Okhttp拦截器分为2类:Application Interceptors和Network Interceptors。

这两类拦截器本质上没有区别只是它们作用的时机不同。由上图我们可以看出Application Interceptors作用于Okhttp Core之前而Network Interceptors则作用于Okhttp Core之后。我的理解就是Application Interceptors调用是在请求发出之前,Network Interceptors则是在请求发出后与webserver连接的过程。
每种拦截器链都有其优点:
Application interceptors

  • 不必担心重定向和重试之类的中间响应
  • 总是调用一次,即使 HTTP 响应是从缓存中提供的
  • 关注应用程序的原始意图,而不关注OkHttp注入的headers
  • 允许短路和不调用Chain.proceed().
  • 允许重试并多次调用Chain.proceed().
    Network Interceptors
  • 能够操作中间respone,如重定向和重试
  • 不会为短路网络的缓存响应调用
  • 仅当数据将要通过网络传输时才会关注
  • 允许带有Connection请求
    最后补充两个示例:
    使用Interceptor重写请求
/** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
final class GzipRequestInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request originalRequest = chain.request();
if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
return chain.proceed(originalRequest);
}

Request compressedRequest = originalRequest.newBuilder()
.header("Content-Encoding", "gzip")
.method(originalRequest.method(), gzip(originalRequest.body()))
.build();
return chain.proceed(compressedRequest);
}

private RequestBody gzip(final RequestBody body) {
return new RequestBody() {
@Override public MediaType contentType() {
return body.contentType();
}

@Override public long contentLength() {
return -1; // We don't know the compressed length in advance!
}

@Override public void writeTo(BufferedSink sink) throws IOException {
BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
body.writeTo(gzipSink);
gzipSink.close();
}
};
}
}

使用Interceptor重写响应

/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.header("Cache-Control", "max-age=60")
.build();
}
};

ConnectionSpec

ConnectionSpec的引入是为了HTTPS,在协商与 HTTPS 服务器的连接时,OkHttp 需要知道要提供哪些 TLS 版本和密码套件。Okhttp为了与尽可能多的主机连接的同时保证连接的安全性引入ConnectionSpec,它实现了特定的安全性和连接性决策,Okhttp 包括四个内置的连接规范RESTRICTED_TLS、MODERN_TLS、COMPATIBLE_TLS、CLEARTEXT。
默认情况下,OkHttp 将尝试建立一个 MODERN_TLS 连接。 但是,通过配置client的 connectionSpecs,如果 MODERN_TLS失败,回退到 COMPATIBLE_TLS 连接。

OkHttpClient client = new OkHttpClient.Builder()
.connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.build();

Events

Events的引入是为了监控call请求,因为我们有必要了解我们应用的HTTP请求。具体来说就是监控以下内容:
应用程序 HTTP 请求调用的频率和请求大小。
基础网络的性能监控,如果网络差你应该减少请求或者改善网络。

EventListener

Okhttp提供了EventListener,我们可以继承它并重写我们感兴趣的方法来进行Event的监控。

上图是在没有重试和重定向的情况下EventListener所能监控的Events流。下面是对应的代码

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");
}

...
}

Eventlistener. Factory

上面Eventlistener只适用于没有并发的情况,如果有多个请求并发执行我们需要使用Eventlistener. Factory来给每个请求创建一个Eventlistener。

下面是一个给每个请求创建一个带唯一ID的Eventlistener的示例:

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");
}

...
}

上面说的都是请求正常时event,接下来说下非正常情况下的event流程。

Events with Failures

当请求失败时,将调用一个失败方法 connectFailed () ,用于在建立到服务器的连接时发生故障,当 HTTP 请求永久失败时调用 callFailed ()。 当发生故障时,有可能开始事件没有相应的结束事件。

Events with Retries and Follow-Ups

Okhttp是健壮的,可以从一些连接故障中自动恢复。 在这种情况下,connectFailed ()事件不是终结符,后面不跟 callFailed ()。 当尝试重试时,事件侦听器将收到多个相同类型的事件。
单个 HTTP 调用可能需要发出后续请求来处理身份验证、重定向和 HTTP 层超时。 在这种情况下,可以尝试多个连接、请求和响应。 重定向是单个请求可能触发同一类型多个事件的另一个原因。

以上就是本文全部内容,接下来就是源码分析了,不过建议在看源码前把Okhttp的整个运行过程以及其中涉及的概念搞懂这样看源码才会事半功倍。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: