Okhttp源码学习四(连接拦截器的内部实现)

Okhttp的5个内置拦截器可以说是Okhttp的核心,因为整个请求的过程都被封装在这5个拦截器里面。而5个拦截器里面的核心就是这篇要分析的ConnectInterceptor,因为ConnectInterceptor才是真正发起请求,建立连接地方

ConnectInterceptor

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

 @Override 
  public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    //从拦截器链里面拿到流分配管理类StreamAllocation对象
    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类的代码也就这么几行,但从这几行中可以看出,真正创建流和建立连接的逻辑其实都在StreamAllocation里面. StreamAllocation对象是从RealInterceptorChain获取的. 通过之前的几篇对Okhttp的源码的学习,我们知道拦截器的拦截方法intercept(Chain chain)中的chain最开始是在RealCallgetResponseWithInterceptorChain()中初始化的:

 Response getResponseWithInterceptorChain() throws IOException {
........
//第二个参数就是StreamAllocation 类型,但是传的是null
  Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
      originalRequest, this, eventListener, client.connectTimeoutMillis(),
      client.readTimeoutMillis(), client.writeTimeoutMillis());

   return chain.proceed(originalRequest);
 }

可以看到第一个拦截器链创建的时候,StreamAllocation 传的是null,那么StreamAllocation 是什么时候赋值的呢,其实是在第一个拦截器RetryAndFollowUpInterceptor的拦截方法里面赋值的:

 @Override
 public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;
    .......

    response = realChain.proceed(request, streamAllocation, null, null);
    ........
 } 

第一个拦截器给StreamAllocation赋值后,后面的拦截器中的拦截器链的StreamAllocation就都是这个值. 前面说StreamAllocation很重要,具体的建立连接过程,还有创建流都是在这个类里面,下面就看看StreamAllocation

StreamAllocation

先看一下StreamAllocation的成员和构造函数

public final class StreamAllocation {
  public final Address address;      //请求的url地址
  private RouteSelector.Selection routeSelection;    //选择的路由
  private Route route;
  private final ConnectionPool connectionPool;    //连接池
  public final Call call;
  public final EventListener eventListener;
  private final Object callStackTrace;

  // State guarded by connectionPool.
  private final RouteSelector routeSelector;    //路由选择器
  private int refusedStreamCount;    //拒绝的次数
  private RealConnection connection;  //连接
  private boolean reportedAcquired;
  private boolean released;
  private boolean canceled;
  private HttpCodec codec;    //负责写入请求数据或读出响应数据的IO流

  public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
      EventListener eventListener, Object callStackTrace) {
    this.connectionPool = connectionPool;
    this.address = address;
    this.call = call;
    this.eventListener = eventListener;
    this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);
    this.callStackTrace = callStackTrace;
  }
  ....
}

StreamAllocation的成员和构造函数可以看到,StreamAllocation里面主要包含了连接池,连接,还有流. 为了更好理解这个类,先屡一下基本概念:

  1. HTTP请求网络的时候,首先需要通过一个Socket与服务端建立TCP连接,Socket中还需要有主机名host和端口号port
  2. 建立好连接后,就可以使用流在这个连接上向服务端写入数据和读取服务端返回的数据
  3. HTTP/1.1提出了Keep-Alive机制:当一个HTTP请求的数据传输结束后,TCP连接不立即释放,如果此时有新的HTTP请求,且其请求的Host同上次请求相同,则可以直接复用未释放的TCP连接,从而省去了TCP的释放和再次创建的开销,减少了网络延时
  4. HTTP2.0的多路复用:允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息

OkHttp为了解耦,对请求中的各个概念进行了封装, RealConnection就对应着请求中的连接,HttpCodec对应流,为了HTTP1.1的连接复用以及HTTP2.0的多路复用,就需要将请求连接保存下来,以便复用,所以就有了ConnectionPool. 而为了执行一次网络请求,需要从连接池找到可用的的连接,然后创建流,所以就需要一个分配管理流的角色,这个角色就是StreamAllocation

StreamAllocation.newStream()
 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);
    //通过找到的连接获取流
    HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

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

调用了findHealthyConnection()resultConnection.newCodec(),先看findHealthyConnection()

//StreamAllocation.findHealthyConnection()
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
  int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
  boolean doExtensiveHealthChecks) throws IOException {
  while (true) {
    //找到可用的连接
    RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
        pingIntervalMillis, connectionRetryEnabled);

    // If this is a brand new connection, we can skip the extensive health checks.
    synchronized (connectionPool) {
      //successCount为0,说明是新建立的连接,没有用过,默认可用,直接返回
      if (candidate.successCount == 0) {
        return candidate;
      }
    }

    // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
    // isn't, take it out of the pool and start again.
    //如果找到的连接不健康,就不让创建流,并关闭该连接,继续找
    if (!candidate.isHealthy(doExtensiveHealthChecks)) {
      noNewStreams();
      continue;
    }
    
    return candidate;
  }
}

又调用了findConnection去查找连接

//StreamAllocation.findConnection()
 private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
  int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  boolean foundPooledConnection = false;
  RealConnection result = null;
  Route selectedRoute = null;
  Connection releasedConnection;
  Socket toClose;
  synchronized (connectionPool) {
    if (released) throw new IllegalStateException("released");
    if (codec != null) throw new IllegalStateException("codec != null");
    if (canceled) throw new IOException("Canceled");

    // Attempt to use an already-allocated connection. We need to be careful here because our
    // already-allocated connection may have been restricted from creating new streams.
    releasedConnection = this.connection;
    toClose = releaseIfNoNewStreams();
    //如果当前StreamAllocation持有的连接不为空
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      //将这个持有的连接赋值给result
      result = this.connection;
      releasedConnection = null;
    }
    if (!reportedAcquired) {
      // If the connection was never reported acquired, don't report it as released!
      releasedConnection = null;
    }
    //当前StreamAllocation持有的连接为空,reuslt在这里就会为空,说明还没找到可用连接
    if (result == null) {
      // Attempt to get a connection from the pool.
      //从连接池中找一下,如果找到了会给持有的连接赋值
      Internal.instance.get(connectionPool, address, this, null);
      //如果从连接池中找到了可用的连接
      if (connection != null) {
        foundPooledConnection = true;
        //赋值给result
        result = connection;
      } else {
        //如果没找到,将StreamAllocation持有的路由赋值给已找到的路由
        selectedRoute = route;
      }
    }
  }
  closeQuietly(toClose);

  if (releasedConnection != null) {
    eventListener.connectionReleased(call, releasedConnection);
  }
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
  }
  if (result != null) {  //result不为空意味着当前连接可以用,或者从连接池中找到了可以复用的连接
    // If we found an already-allocated or pooled connection, we're done.
    return result;      //直接返回
  }

  // If we need a route selection, make one. This is a blocking operation.
  boolean newRouteSelection = false;
  //如果没找到路由,并且路由选择区为空
  if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
    newRouteSelection = true;
    //就切换路由
    routeSelection = routeSelector.next();
  }

  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);
      //关联到流管理引用列表connection.allocations,并用this.connection记录当前连接
      acquire(result, false);
    }
  }

  // If we found a pooled connection on the 2nd time around, we're done.
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
    return result;
  }

  // Do TCP + TLS handshakes. This is a blocking operation.
  //与服务端建立TCP连接
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);
  routeDatabase().connected(result.route());

  Socket socket = null;
  synchronized (connectionPool) {
    reportedAcquired = true;

    // Pool the connection.
    //将新建的连接存进连接池
    Internal.instance.put(connectionPool, result);

    // If another multiplexed connection to the same address was created concurrently, then
    // release this connection and acquire that one.
    if (result.isMultiplexed()) {
      socket = Internal.instance.deduplicate(connectionPool, address, this);
      result = connection;
    }
  }
  closeQuietly(socket);

  eventListener.connectionAcquired(call, result);
  return result;
 }

从源码可以看到,OkHttp寻找可用连接的过程如下:

1. 如果是重定向请求,就使用StreamAllocation持有的连接
    releasedConnection = this.connection;
    //如果当前连接不能创建新的流,就释放
    toClose = releaseIfNoNewStreams();
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      //将StreamAllocation持有的连接赋值给result,表示就用这个持有的连接
      result = this.connection;
      //不释放当前连接
      releasedConnection = null;
    }

StreamAllocation持有的连接this.connection一开始肯定是为null,但是当从连接池中找到了可用的连接后,或者从连接池没找到,新建一个连接后,StreamAllocation持有的连接this.connection就不为null. 不为null的时候就把它赋值给 result,表示就用这个持有的连接。

但我们知道在RetryAndFollowUpInterceptor的拦截方法里面,StreamAllocation是新建的。每调用一次RetryAndFollowUpInterceptor的拦截方法就会新建一个StreamAllocation,也就是说每一次请求都会新建一个StreamAllocation. 那么也就意味着每一次请求使用的连接根本不可能用到StreamAllocation持有的连接this.connection,因为StreamAllocation 是新建的,this.connection一直是null. 那么什么时候this.connection才会不为null呢?其实只有在第一次请求,服务端返回一个比如状态码为307这样的需要重定向的响应的时候,并且重定向的Request的host、port、scheme与之前一致时出现。在RetryAndFollowUpInterceptor中,如果响应为需要重定向,那么会再发起一次请求,第二次请求时,使用的StreamAllocation就是第一次创建的,这个时候就会用到这个StreamAllocation持有的连接(不太明白可以去看下Okhttp源码学习三(重试和重定向,桥接,缓存拦截器的内部原理))

2. 如果不是重定向请求,就遍历连接池中的所有连接,看是否有可复用的连接
  if (result == null) {    //result为null,意味着不是重定向请求
    // Attempt to get a connection from the pool.
    Internal.instance.get(connectionPool, address, this, null);
    if (connection != null) {
      foundPooledConnection = true;
      result = connection;
    } else {
      selectedRoute = route;
    }
  }

调用了Internal.instance.get(connectionPool, address, this, null)从连接池中去找

public abstract class Internal {

  public static void initializeInstanceForTests() {
   // Needed in tests to ensure that the instance is actually pointing to something.
    new OkHttpClient();
  }

  public static Internal instance;

  public abstract void addLenient(Headers.Builder builder, String line);
  .......
}

Internal是一个抽象类,它的唯一实现是在 OkHttpClient中:

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

  static {
    Internal.instance = new Internal() {
     ........
    @Override public RealConnection get(ConnectionPool pool, Address address,StreamAllocation streamAllocation, Route route) {
      //调用的是连接池ConnectionPool的get方法
      return pool.get(address, streamAllocation, route);
    }
    .........
  };
 ......
}

看一下ConnectionPool的get()方法:

 @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    //变量连接池中的所有连接,connections是Deque类型
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
   return null;
 }

调用了 connection.isEligible(address, route)来判断是否可以复用

//RealConnection.isEligible()
public boolean isEligible(Address address, @Nullable Route route) {
  //如果当前连接上的并发流数量超过最大值1,或当前连接不能创建新的流,返回false
  if (allocations.size() >= allocationLimit || noNewStreams) return false;
  //如果两个address除了host以外的所有域不相同,返回false
  if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
  //如果host也相同,那么当前连接可以复用,直接返回
  if (address.url().host().equals(this.route().address().url().host())) {
    return true; // This connection is a perfect match.
  }
  //http2连接为空,直接返回false
  if (http2Connection == null) return false;

  if (route == null) return false;
  //路由用到了代理,返回false
  if (route.proxy().type() != Proxy.Type.DIRECT) return false;
  if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
  //socket地址相同不同,返回false
  if (!this.route.socketAddress().equals(route.socketAddress())) return false;

  // 3. This connection's server certificate's must cover the new host.
  if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
  if (!supportsUrl(address.url())) return false;

  // 4. Certificate pinning must match the host.
  try {
    address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
  } catch (SSLPeerUnverifiedException e) {
    return false;
  }

  return true; // The caller's address can be carried by this connection.
}

判断是否可以复用的条件大致就是(后面对HTTP2的复用条件判断暂时没看明白):

当前连接的流的数量要少于1个,请求地址的host相同

如果连接池中有符合上面的条件的连接,就调用streamAllocation.acquire(connection, true);

//StreamAllocation.acquire()
public void acquire(RealConnection connection, boolean reportedAcquired) {
  assert (Thread.holdsLock(connectionPool));
  if (this.connection != null) throw new IllegalStateException();
  //给StreamAllocation持有的连接赋值
  this.connection = connection;
  this.reportedAcquired = reportedAcquired;
  //将连接connection与StreamAllocation绑定
  connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}

connection.allocations是一个列表:

public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

每一个连接对象RealConnection都有一个列表,列表的元素类型是StreamAllocation的弱引用,它用来记录当前连接上建立的流。因为每一次请求都会创建一个新的StreamAllocation

回到StreamAllocation.findConnection()中:

 if (result == null) {
    // Attempt to get a connection from the pool.
    Internal.instance.get(connectionPool, address, this, null);
    //如果连接池中有复用的连接,connection就不为null
    if (connection != null) {
      foundPooledConnection = true;
      //使用复用的连接
      result = connection;
    } else {
      selectedRoute = route;
    }
  }
}
closeQuietly(toClose);

if (releasedConnection != null) {
  eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
  eventListener.connectionAcquired(call, result);
}
if (result != null) {
  // If we found an already-allocated or pooled connection, we're done.
  return result;
}

如果从连接池找到了可以复用的连接,直接返回这个复用的连接

3. 如果在连接池没有找到可复用的连接,就切换路由,再从连接池中找一次
 if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
  newRouteSelection = true;
  //切换路由
  routeSelection = routeSelector.next();
}

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

关于RouteSelector以及路由的选择,切换,下篇再分析

4. 切换路由后,连接池中还是没有找到可以复用的连接,就新建一个连接,并将新建的connection和当前的StreamAllocation绑定
 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);
    //绑定connection和StreamAllocation
    acquire(result, false);
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容