【Android】OkHttp内部工作原理

前言:本文的写作主要目的有两个,一方面是为了加强对于 OkHttp 的理解,巩固对已学知识,另一方面是为了在往后遗忘时便于回顾。由于笔者的学习资源来源于网络博客,所以可能会有一些内容跟其他文章相似,或者引用了其他文章的内容(文中已注明)。首先放两个推荐阅读的文章:

简介

特点

  • 支持 Http2/SPDY 协议
  • 连接池复用,减少延迟
  • 缓存响应信息减少重复请求
  • 支持 GZIP 压缩

使用

  1. 创建 OkHttpClient 对象。OkHttpClient 大部分时候都只需创建一个,以便可以最大限度地资源复用(每个实例中都有连接池、线程池、缓存等)。
  2. 创建 Request 对象。Request 代表请求报文,可以通过它设置请求Url、请求方法、请求体等。
  3. 创建 Call 对象,通过 Call 发起网络请求。可通过 Call 对象发起同步或异步请求。
  4. 处理响应结果。
OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
    .url(url)
    .build();
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        Log.v(TAG, "**onFailure**" + e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        Log.v(TAG, "**onResponse**" + response.body().string());
    }
});

上面的代码进行了一个简单的 GET 请求,下文将描述这一请求的内部工作流程。

工作过程

创建 OkHttpClient

OkHttpClient 类使用了建造者模式,可以通过其内部类 Builder 对其成员变量进行配置。OkHttpClient 只对外提供了一个无参的构造方法,但内部拥有另外一个以 default 修饰的构造方法,当使用new OkHttpClient 时通过内部类 Builder 为其提供了默认值,下面是 OkHttpClient 类的一部分成员变量:

  //OkHttpClient.java
  final Dispatcher dispatcher;
  final List<Protocol> protocols;
  final List<Interceptor> interceptors;
  final List<Interceptor> networkInterceptors;
  final CookieJar cookieJar;
  final @Nullable Cache cache;
  final SocketFactory socketFactory;
  final ConnectionPool connectionPool;
  final Dns dns;
  final int connectTimeout;
  final int readTimeout;
  final int writeTimeout;
  ...

创建 Request

Request 类同样使用了建造者模式,可通过其内部类 Builder 对请求体进行配置。

public static class Builder {
    HttpUrl url;
    String method;
    Headers.Builder headers;
    RequestBody body;
    Object tag;
    ...
    public Builder url(String url) {
      if (url == null) throw new NullPointerException("url == null");

      // Silently replace web socket URLs with HTTP URLs.
      if (url.regionMatches(true, 0, "ws:", 0, 3)) {
        url = "http:" + url.substring(3);
      } else if (url.regionMatches(true, 0, "wss:", 0, 4)) {
        url = "https:" + url.substring(4);
      }

      HttpUrl parsed = HttpUrl.parse(url);
      if (parsed == null) throw new IllegalArgumentException("unexpected url: " + url);
      return url(parsed);
    }
    ...
    public Request build() {
      if (url == null) throw new IllegalStateException("url == null");
      return new Request(this);
    }
    ...
}

网络请求

OkHttp 支持同步和异步两种请求,总的来说,同步会直接执行,而异步最后会使用线程池来执行,但不管是同步还是异步,最后都会通过 getResponseWithInterceptorChain() 方法来执行,下图是同步请求和异步请求执行到getResponseWithInterceptorChain() 的区别,后文只描述了异步执行的过程。

OkHttp 同步和异步

OkHttpClient 对象生成 Call 对象时,最终内部调用的是 RealCall,总体来说 client.newCall(request).equeue(callback) 分为两步:

  1. OkHttpClient 接收 Request 对象,生成 RealCall 对象。RealCall 实现了 Call 接口,在同步中它是被执行的对象,而在异步中 AsyncCall 时被执行的对象,AsncCall 间接实现了 Runnable 接口,后者是前者的内部类。
  2. RealCall 把实现了回调接口 Callback 的对象加入到队列当中。Callback 接口定义了两个方法 onFailure()onResponse() ,在请求成功或失败时会被调用。
//AsyncCall.java
@Override protected void execute() {
    boolean signalledCallback = false;
    try {
        Response response = getResponseWithInterceptorChain(); //这里是真正进行网络请求的入口
        if (retryAndFollowUpInterceptor.isCanceled()) {
            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!
            Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
            eventListener.callFailed(RealCall.this, e);
            responseCallback.onFailure(RealCall.this, e);
        }
    } finally {
        client.dispatcher().finished(this); //这里最终会调用 promoteCall(),promoteCall 将从就绪队列中取出新的 AsyncCall 交给线程池执行。
    }
 }
}

下面的代码是 AsyncCall#execute() 代码,大体的逻辑是:首先使用getResponseWithInterceptorChain() 进行网络请求,根据响应结果进行回调,最后取出新的 AsyncCall 执行。

进入 RealCall#getResponseWithInterceptorChain()

//RealCall.java
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
        interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
                                                       originalRequest, this, eventListener, client.connectTimeoutMillis(),
                                                       client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
}

OkHttp 的请求使用了责任链模式,把缓存、建立连接、网络请求等不同的功能分配给不同的 Interceptor,最后把所有的 Interceptor 连成一条链,按顺序去执行,当最后一个 Interceptor 执行完毕之后逐级地返回响应结果,所有的 Interceptor 功能如下(来源:OkHttp 实现原理):

  • 用户自定义的 Interceptor 通过client.interceptors()拿到整个自定义列表。上面提到的 HttpLoggingInterceptor 就是在这个列表中。
  • RetryAndFollowUpInterceptor 主要作用就是处理失败之后重试。比如处理未授权、PROXY 授权等等。OkHttpClient.Builder 中的 proxyAuthenticator 还有 authenticator 等都会在这里被调用。Chain 里面的 StreamAllocation 在这里开始实例化,前面都是 null。
  • BridgeInterceptor 字面意思是桥梁连接应用和网络。主要会完善(添加)请求的 Header、处理 cookie、自动解压 Gzip 等等。
  • CacheInterceptor 主要作用是缓存 Response。官方推荐在 OkHttpClient.Builder 中使用okhttp3.Cache
  • ConnectInterceptor 主要是生成网络连接。调用 StreamAllocation.newStream,分配一个复用的 Connection。然后以 HttpStream 和 RealConnection 的形式交给下一个 Interceptor (即 CallServerInterceptor )。其中在 HttpStream 中来确定使用 HTTP1x 还是 HTTP/2 ( HTTP/2 and SPDY )协议。
  • CallServerInterceptor 请求服务器。与服务器进行交互。获取数据并且封装起来返回给 ConnectInterceptor。然后逐级分发回去。最后 getResponseWithInterceptorChain() 接受数据,返回给用户。

进入 RealInterceptorChain#proceed()

//RealInterceptorChain.java
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
    RealConnection connection) throws IOException {
  if (index >= interceptors.size()) throw new AssertionError();

  calls++;

  // If we already have a stream, confirm that the incoming request will use it.
  if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
    throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
        + " must retain the same host and port");
  }

  // If we already have a stream, confirm that this is the only call to chain.proceed().
  if (this.httpCodec != null && calls > 1) {
    throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
        + " must call proceed() exactly once");
  }

  // Call the next interceptor in the chain.
  RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
      connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
      writeTimeout); //index 是列表的索引,从 0 开始
  Interceptor interceptor = interceptors.get(index); //获得具体的 Interceptor
  Response response = interceptor.intercept(next); //调用具体的 Interceptor 的方法

  // Confirm that the next interceptor made its required call to chain.proceed().
  if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
    throw new IllegalStateException("network interceptor " + interceptor
        + " must call proceed() exactly once");
  }

  // Confirm that the intercepted response isn't null.
  if (response == null) {
    throw new NullPointerException("interceptor " + interceptor + " returned null");
  }

  if (response.body() == null) {
    throw new IllegalStateException(
        "interceptor " + interceptor + " returned a response with no body");
  }

  return response;
}

这里从0开始取出拦截器列表中具体的 Interceptor,调用其 intercepte() 方法, 当获得响应结果之后逐级地返回。

OkHttp 拥有的 Interceptor 如上文所述,下文将只对 ConnectionInterceptor 和 CallServcerInterceptor 进行说明。

ConnectInterceptor

ConnectInterceptor 的工作是建立网络连接、复用可用连接等,下面是 ConnectInterceptor 提供的唯一的非构造方法,这个方法也将被责任链中上一个 Interceptor 调用。

//ConnectInterceptor.java
@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

ConnectInterceptor#intercept()基本上通过 StreamAllocation 来建立、复用网络连接等功能,下面是StreamAllocation 的介绍(来源 深入理解OkHttp3(3):Connections):

StreamAllocation 类似中介者模式,协调 Connections、Stream 和 Call 三者之间的关系。每个 Call 在 Application 层RetryAndFollowUpInterceptor实例化一个StreamAllocation

相同 Address(相同的Host与端口)可以共用相同的连接 RealConnection。

  1. StreamAllocation 通过 Address,从连接池 ConnectionPools 中取出有效的 RealConnection,与远程服务器建立 Socket 连接。
  2. 在处理响应结束后或出现网络异常时,释放 Socket 连接。
  3. 每个 RealConnection 都持有对 StreamAllocation 的弱引用,用于连接闲置状态的判断。

进入StreamAllocation#newStream

//StreamAllocation.java
public HttpCodec newStream(
    OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
        RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
                                                                writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks); //找到可复用的 RealConnection
        HttpCodec resultCodec = resultConnection.newCodec(client, chain, this); 

        synchronized (connectionPool) {
            codec = resultCodec;
            return resultCodec;
        }
    } catch (IOException e) {
        throw new RouteException(e);
    }
}

newStream() 首先通过findHealthyConnection()找到健全可用的连接,再根据协议生成 HttpCodec,HttpCodec 是什么?(来源:拆轮子系列:拆 OkHttp

它是对 HTTP 协议操作的抽象,有两个实现:Http1CodecHttp2Codec,顾名思义,它们分别对应 HTTP/1.1 和 HTTP/2 版本的实现。

Http1Codec 中,它利用 OkioSocket 的读写操作进行封装,Okio 以后有机会再进行分析,现在让我们对它们保持一个简单地认识:它对 java.iojava.nio 进行了封装,让我们更便捷高效的进行 IO 操作。

进入StreamAllocation#findHealthyConnection() ,这里不贴代码,概括性地说,该方法主要做了两件事:

  • 调用StreamAllocation#findConnection(),寻找可复用的连接(也有可能是新创建的)
  • 通过RealConnection#isHealthy()判断该连接是否健全可用

首先看第一点,下面是findConnection()部分主要的代码:

  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
      ...
      synchronized (connectionPool) {
          if (released) throw new IllegalStateException("released");
          if (codec != null) throw new IllegalStateException("codec != null");
          if (canceled) throw new IOException("Canceled");
          ...
          if (this.connection != null) {
            // We had an already-allocated connection and it's good.
            result = this.connection;
            releasedConnection = null;
          }
          ...
          if (result == null) {
            // 从连接池中获取 connection
            Internal.instance.get(connectionPool, address, this, null);
            if (connection != null) {
              foundPooledConnection = true;
              result = connection;
            } else {
              selectedRoute = route;
            }
          }
          ...
          if (result != null) {
             // If we found an already-allocated or pooled connection, we're done.
              return result;
      }
       ...
      synchronized (connectionPool) {
          if (canceled) throw new IOException("Canceled");

          if (newRouteSelection) {
            // Now that we have a set of IP addresses, make another attempt at getting a         connection from the pool. This could match due to connection coalescing.
            List<Route> routes = routeSelection.getAll();
            for (int i = 0, size = routes.size(); i < size; i++) {
              Route route = routes.get(i);
              Internal.instance.get(connectionPool, address, this, route);
              if (connection != null) {
                foundPooledConnection = true;
                result = connection;
                this.route = route;
                break;
              }
            }
          }

          if (!foundPooledConnection) {
            if (selectedRoute == null) {
              selectedRoute = routeSelection.next();
            }

            // Create a connection and assign it to this allocation immediately. This makes it possible
            // for an asynchronous cancel() to interrupt the handshake we're about to do.
            route = selectedRoute;
            refusedStreamCount = 0;
              //创建新的连接
            result = new RealConnection(connectionPool, selectedRoute);
            acquire(result, false);
         }
    }
    ...
}

方法首先尝试从连接池中获取已存在的、具有相同地址的、请求数量未超上限的 RealConnection 返回,如果没有,将创建一个新的连接对象。

再看 RealConnection#isHeathy()

public boolean isHealthy(boolean doExtensiveChecks) {
    if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
        return false;
    }

    if (http2Connection != null) {
        return !http2Connection.isShutdown();
    }

    if (doExtensiveChecks) {
        try {
            int readTimeout = socket.getSoTimeout();
            try {
                socket.setSoTimeout(1);
                if (source.exhausted()) {
                    return false; // Stream is exhausted; socket is closed.
                }
                return true;
            } finally {
                socket.setSoTimeout(readTimeout);
            }
        } catch (SocketTimeoutException ignored) {
            // Read timed out; socket is good.
        } catch (IOException e) {
            return false; // Couldn't read; socket is closed.
        }
    }

总结得出,以下情况的 RealConnection 会被认为是不健康的:

  • Socket 关闭
  • Socket 输入/输出流关闭
  • Http2Connection 处于 shutdown 状态
  • post 请求下输入流 Source 处于 exhuasted 状态
CallServerInterceptor

CallServerInterceptor 是责任链的最后一个 Interceptor,它的职责是发起网络请求。

CallServerInterceptor#intercept()

  @Override public Response intercept(Chain chain) throws IOException {
    ...
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      ...
      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        realChain.eventListener().requestBodyStart(realChain.call());
        long contentLength = request.body().contentLength();
        CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); //使用 Okio

        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
        realChain.eventListener()
            .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
      } else if (!connection.isMultiplexed()) {
        // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
        // from being reused. Otherwise we're still obligated to transmit the request body to
        // leave the connection in a consistent state.
        streamAllocation.noNewStreams();
      }
    }

    httpCodec.finishRequest();

    if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
    ...
    return response;
  }

代码的大概逻辑是利用 HttpCodec 对象写入请求的 Header、Body,然后接收响应的结果进行处理返回。这里引用其他文章的解读(来源:拆轮子系列:拆 OkHttp):

核心工作都由 HttpCodec 对象完成,而 HttpCodec 实际上利用的是 Okio,而 Okio 实际上还是用的 Socket,所以没什么神秘的,只不过一层套一层,层数有点多。

参考或拓展