前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OKHttp源码解析(八)--中阶之连接与请求前奏

OKHttp源码解析(八)--中阶之连接与请求前奏

作者头像
隔壁老李头
发布2018-08-30 11:44:29
1.6K0
发布2018-08-30 11:44:29
举报
文章被收录于专栏:Android 研究Android 研究

经过一番的学习,终于到连接与请求,最后一步了。本片的主要内容如下: 1、为什么要做app网络优化 2、ConnectionSpec与ConnectionSpecSelector简介 3、HttpCodec类及他的子类 4、AndroidPlatform类 5、Connection类

一、为什么要做app网络优化

1、keepalive

在http请求中,对于请求速度提升和降低延迟,keepalive在网络连接发挥着重大作用。

所有做过http请求的同学都知道http请求的三次握手和4次挥手,所以我们一般的http链接都是先tcp握手,然后传输数据,最后释放资源。流程如下图

链接.png

的确很简单,但是在复杂的网路内容中就不够用了,因为需要先进行socket的3次握手,和四次挥手,重复的连接和释放,就像“便秘”一样,十分难受!而且每次链接大概是TTL的一次的时间。特别现在IOS那边已经HTTPS了,安卓这边HTTPS也是趋势,在TLS环境下消耗的时间更多了。很明显在复杂网络时,延时(而不是带宽)将成为一个app非常重要的核心竞争因素,特别是在移动网络的使用场景下。

当然,其实这个问题老早就已经fix了,在http里面有一个叫做keepalive connections的机制,它可以在传输数据后仍然保持连接,当客户端需要再次获取数据的时候,直接使用刚刚空闲下来的链接而不需要再次握手。

复用链接.png

在PC的浏览器里面,一般会同时开启6-8个keepalive connections的socket链接,并保持一定的链路生命,当不需要时再关闭,而在服务器中,一般由软件根据负载的情况,决定是否关闭。

当然上帝给你打开一扇窗子的时候,也会关闭另外一扇窗户,凡事皆有利弊。keepalive也有缺点,在减轻客户端的延迟的同时,也妨碍了其它客户端的链路速度。比如如果存在大量空闲的keepalive connections(僵尸链接),其它客户端正常的链接速度就会受到影响,因为毕竟的的管道大小是确定。而且有一种恶意的攻击就是产生大量的僵尸链接,耗尽服务器的资源

二、ConnectionSpec与ConnectionSpecSelector简介

(一)、ConnectionSpec 在OkHttp中,ConnectionSpec用于描述传输HTTP流量的socket连接的配置。对于https请求,这些配置主要包括协商安全连接时要使用的TLS版本号和密码套间,是否支持TLS扩展等;对于http请求则几乎包含什么信息。 OkHttp有预定义几组ConnectionSpec ,如下:

代码语言:javascript
复制
  //ConnectionSpec.java
  /** A modern TLS connection with extensions like SNI and ALPN available. */
  public static final ConnectionSpec MODERN_TLS = new Builder(true)
      .cipherSuites(APPROVED_CIPHER_SUITES)
      .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
      .supportsTlsExtensions(true)
      .build();

  /** A backwards-compatible fallback connection for interop with obsolete servers. */
  public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS)
      .tlsVersions(TlsVersion.TLS_1_0)
      .supportsTlsExtensions(true)
      .build();

预定义的这些ConnectionSpec被组织定义为OkHttpClint默认的ConncetionSpec集合

代码语言:javascript
复制
//OkHttpClient.java
public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
  static final List<Protocol> DEFAULT_PROTOCOLS = Util.immutableList(
      Protocol.HTTP_2, Protocol.HTTP_1_1);

  static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
      ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
}

OkHttp中由OkHttpClient管理ConnectionSpec集合。OkHttp的用户可以构造OkHttpClient的过程中提供自己的ConnectionSpec集合。默认情况下OkHttpClient会使用前面的默认ConnectionSpec集合。

OKHttp还提供了ConnectionSpecSelector,用以从ConnectionSpec几个中选择与SSLSocket匹配的ConnectionSpec,并对SSLSocket做配置操作。

在RetryAndFollowUpInterceptor中创建Address时,ConnectionSpec集合被从OkHttpClient获取,并由Anddress引用。

在StreamAllocation的findConnection()中,ConnectionSpec集合被从Address中取出来,用于连接建立过程。

下面介绍一个后面会调用的方法,isCompatible()方法。在ConnectionSpec集合中选择一个与SSLSocket兼容的一个,如果有兼容的返回true,不兼容返回false。

代码语言:javascript
复制
  /**
   * Returns {@code true} if the socket, as currently configured, supports this connection spec. In
   * order for a socket to be compatible the enabled cipher suites and protocols must intersect.
   *
   * <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must
   * match the socket's enabled cipher suites. If there are no required cipher suites the socket
   * must have at least one cipher suite enabled.
   *
   * <p>For protocols, at least one of the {@link #tlsVersions() required protocols} must match the
   * socket's enabled protocols.
   */
  public boolean isCompatible(SSLSocket socket) {
    if (!tls) {
      return false;
    }

    if (tlsVersions != null && !nonEmptyIntersection(
        Util.NATURAL_ORDER, tlsVersions, socket.getEnabledProtocols())) {
      return false;
    }

    if (cipherSuites != null && !nonEmptyIntersection(
        CipherSuite.ORDER_BY_NAME, cipherSuites, socket.getEnabledCipherSuites())) {
      return false;
    }

    return true;
  }

isCompatible(SSLSocket) 里面调用Util.nonEmptyIntersection(Comparator, String[] , String[] )主要是对比两个String数组。必须每一个字符串都一样,才可以。即ConnectionSpec启动的TLS版本和密码套间与SSLSocket启动的有交集,如果有交集返回true,反之返回false。

再来看下,将选择的ConnectionSpec应用到SSLSocket上。

代码语言:javascript
复制
  /** Applies this spec to {@code sslSocket}. */
  void apply(SSLSocket sslSocket, boolean isFallback) {
    ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);

    if (specToApply.tlsVersions != null) {
      sslSocket.setEnabledProtocols(specToApply.tlsVersions);
    }
    if (specToApply.cipherSuites != null) {
      sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
    }
  }

  /**
   * Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
   * sslSocket}.
   */
  private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
    String[] cipherSuitesIntersection = cipherSuites != null
        ? intersect(CipherSuite.ORDER_BY_NAME, sslSocket.getEnabledCipherSuites(), cipherSuites)
        : sslSocket.getEnabledCipherSuites();
    String[] tlsVersionsIntersection = tlsVersions != null
        ? intersect(Util.NATURAL_ORDER, sslSocket.getEnabledProtocols(), tlsVersions)
        : sslSocket.getEnabledProtocols();

    // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
    // the SCSV cipher is added to signal that a protocol fallback has taken place.
    String[] supportedCipherSuites = sslSocket.getSupportedCipherSuites();
    int indexOfFallbackScsv = indexOf(
        CipherSuite.ORDER_BY_NAME, supportedCipherSuites, "TLS_FALLBACK_SCSV");
    if (isFallback && indexOfFallbackScsv != -1) {
      cipherSuitesIntersection = concat(
          cipherSuitesIntersection, supportedCipherSuites[indexOfFallbackScsv]);
    }

    return new Builder(this)
        .cipherSuites(cipherSuitesIntersection)
        .tlsVersions(tlsVersionsIntersection)
        .build();
  }

上面流程大体可以理解为:

  • 1、求得ConnectionSepc启动的TLS版本和密码套间与SSLSocket启动的TLS版本以及密码套间之间的交集,构造新的ConnectionSpec。
  • 2、重新为SSLSocket设置启用的TLS版本及密码套间为上一步求得的交集。

现在我们再来看下和ConnectionSpec相关的一个类 (二)、ConnectionSpecSelector 通过类的注释 我们可知该类是ConnectionSpec的选择类,当握手或者协议出现问题的时候,需要尝试不同的协议进行连接。 处理连接规范回退策略:当安全套接字连接由于握手/协议问题而失败时,可能会使用不同的协议重试连接。当创建单个连接速的时候会被创建该了的实例。

这里先说一下后面会用到的一个方法,configureSecureSocket()方法

代码语言:javascript
复制
 /**
   * Configures the supplied {@link SSLSocket} to connect to the specified host using an appropriate
   * {@link ConnectionSpec}. Returns the chosen {@link ConnectionSpec}, never {@code null}.
   *
   * @throws IOException if the socket does not support any of the TLS modes available
   */
  public ConnectionSpec configureSecureSocket(SSLSocket sslSocket) throws IOException {
    ConnectionSpec tlsConfiguration = null;
    for (int i = nextModeIndex, size = connectionSpecs.size(); i < size; i++) {
      ConnectionSpec connectionSpec = connectionSpecs.get(i);
      if (connectionSpec.isCompatible(sslSocket)) {
        tlsConfiguration = connectionSpec;
        nextModeIndex = i + 1;
        break;
      }
    }

    if (tlsConfiguration == null) {
      // This may be the first time a connection has been attempted and the socket does not support
      // any the required protocols, or it may be a retry (but this socket supports fewer
      // protocols than was suggested by a prior socket).
      throw new UnknownServiceException(
          "Unable to find acceptable protocols. isFallback=" + isFallback
              + ", modes=" + connectionSpecs
              + ", supported protocols=" + Arrays.toString(sslSocket.getEnabledProtocols()));
    }

    isFallbackPossible = isFallbackPossible(sslSocket);

    Internal.instance.apply(tlsConfiguration, sslSocket, isFallback);

    return tlsConfiguration;
  }

主要分为两个部分 1 从OkHttp配置的ConnectionSpec集合中选择一个SSLSocket兼容的一个。 2 将选择的ConnectionSpec应用到SSLSocket上。

三、HttpCodec类及他的子类

在okHttp中,HttpCodec是网络读写的管理类,也可以理解为解码器(注释上就是这样写的),它有对应的两个子类,Http1Codec和Http2Codec,分别对应HTTP/1.1以及HTTP/2.0协议 HttpCodec Http1Codec Http2Codec

(一)、咱们先来看下HttpCodec接口。
代码语言:javascript
复制
/** Encodes HTTP requests and decodes HTTP responses. */
public interface HttpCodec {
  /**
   * The timeout to use while discarding a stream of input data. Since this is used for connection
   * reuse, this timeout should be significantly less than the time it takes to establish a new
   * connection.
   */
  int DISCARD_STREAM_TIMEOUT_MILLIS = 100;

  /** Returns an output stream where the request body can be streamed. */
  Sink createRequestBody(Request request, long contentLength);

  /** This should update the HTTP engine's sentRequestMillis field. */
  void writeRequestHeaders(Request request) throws IOException;

  /** Flush the request to the underlying socket. */
  void flushRequest() throws IOException;

  /** Flush the request to the underlying socket and signal no more bytes will be transmitted. */
  void finishRequest() throws IOException;

  /**
   * Parses bytes of a response header from an HTTP transport.
   *
   * @param expectContinue true to return null if this is an intermediate response with a "100"
   *     response code. Otherwise this method never returns null.
   */
  Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;

  /** Returns a stream that reads the response body. */
  ResponseBody openResponseBody(Response response) throws IOException;

  /**
   * Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously.
   * That may happen later by the connection pool thread.
   */
  void cancel();
}

里面定义了7个抽象方法分别是:

  • writeRequestHeaders(Request request) :写入请求头
  • createRequestBody(Request request, long contentLength) :写入请求体
  • flushRequest() 相当于flush,把请求刷入底层socket
  • finishRequest() throws IOException : 相当于flush,把请求输入底层socket并不在发出请求
  • readResponseHeaders(boolean expectContinue) //读取响应头
  • openResponseBody(Response response) //读取响应体
  • void cancel() :取消请求

典型的面向接口变成,抽象了几个网络请求的几个行为,一般的网络请求包括特殊情况基本上就是下面的四个步骤

  • 第一步,写入请求头
  • 第二步,写入请求头
  • 第三步,读取响应头
  • 第四步,读取响应体

因为OkHttp是同时支持HTTP/2与HTTP/1.x的,为了让上层更方便的调用。所以在CallServerInterceptor类里面使用都是HttpCodec。由于HTTP/2与HTTP/1.x的原理不一样,那就由自己的子类去实现即可。所以有了Http1Codec与Http2Codec,

(二)、那我们来看下Http1Codec与Http2Codec
1、先来看下Http1Codec

在Http1Codec中主要两个重要属性即source和sink,它们分别封装了socket的输入和输出。而Http2Codec中主要属性是Http2Stream和Http2Connection。 Http1Codec提供I/O操作完成网络通信。 这里先简单的介绍下Http1Codec这里先看下Http1Codec的各种状态的定义:

代码语言:javascript
复制
  private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers.
  private static final int STATE_OPEN_REQUEST_BODY = 1;
  private static final int STATE_WRITING_REQUEST_BODY = 2;
  private static final int STATE_READ_RESPONSE_HEADERS = 3;
  private static final int STATE_OPEN_RESPONSE_BODY = 4;
  private static final int STATE_READING_RESPONSE_BODY = 5;
  private static final int STATE_CLOSED = 6;

上面定义了Http1Codec中使用的的状态模式,其实就是对象维护它所处的状态,在不同状态下执行对应的逻辑,并更新状态,在执行逻辑之前通过检查对象状态避免网络请求的若干执行步骤发生错乱。 再来看下:Http1Codec的属性

代码语言:javascript
复制
  /** The client that configures this stream. May be null for HTTPS proxy tunnels. */
  final OkHttpClient client;
  /** The stream allocation that owns this stream. May be null for HTTPS proxy tunnels. */
  final StreamAllocation streamAllocation;

  final BufferedSource source;
  final BufferedSink sink;
  int state = STATE_IDLE;

这些属性很容易理解,首先持有client,可以使用它所提供的功能,通常是获取一些用户所设置的属性,其次是streamAllocation,它是连接流的桥梁,所以很容易理解需要它获取关于连接的功能。然后就是该流对象封装的输出流和输入流,两个流内部封装的自然就是socket的了,最后就是对象所处的状态。介绍完属性之后,我们就可以再来分析下Http1Codec提供的能功能了,在这些步骤中,逻辑很很明确,首先检查状态,然后执行逻辑,最后更新状态。当然执行逻辑和更新状态时可以交换的,不会造成影响,这步骤分析中我们不再考虑状态的问题,重点是关系逻辑的执行。 来看下写请求头的方法

代码语言:javascript
复制
  /**
   * Prepares the HTTP headers and sends them to the server.
   *
   * <p>For streaming requests with a body, headers must be prepared <strong>before</strong> the
   * output stream has been written to. Otherwise the body would need to be buffered!
   *
   * <p>For non-streaming requests with a body, headers must be prepared <strong>after</strong> the
   * output stream has been written to and closed. This ensures that the {@code Content-Length}
   * header field receives the proper value.
   */
  @Override public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    writeRequest(request.headers(), requestLine);
  }

  /** Returns bytes of a request header for sending on an HTTP transport. */
  public void writeRequest(Headers headers, String requestLine) throws IOException {
    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
    sink.writeUtf8(requestLine).writeUtf8("\r\n");
    for (int i = 0, size = headers.size(); i < size; i++) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    sink.writeUtf8("\r\n");
    state = STATE_OPEN_REQUEST_BODY;
  }

本方法主要是写请求头 执行逻辑很清晰,可以分成两部分,对应Http协议,即写入请求行和请求头,至于RequestLine(请求行)里面的代码比较简单就是用StringBuilder进行拼接。

然后看下写入请求体

代码语言:javascript
复制
  @Override public Sink createRequestBody(Request request, long contentLength) {
    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
      // Stream a request body of unknown length.
      return newChunkedSink();
    }

    if (contentLength != -1) {
      // Stream a request body of a known length.
      return newFixedLengthSink(contentLength);
    }

    throw new IllegalStateException(
        "Cannot stream a request body without chunked encoding or a known content length!");
  }

熟悉HTTP协议的同学都知道其实请求和响应体可以分为固定长度和非固定长度的两种,其中非固定长度由头部信息中的Transfer-Encoding=chunked来表示,固定长度则由对应的头部信息表示实体信息的对应长度。

代码语言:javascript
复制
  public Sink newChunkedSink() {
    if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
    state = STATE_WRITING_REQUEST_BODY;
    return new ChunkedSink();
  }

/**
   * An HTTP body with alternating chunk sizes and chunk bodies. It is the caller's responsibility
   * to buffer chunks; typically by using a buffered sink with this sink.
   */
  private final class ChunkedSink implements Sink {
    private final ForwardingTimeout timeout = new ForwardingTimeout(sink.timeout());
    private boolean closed;

    ChunkedSink() {
    }

    @Override public Timeout timeout() {
      return timeout;
    }

    @Override public void write(Buffer source, long byteCount) throws IOException {
      if (closed) throw new IllegalStateException("closed");
      if (byteCount == 0) return;

      sink.writeHexadecimalUnsignedLong(byteCount);
      sink.writeUtf8("\r\n");
      sink.write(source, byteCount);
      sink.writeUtf8("\r\n");
    }

    @Override public synchronized void flush() throws IOException {
      if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
      sink.flush();
    }

    @Override public synchronized void close() throws IOException {
      if (closed) return;
      closed = true;
      sink.writeUtf8("0\r\n\r\n");
      detachTimeout(timeout);
      state = STATE_READ_RESPONSE_HEADERS;
    }
  }

这里使用一个内部类ChunkedSink来封装sink,这里我们只看到其中的三个重要方法,即write()、flush()、close()方法,逻辑很清晰,非固定长度的请求,都是在第一行写入一段数据的长度,然后在之后写入该段数据。从write()方法中可以看出将buffer中的数据写入到sink对象中,如果熟悉okio的执行逻辑,对此应该很容易理解。然后刷新和关闭逻辑很简单,其中关闭时注意更新状态。

对于固定长度的请求体,其封装的sink逻辑是类似的,其中需要传入一个bytesRemaining,保证写数据结束时保证数据长度是正确的。

代码语言:javascript
复制
  public Sink newFixedLengthSink(long contentLength) {
    if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
    state = STATE_WRITING_REQUEST_BODY;
    return new FixedLengthSink(contentLength);
  }
 /** An HTTP body with a fixed length known in advance. */
  private final class FixedLengthSink implements Sink {
    private final ForwardingTimeout timeout = new ForwardingTimeout(sink.timeout());
    private boolean closed;
    private long bytesRemaining;

    FixedLengthSink(long bytesRemaining) {
      this.bytesRemaining = bytesRemaining;
    }

    @Override public Timeout timeout() {
      return timeout;
    }

    @Override public void write(Buffer source, long byteCount) throws IOException {
      if (closed) throw new IllegalStateException("closed");
      checkOffsetAndCount(source.size(), 0, byteCount);
      if (byteCount > bytesRemaining) {
        throw new ProtocolException("expected " + bytesRemaining
            + " bytes but received " + byteCount);
      }
      sink.write(source, byteCount);
      bytesRemaining -= byteCount;
    }

    @Override public void flush() throws IOException {
      if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
      sink.flush();
    }

    @Override public void close() throws IOException {
      if (closed) return;
      closed = true;
      if (bytesRemaining > 0) throw new ProtocolException("unexpected end of stream");
      detachTimeout(timeout);
      state = STATE_READ_RESPONSE_HEADERS;
    }
  }

PS:在读取响应体的时候也会用到这个类。 这里可以看到有一个成员变量bytesRemaining标示剩余的字节数,保证读取到的字节长度与头部信息中的长度保证一致。read()中的代码可以看到就是将该source对象的数据读取到封装的source中,用于构建resposne。 PS:sink,source对象以及封装的sink和source对象,代表http1Codec中的sink和source对象。即封装了socket的输出和输入流,而封装的sink和source对象则是构建的固定长度和非固定长度的输出输入流,其实它们只是对http1Codec成员变量的·中的sink和source的一种封装,其实就是装饰者模式,封装以后的sink和source对象可以用在外部请求和构建ReponseBody。

当写完请求头和请求体之后,需要完成,这时候会调用finishReqeust()方法

代码语言:javascript
复制
  @Override public void finishRequest() throws IOException {
    sink.flush();
  }

其实这一步其实很简单,只有一行代码,就是执行流的刷新。 注意这一步是不需要检查状态的,因为此时的状态有可能是STATE_OPEN_REQUEST_BODY(没有请求体的情况)或者STATE_READ_RESPONSE_HEADERS(已经完成请求体写入的情况)。这一步只是刷新新流,所以什么情况都不会造成影响,所以没有必要检查状态,也没有更新状态,保持之前的状态即可。

再看下读取请求头的readResponseHeaders()方法

代码语言:javascript
复制
  @Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
      throw new IllegalStateException("state: " + state);
    }

    try {
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());

      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());

      if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
      }

      state = STATE_OPEN_RESPONSE_BODY;
      return responseBuilder;
    } catch (EOFException e) {
      // Provide more context if the server ends the stream before sending a response.
      IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
      exception.initCause(e);
      throw exception;
    }
  }

此时所处的状态有可能为STATE_OPEN_REQUEST_BODY和STATE_READ_RESPONSE_HEADERS两种,然后读取请求航和请求头部信息,并返回响应的Builder。

然后再看下读取请求头体的方法再看下读取响应体的方法

代码语言:javascript
复制
  @Override public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
  }

在之前介绍中,我们知道响应Response对象是封装一个source对象,用于读取响应数据。所以ResponseBody的构建就是需要响应头和响应体的两部分即可,响应头在上一部分中已经添加到response对象中了,headers()获取响应头即可。下面分析,如何封装source对象,获取一个对应的source对象,可能有些拗口,让我们来看下getTransferStream()方法的代码:

代码语言:javascript
复制
  private Source getTransferStream(Response response) throws IOException {
    if (!HttpHeaders.hasBody(response)) {
      return newFixedLengthSource(0);
    }

    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      return newChunkedSource(response.request().url());
    }

    long contentLength = HttpHeaders.contentLength(response);
    if (contentLength != -1) {
      return newFixedLengthSource(contentLength);
    }

    // Wrap the input stream from the connection (rather than just returning
    // "socketIn" directly here), so that we can control its use after the
    // reference escapes.
    return newUnknownLengthSource();
  }

这里和写入请求体的地方十分类似,响应体也是分为固定长度和非固定长度两种,除此以外,为了代码的健壮性okhttp还定义了UnknownLengthSource(位置长度Source,代表意外情况)。FixedLengthSource已经分析过了,这里就不详细描述了。

这里需要提及一下endOfInput()方法:

代码语言:javascript
复制
    /**
     * Closes the cache entry and makes the socket available for reuse. This should be invoked when
     * the end of the body has been reached.
     */
    protected final void endOfInput(boolean reuseConnection) throws IOException {
      if (state == STATE_CLOSED) return;
      if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);

      detachTimeout(timeout);

      state = STATE_CLOSED;
      if (streamAllocation != null) {
        streamAllocation.streamFinished(!reuseConnection, Http1Codec.this);
      }
    }
  }

这里的执行逻辑也简单,除去检查状态和更新状态之外,就是接触超时机制,最后需要注意就是调用streamAllocation的streamFinish()方法 这里的执行逻辑也不多,除去检查状态和更新状态之外,就是接触超时机制,最后需要注意就是调用streamAllocation的

这里我们简单介绍下Http2Codec。先看下他的属性

代码语言:javascript
复制
  //这里是header
  private static final ByteString CONNECTION = ByteString.encodeUtf8("connection");
  private static final ByteString HOST = ByteString.encodeUtf8("host");
  private static final ByteString KEEP_ALIVE = ByteString.encodeUtf8("keep-alive");
  private static final ByteString PROXY_CONNECTION = ByteString.encodeUtf8("proxy-connection");
  private static final ByteString TRANSFER_ENCODING = ByteString.encodeUtf8("transfer-encoding");
  private static final ByteString TE = ByteString.encodeUtf8("te");
  private static final ByteString ENCODING = ByteString.encodeUtf8("encoding");
  private static final ByteString UPGRADE = ByteString.encodeUtf8("upgrade");

  /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */
   //request里面的header组合
  private static final List<ByteString> HTTP_2_SKIPPED_REQUEST_HEADERS = Util.immutableList(
      CONNECTION,
      HOST,
      KEEP_ALIVE,
      PROXY_CONNECTION,
      TE,
      TRANSFER_ENCODING,
      ENCODING,
      UPGRADE,
      TARGET_METHOD,
      TARGET_PATH,
      TARGET_SCHEME,
      TARGET_AUTHORITY);
   //response 里面的的header组合
  private static final List<ByteString> HTTP_2_SKIPPED_RESPONSE_HEADERS = Util.immutableList(
      CONNECTION,
      HOST,
      KEEP_ALIVE,
      PROXY_CONNECTION,
      TE,
      TRANSFER_ENCODING,
      ENCODING,
      UPGRADE);

  private final OkHttpClient client;
  //负责连接、请求、流的分配
  final StreamAllocation streamAllocation;
  //HTTP/2.0的连接
  private final Http2Connection connection;
  //HTTP/2.0的流
  private Http2Stream stream;

这里说下关于HttpCodec 面向接口编程,在外层不关系具体的实现类,外层会调用对应的抽象方法来实现它的逻辑,而内在的实现类再根据情况去实现。 其中Http2Connection代表着HTTP/2.0的连接,Http2Stream代表着HTTP/2.0的流。 这里说下,由于HTTP/2 里面支持一个"连接"可以发送多个请求,所以和HTTP/1.x有着本质的区别,所以Http1Codec里面有source和sink,而Http2Codec没有,因为在HTTP/1.x里面一个连接对应一个请求。而HTTP2则不是,一个TCP连接上可以跑多个请求。所以OkHttp里面用一个Http2Connection代表一个连接。然后用Http2Stream代表一个请求的流。

四、AndroidPlatform

大家看代码的时候经常会遇到Platform.get()方法。这个方法返回的不是Plarform对象,返回的是AndroidPlatform对象 那么这里简单介绍下其对应的方法

代码语言:javascript
复制
  @Override public boolean isCleartextTrafficPermitted(String hostname) {
    try {
      Class<?> networkPolicyClass = Class.forName("android.security.NetworkSecurityPolicy");
      Method getInstanceMethod = networkPolicyClass.getMethod("getInstance");
      Object networkSecurityPolicy = getInstanceMethod.invoke(null);
      Method isCleartextTrafficPermittedMethod = networkPolicyClass
          .getMethod("isCleartextTrafficPermitted", String.class);
      return (boolean) isCleartextTrafficPermittedMethod.invoke(networkSecurityPolicy, hostname);
    } catch (ClassNotFoundException | NoSuchMethodException e) {
      return super.isCleartextTrafficPermitted(hostname);
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
      throw new AssertionError();
    }
  }

在RealConnection.connect()方法里面被调用 平台的安全策略:安卓平台本身的安全策略允许向相应的主机送法明文请求。对于Android平台而言,这种安全策略主要由系统组件android.security.NetworkSecurityPolicy执行,平台这种安全策略并不是每个Android版本都有,6.0之后才有有这种权限控制 平台本身的安全策略允许向相应的主机发送明文请求。对于Android平台而言,这种安全策略主要由系统的组件。

再来看下另外一个方法的调用

代码语言:javascript
复制
  @Override public void configureTlsExtensions(
      SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
    // Enable SNI and session tickets.
    if (hostname != null) {
      setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true);
      setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname);
    }

    // Enable ALPN.
    if (setAlpnProtocols != null && setAlpnProtocols.isSupported(sslSocket)) {
      Object[] parameters = {concatLengthPrefixed(protocols)};
      setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters);
    }
  }

在RealConnection里面调用connectTls()方法里面调用了 Platform.get().configureTlsExtensions()方法 这个方法主要利用了TLS的ALPN扩展来完成的。这里再来详细的看一下配置TLS扩展的过程。 TLS扩展相关的方法不是SSLSocket接口的标准方法,不同是SSL/ TLS实现对这些接口的支持程度不一样,因而这里荣国反射机制调用TLS扩展相关方法。 这里主要配置了3个TLS扩展,分别是session tickes、SNI和ALPN、session tickets用于会话恢复,SNI用于支持单个主机配置多个域名的情况。ALPN则用于HTTP/2的协议协商。可以到位SNI设置的hostname最终来源于Url,也就意味着使用HttpDns时,如果没有直接将IP地址替换成Url中的域名来发起HTTPS请求的话,SNI将是IP地址,这里可能使服务器下发不恰当的证书。

TLS扩展相关方法的OptionaMethod创建过程也在AndroidPlatform中:

代码语言:javascript
复制
 public AndroidPlatform(Class<?> sslParametersClass, OptionalMethod<Socket> setUseSessionTickets,
      OptionalMethod<Socket> setHostname, OptionalMethod<Socket> getAlpnSelectedProtocol,
      OptionalMethod<Socket> setAlpnProtocols) {
    this.sslParametersClass = sslParametersClass;
    this.setUseSessionTickets = setUseSessionTickets;
    this.setHostname = setHostname;
    this.getAlpnSelectedProtocol = getAlpnSelectedProtocol;
    this.setAlpnProtocols = setAlpnProtocols;
  }

public static Platform buildIfSupported() {
    // Attempt to find Android 2.3+ APIs.
    try {
      Class<?> sslParametersClass;
      try {
        sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
      } catch (ClassNotFoundException e) {
        // Older platform before being unbundled.
        sslParametersClass = Class.forName(
            "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
      }

      OptionalMethod<Socket> setUseSessionTickets = new OptionalMethod<>(
          null, "setUseSessionTickets", boolean.class);
      OptionalMethod<Socket> setHostname = new OptionalMethod<>(
          null, "setHostname", String.class);
      OptionalMethod<Socket> getAlpnSelectedProtocol = null;
      OptionalMethod<Socket> setAlpnProtocols = null;

      // Attempt to find Android 5.0+ APIs.
      try {
        Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
        getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
        setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
      } catch (ClassNotFoundException ignored) {
      }

      return new AndroidPlatform(sslParametersClass, setUseSessionTickets, setHostname,
          getAlpnSelectedProtocol, setAlpnProtocols);
    } catch (ClassNotFoundException ignored) {
      // This isn't an Android runtime.
    }

    return null;
  }

建立TLS连接的步骤中,获取协议的过程与配置TLS的过程类似,同样利用反射调用SSLSocket方法。 RealConnection.connectTls()调用Platform.get().getSelectedProtocol(sslSocket)

代码语言:javascript
复制
  @Override public String getSelectedProtocol(SSLSocket socket) {
    if (getAlpnSelectedProtocol == null) return null;
    if (!getAlpnSelectedProtocol.isSupported(socket)) return null;

    byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket);
    return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null;
  }

五、Connection

Connection是个接口,里面四个抽象方法

代码语言:javascript
复制
 Route route(); //返回一个路由
 Socket socket();  //返回一个socket
 Handshake handshake();  //如果是一个https,则返回一个TLS握手协议
Protocol protocol(); //返回一个协议类型 比如 http1.1 等或者自定义类型 

注意: 大家看下这个类的注释,上面说了,这个类和HttpURLConnection不是一个东西,Connection这个类不是一个单一的请求/响应交换的连接,可用于多个HTTP请求/响应交换 其中有一句比较重要

代码语言:javascript
复制
Each connection can carry a varying number streams, depending on the underlying protocol being used. HTTP/1.x connections can carry either zero or one streams. HTTP/2 connections can carry any number of streams, dynamically configured with {@code SETTINGS_MAX_CONCURRENT_STREAMS}. A connection currently carrying zero streams is an idle stream. We keep it alive because reusing an existing connection is typically faster than establishing a new one.

简单翻译一下就是: 每个连接可以携带不同数量的流,这取决于所使用的底层协议。 HTTP / 1.x连接可以携带零个或一个流。 HTTP / 2连接可以携带任意数量的流,使用{@code SETTINGS_MAX_CONCURRENT_STREAMS}动态配置。 目前携带零流的连接是空闲流。 我们保持活着,因为重用现有的连接通常比建立新的连接更快。

代码语言:javascript
复制
When a single logical call requires multiple streams due to redirects or authorization challenges, we prefer to use the same physical connection for all streams in the sequence. There are potential performance and behavior consequences to this preference. To support this feature, this class separates <i>allocations</i> from <i>streams</i>. An allocation is created by a call, used for one or more streams, and then released. An allocated connection won't be stolen by other calls while a redirect or authorization challenge is being handled.

简单翻译一下就是: 当一个请求被重定向或者证书验证的时候,需要多个流。为了拥有更好的性能,我们更愿意为序列中的所有流使用相同的物理连接。为了支持此功能,此类将”流“和"分配"分开。 分配由呼叫创建,用于一个或多个流,然后释放。 在处理重定向或授权挑战时,分配的连接不会被其他呼叫所窃取。

有人问,为什么要看这段注释,因为这段注释其实就是okhttp的复用连接池的精神,为后面复用连接池的时候做预热。

因为Connection是接口,他的具体实现类是RealConnection,其实大家可以发现OKHttp的代码风格是先写一个InterfaceA,然后具体的实现类是RealInterfaceA.

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.06.04 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么要做app网络优化
    • 1、keepalive
    • 二、ConnectionSpec与ConnectionSpecSelector简介
    • 三、HttpCodec类及他的子类
      • (一)、咱们先来看下HttpCodec接口。
        • (二)、那我们来看下Http1Codec与Http2Codec
        • 四、AndroidPlatform
        • 五、Connection类
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档