OkHttpClient核心类StreamAllocation

StreamAllocation主要功能:

  1. 屏蔽协议细节处理不同Connection的复用功能,主要是协议流的可复用性和ConnectionPool的处理;
  2. 屏蔽协议(这些协议主要指HTTP1和HTTP2)细节给请求提供HttpCodec用于处理输入输出;
  3. StreamAllocation关联了一个Call请求的所有周期,StreamAllocation可以用来判断一个Call周期内Connection是否还在被占用,其它的Call能不能使用该Connection;
  4. StreamAllocation、Connection和ConnectionPool一起还提供了简单的StreamAllocation泄露检测功能。

StreamAllocation生命周期

StreamAllocation的创建

每次调用RealCall#execute时会新建一组interceptor用于处理请求,其中有一个是用于处理重试和重定向的拦截器RetryAndFollowUpInterceptor,RetryAndFollowUpInterceptor是在Recall的构造函数中创建。

// RetryAndFollowUpInterceptor
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.forWebSocket = forWebSocket;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}

RetryAndFollowUpInterceptor#intercept会新建一个StreamAllocation对象,可以看到一个Realcall#execute(在OkHttp中一个Realcall只能执行一次)会新建一个StreamAllocation对象。有两个地方会存这个对象的引用:

  1. 以成员变量streamAllocation保存在RetryAndFollowUpInterceptor中;
  2. 请求的执行过程中在ConnectInterceptor#intercept这个方法中会调用streamAllocation#newStream,这个方法新建或者在ConnectionPool中找到一个可用的Connection,然后将streamAllocation以弱引用的形式保存在 Connection#allocations中。

StreamAllocation的释放

请求相应完毕,用户读取数据,就会释放StreamAllocation,主要是解除StreamAllocation和其对应的Connection的关系,真正解除StreamAllocation与其对应Connection关系的地方StreamAllocation#release(RealConnection)这个私有方法中。主要功能是将StreamAllocation对应的弱引用从Connection#allocations移除。

// StreamAllocation#release
private void release(RealConnection connection) {
    for (int i = 0, size = connection.allocations.size(); i < size; i++) {
      Reference<StreamAllocation> reference = connection.allocations.get(i);
      if (reference.get() == this) {
        connection.allocations.remove(i);
        return;
      }
    }
    throw new IllegalStateException();
}

为什么移除了就能解除关系呢?
StreamAllocation从ConnectionPool中获取连接池中的Connection的第一个条件就是allocations.size() >= allocationLimit而对于Http1来说一个Connection#allocationLimit = 1,只有allocations.size() = 0,该连接才符合其它请求使用的第一个条件。所以,当请求结束时将Reference<StreamAllocation>从allocations中移除之后表示该请求能被复用了。

// RealConnection#isEligible
public boolean isEligible(Address address, @Nullable Route route) {
    // If this connection is not accepting new streams, we're done.
    if (allocations.size() >= allocationLimit || noNewStreams) return false;
}

什么情况下会调用StreamAllocation#release(RealConnection)这个私有方法呢?
StreamAllocation#release(RealConnection)只在StreamAllocation#deallocate这个方法中被调用。

// StreamAllocation#deallocate
private Socket deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {
    assert (Thread.holdsLock(connectionPool));

    if (streamFinished) {
      this.codec = null;
    }
    if (released) {
      this.released = true;
    }
    Socket socket = null;
    if (connection != null) {
      if (noNewStreams) {
        connection.noNewStreams = true;
      }
      if (this.codec == null && (this.released || connection.noNewStreams)) {
        release(connection);
        if (connection.allocations.isEmpty()) {
          connection.idleAtNanos = System.nanoTime();
          if (Internal.instance.connectionBecameIdle(connectionPool, connection)) {
            socket = connection.socket();
          }
        }
        connection = null;
      }
    }
    return socket;
}
  • 第一个参数streamFinished表示当前的请求的响应流数据读取完毕或者被关闭;
  • 第二个参数released表示当前的请求处理完毕或出现异常,可以释放AllocationStream和其对应的Connection;
  • 第三个参数noNewStreams表示当前的连接I/O出现异常,需要强制关闭连接底层的socket。
    需要同时满足以下两个条件才发触发StreamAllocation#release(RealConnection)
  1. 第一个参数streamFinished=true或者StreamAllocation#codec=null
  2. 第二个参数released=true或者第三个参数noNewStreams= true

streamFinished=true的情况:

  1. 对于http1正常关闭流或者流里面的数据读取完毕会调用StreamAllocation#streamFinished此时触发StreamAllocation#deallocate(false, false, true);
  2. 对于http1读取流里的数据出现异常会调用StreamAllocation#streamFinished此时触发StreamAllocation#deallocate(true, false, true);
  3. 对于http2会始终调用StreamAllocation#deallocate(false, false, true);
  4. RetryAndFollowUpInterceptor中会调用StreamAllocation#streamFailed最终会调用StreamAllocation#deallocate(noNewStreams, false, true) (异常可以先不考虑);
    上述几种情况传入的released的值均为false,是否强制关闭连接并解除StreamAllocation与connection的关系取决于noNewStreams的值。

StreamAllocation#codec=null的情况
有一种情况会出现这种了(有body,body中的数据被读取完毕也会出现下面的这种情况了吧)。。。
某个请求服务端的http响应没有body,此时构建ResponseBody的时候直接新建一个FixedLengthSource
且FixedLengthSource#bytesRemaining = 0,此时本来就没有数据可读,不需要用户调用response#close了,构造方法中会直接Http1Codec#AbstractSource#endOfInput(此时触发逻辑和streamFinished的逻辑一样)触发StreamAllocation#deallocate(false, false, true);很明显此时deallocate不会回收connection,仅仅
将StreamAllocation#codec标记为null。

// FixedLengthSource的构造函数,
FixedLengthSource(long length) throws IOException {
      bytesRemaining = length;
      if (bytesRemaining == 0) {
        endOfInput(true, null);
      }
}

这个null什么时候起作用呢? 调用StreamAllocation#release(这个release是个public且无参的方法)。StreamAllocation#release直接触发deallocate(false, true, false),即此时第二个变量released = true。streamAllocation#release经典用法之一是结合上面的StreamAllocation#codec = null,这种情况出现在RetryAndFollowUpInterceptor#intercept中,此次请求正常结束且服务端没有返回重定向此时结合上面的调用接口没有返回值即StreamAllocation#codec = null就可以正常回收连接。

RetryAndFollowUpInterceptor#intercept
public Response intercept(Chain chain) throws IOException {
      ...
      Request followUp = followUpRequest(response, streamAllocation.route());

      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }  
      ...
}

StreamAllocation#release其它调用的地方都是在RetryAndFollowUpInterceptor中出现的都是出现异常的一些逻辑StreamAllocation#release会触发StreamAllocation#deallocate(false, true, false);可以看到除了released = true之外,其它两个变量都为false,可以看做是一种尝试吧,此时只有配合StreamAllocation#codec = null才能真正释放StreamAllocation和其对应的Connection之间的关系了。

StreamAllocation#deallocate最后一种触发的逻辑是在StreamAllocation#releaseIfNoNewStreams,原理类似。一个场景是一个HTTP1请求出现服务端返回重定向,重复利用StreamAllocation的Connection,重新利用肯定要检查一下Connection的状态,如果不能建立新的流,强制回收该connection。触发逻辑是在下面的方法中:

StreamAllocation#findConnection
private RealConnection findConnection(){
      .....
      // 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.
      // 在哪种情况下会出现`this.connection != null`呢?用户的第一次请求发送个服务端并成功收
      // 到服务端的相应,但是发现是重定向需要重新发送请求,这时发送的请求就会复用RetryAndFollowUpInterceptor中
      // 已有的StreamAllocation。出现这种情况this.connection就不会为null
      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      ...
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357