okhttp3源码解析(6)-ConnectionPool、StreamAllocation补充

OkHttp3源码详解之 okhttp连接池复用机制(一,2021吊打面试官系列 在timeout空闲时间内,连接不会关闭,相同重复的request将复用原先的connection,减少握手的次数,大幅提高效率。 并非keep-alive的timeout设置时间越长,就越能提升性能。长久不关闭会造成过多的僵尸连接和泄露连接出现。 那么okttp在客户端是如果类似于客户端做到的keep-alive的机制。 2、连接池的使用 ======== 连接池的类位于okhttp3.ConnectionPool。我们的主旨是了解到如何在timeout时间内复用connection,并且有效的对其进行回 阅读详情

okhttp3源码解析(6)-ConnectionPool、StreamAllocation补充

前言

上一篇文章把RealConnection和Http2Connection讲了讲,写得有点又臭又长了,这篇主要讲讲ConnectionPool,感觉会稍微简单点,顺带把Internal看看,并补一补StreamAllocation漏掉得一些东西。

ConnectionPool

ConnectionPool是在OkHttpClient.Builder里面用默认构造方法创建的,设置最大空闲连接数和保持连接的时间(5分钟)。

    public ConnectionPool() {
        this(5, 5, TimeUnit.MINUTES);
    }

    public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
        this.maxIdleConnections = maxIdleConnections;
        this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

        // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
        if (keepAliveDuration <= 0) {
            throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
        }
    }

下面来好好介绍下ConnectionPool的一些内容,代码从上往下看吧。

清理线程

ConnectionPool上来就是个线程池配Runnable,还是有点懵逼的,不过首先我们要知道它们的作用,即cleanup工作。

第一个线程池是直接创建的,我还记得有个阿里巴巴手册好像也是写的线程池要手动创建,要明白其中用的东西。

    // 配合下面的cleanupRunnable用来清楚超时的Connection,SynchronousQueue所以保证了只有一个线程嘛?
    private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
            Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

下面就是对应的runnable,maxIdleConnections和keepAliveDurationNs是构造传进来的。

    /** The maximum number of idle connections for each address. */
    private final int maxIdleConnections;
    private final long keepAliveDurationNs;
    // 用来清理connectionPool内的connection,清理一个只会会在延迟时间后清理下一个
    private final Runnable cleanupRunnable = new Runnable() {
        @Override public void run() {
            while (true) {
                // 得到延迟执行的时间
                long waitNanos = cleanup(System.nanoTime());
                // 对应没有connection的情况,会停止这个runnable,在put方法内会重新开始
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    // 分成秒和纳秒两部分,Java的wait方法需要,接收纳秒范围0-999999
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    // 连接池是全局单例?使用累锁进行卡线程
                    synchronized (ConnectionPool1.this) {
                        try {
                            ConnectionPool1.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

这里就是清除的代码,不过放在这里看得不是很明白,这里只是数据域,接下来我们开始追踪到底是在哪调用的,会找到put方法;

    // 通过Internal在StreamAllocation的findConnection中使用
    void put(RealConnection connection) {
        assert (Thread.holdsLock(this));
        // 启动清理connection的线程,正常情况按keepAliveDurationNs周期执行
        if (!cleanupRunning) {
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }

Internal方法暂时不说,这里会判断下清理线程是否开启了,没开启的话,就运行cleanupRunnable,并将connection添加到ConnectionPool1中。

下面我们再回过头来分析cleanupRunnable,注释写的很清楚了,我们进去看cleanup方法,这里才是清除操作的核心所在:

    long cleanup(long now) {
        int inUseConnectionCount = 0;
        int idleConnectionCount = 0;
        RealConnection longestIdleConnection = null;
        long longestIdleDurationNs = Long.MIN_VALUE;

        // 找到要去除的connection,或者下一个要调用驱除的时间
        // Find either a connection to evict, or the time that the next eviction is due.
        synchronized (this) {
            for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
                RealConnection connection = i.next();

                // 修剪任何泄漏的分配(整理connection内的StreamAllocation),返回正在使用的流的数量,并对泄露情况处理
                // If the connection is in use, keep searching.
                if (pruneAndGetAllocationCount(connection, now) > 0) {
                    inUseConnectionCount++;
                    continue;
                }

                // connection没有分配流,即该connection不在使用了
                idleConnectionCount++;

                // 拿到空闲最久的connection,只拿一个?
                // If the connection is ready to be evicted, we're done.
                long idleDurationNs = now - connection.idleAtNanos;
                if (idleDurationNs > longestIdleDurationNs) {
                    longestIdleDurationNs = idleDurationNs;
                    longestIdleConnection = connection;
                }
            }

            // 对当前最长空闲的connection处理,超时、超多时移除该connection
            // 这里会return 0 => 直接进入cleanupRunnable下一个cleanup
            if (longestIdleDurationNs >= this.keepAliveDurationNs
                    || idleConnectionCount > this.maxIdleConnections) {
                // We've found a connection to evict. Remove it from the list, then close it below (outside
                // of the synchronized block).
                connections.remove(longestIdleConnection);

                // 最长空闲的connection还没超时,计算下下次触发cleanup的等待时间
            } else if (idleConnectionCount > 0) {
                // A connection will be ready to evict soon.
                return keepAliveDurationNs - longestIdleDurationNs;

                // connection还在使用,进入下一个keepAliveDurationNs周期的cleanup
            } else if (inUseConnectionCount > 0) {
                // All connections are in use. It'll be at least the keep alive duration 'til we run again.
                return keepAliveDurationNs;

                // 当前connectionPool里面没有connection,return -1 => cleanupRunnable周期循环结束
            } else {
                // 为什么写在这,不写到cleanupRunnable里面去。。。
                // No connections, idle or in use.
                cleanupRunning = false;
                return -1;
            }
        }

        // 对应上面if第一个情况,所以最长等待的connection需要关闭socket
        closeQuietly(longestIdleConnection.socket());

        // Cleanup again immediately.
        return 0;
    }

这里比较长,可以结合注释看下,大致内容就是遍历当前池中的connection,看看有没有使用(需不需要使用)了,一次关闭一个,关闭那个到空闲最久的connection,并得到一个延迟时间,来关闭下一个connection。

为什么括号里有“需不需要使用”,这里我们要看下pruneAndGetAllocationCount这个方法了,发生泄露的时候他也会处理下:

    // 修剪任何泄漏的分配,然后返回连接上剩余的活动分配数。如果连接正在跟踪分配但应用程序代码已放弃它们,则分配会泄漏。
    // 泄漏检测是不精确的并且依赖于垃圾收集。
    /**
     * Prunes any leaked allocations and then returns the number of remaining live allocations on
     * {@code connection}. Allocations are leaked if the connection is tracking them but the
     * application code has abandoned them. Leak detection is imprecise and relies on garbage
     * collection.
     */
    private int pruneAndGetAllocationCount(RealConnection connection, long now) {
        // RealConnection中储存的流的情况,在ConnectionPool的get方法中创建的弱引用
        List<Reference<StreamAllocation>> references = connection.allocations;
        for (int i = 0; i < references.size(); ) {
            Reference<StreamAllocation> reference = references.get(i);

            // 流还在使用
            if (reference.get() != null) {
                i++;
                continue;
            }

            // 流已经被GC了,弱引用还存在,内存泄露了,可能是没有关闭response body的流
            // We've discovered a leaked allocation. This is an application bug.
            StreamAllocation.StreamAllocationReference streamAllocRef =
                    (StreamAllocation.StreamAllocationReference) reference;
            String message = "A connection to " + connection.route().address().url()
                    + " was leaked. Did you forget to close a response body?";
            Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);

            // 没关闭response body的流所以不能创建新流了?等着已经有的流关闭?
            references.remove(i);
            connection.noNewStreams = true;

            // If this was the last allocation, the connection is eligible for immediate eviction.
            if (references.isEmpty()) {
                // 用当前的time减去连接保持的时间,修改开始的time,在cleanup里面就会认为这个连接正好超时了,会被清理
                connection.idleAtNanos = now - keepAliveDurationNs;
                return 0;
            }
        }

        return references.size();
    }

这里就是说的一个connection里面可能有多个StreamAllocation,里面如果有泄露的话(response body),需要将这个StreamAllocation去除掉,然后返回还在使用中的StreamAllocation数量。

到这清理的功能就看完了,下面看下其他数据域。

其他数据域

    // 内部保存的RealConnection,遍历比较多所以用ArrayDeque不用LinkedList
    private final Deque<RealConnection> connections = new ArrayDeque<>();
    // RouteDatabase是对失败Route的一个记录,会在Internal中向外公开
    // 在StreamAllocation中参与routeSelector的创建,在findConnection中记录成功,在routeSelector中会有连接失败
    final RouteDatabase routeDatabase = new RouteDatabase();
    boolean cleanupRunning;

cleanupRunning上面已经讲到了,connections也比较简单,上面put方法也是放在它里面,注意下遍历比较多所以用的ArrayDeque,routeDatabase比较有意思,我们这里追踪下。

public final class RouteDatabase {
  private final Set<Route> failedRoutes = new LinkedHashSet<>();

  /** Records a failure connecting to {@code failedRoute}. */
  public synchronized void failed(Route failedRoute) {
    failedRoutes.add(failedRoute);
  }

  /** Records success connecting to {@code route}. */
  public synchronized void connected(Route route) {
    failedRoutes.remove(route);
  }

  /** Returns true if {@code route} has failed recently and should be avoided. */
  public synchronized boolean shouldPostpone(Route route) {
    return failedRoutes.contains(route);
  }
}

里面实现比较简单,但是我发现前面文章漏了很多东西,所以这里值得我追踪下,这里会跳到Internal里面,OkHttpClient -> Internal:

  static {
    Internal.instance = new Internal() {
      //...其他代码
    
      @Override public RouteDatabase routeDatabase(ConnectionPool connectionPool) {
        return connectionPool.routeDatabase;
      }
    
    }
  }

这里Internal是在OkHttpClient里面的一个匿名类,并在static块中完成初始化,看看它的说明:

/**
 * Escalate internal APIs in {@code okhttp3} so they can be used from OkHttp's implementation
 * packages. The only implementation of this interface is in {@link OkHttpClient}.
 */
 // 升级 okhttp3 中的内部 API,以便它们可以从 OkHttp 的实现包中使用。这个接口的唯一实现是在 OkHttpClient 中。

也就是说,它是用来给okhttp3增加能力的工具,很有设计意思啊。里面还有一些方法,也是ConnectionPool里面的,后面讲到了再说。

再追踪RouteDatabase,就是在StreamAllocation中参与routeSelector的创建,和在findConnection中记录成功了,至于失败,会在routeSelector中会有连接失败。

get方法

    @Nullable
    RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
        assert (Thread.holdsLock(this));
        for (RealConnection connection : connections) {
            // connection对address、route可用(Eligible),connection能创建流、不在黑名单、特殊情况
            if (connection.isEligible(address, route)) {
                // 让当前的streamAllocation去持有这个connection,并且让connection中记录这个流的分配
                streamAllocation.acquire(connection, true);
                return connection;
            }
        }
        return null;
    }

这里get方法就是ConnectionPool的取了,就是遍历dequeue找出合适的connection,isEligible可以看下里面的内容,比较长,主要是判断是否合适。

如果connection合适的话,会调用streamAllocation去acquire,这里向connection的allocations里面加了一个StreamAllocation的弱引用,还记得HTTP2的内容不,HTTP可是没有多个StreamAllocation的。

  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

deduplicate方法

    // 如果可能,将 streamAllocation 持有的连接替换为共享连接。当同时创建多个多路复用连接时,这会恢复
    // deduplicate在Internal.instance调用,最终用在StreamAllocation的findConnection中,防止异步创建两个相同地址的connection
    @Nullable
    Socket deduplicate(Address address, StreamAllocation streamAllocation) {
        assert (Thread.holdsLock(this));
        for (RealConnection connection : connections) {
            // isMultiplexed返回http2Connection!=null,只有HTTP2才复用连接
            if (connection.isEligible(address, null)
                    && connection.isMultiplexed()
                    && connection != streamAllocation.connection()) {
                return streamAllocation.releaseAndAcquire(connection);
            }
        }
        return null;
    }

这个方法是在streamAllocation中使用的,用来消除重复情况,可能会异步创建多个同地址的连接。这里也是通过Internal拐了个弯。

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

connectionBecameIdle方法

    // 通过Internal在StreamAllocation的deallocate(解除分配)方法中调用
    // 通知此连接已变为空闲。如果连接已从池中删除并应关闭,则返回 true。
    boolean connectionBecameIdle(RealConnection connection) {
        assert (Thread.holdsLock(this));
        if (connection.noNewStreams || maxIdleConnections == 0) {
            connections.remove(connection);
            return true;
        } else {
            // 唤醒cleanup线程,我们可能已经超过空闲连接限制
            // 在cleanup方法中synchronized (this)了,其他地方对connection加了对象锁的话,会卡住cleanupRunnable的cleanup方法
            notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
            return false;
        }
    }

注释已经写了,这个也是通过Internal来给okhttpClient扩展的,在解除分配的时候用来清除无效connection,或者释放当前ConnectionPool的锁,其他代码地方大量对ConnectionPool进行了锁,释放对象锁,让清除线程能正常工作。

evictAll方法

    // 向外提供的方法,通过client.connectionPool().evictAll()调用
    /** Close and remove all idle connections in the pool. */
    public void evictAll() {
        List<RealConnection> evictedConnections = new ArrayList<>();
        synchronized (this) {
            for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
                RealConnection connection = i.next();
                if (connection.allocations.isEmpty()) {
                    connection.noNewStreams = true;
                    evictedConnections.add(connection);
                    i.remove();
                }
            }
        }

        for (RealConnection connection : evictedConnections) {
            closeQuietly(connection.socket());
        }
    }

前面的方法都是在Internal中拐弯,向外提供,但是这个evictAll好像不一样,这个我没有找到调用的地方,不过看了OkHttpClient的说明,里面说到了用法:

Clear the connection pool with evictAll(). Note that the connection pool's daemon thread may not exit immediately.

     client.connectionPool().evictAll();

到这ConnectionPool就说完了,整个类加注释也就300多行,不多里面出现了一些前面没注意的东西,下面还得继续说说。

Internal

前面已经说过了,Internal是在OkHttpClient里面的一个匿名类,并在static块中完成初始化,它是用来给okhttp3增加能力的工具,,说明如下:

/**
 * Escalate internal APIs in {@code okhttp3} so they can be used from OkHttp's implementation
 * packages. The only implementation of this interface is in {@link OkHttpClient}.
 */
 // 升级 okhttp3 中的内部 API,以便它们可以从 OkHttp 的实现包中使用。这个接口的唯一实现是在 OkHttpClient 中。

里面的方法下面整理了一下,就这么几类:

builder.addLenient(line);
builder.addLenient(name, value);
builder.setInternalCache(internalCache);

pool.connectionBecameIdle(connection);
pool.get(address, streamAllocation, route);
pool.deduplicate(address, streamAllocation);
pool.put(connection);
connectionPool.routeDatabase;

address_a.equalsNonHost(address_b);

responseBuilder.code;

tlsConfiguration.apply(sslSocket, isFallback);

e.getMessage().startsWith(HttpUrl.Builder.INVALID_HOST);

((RealCall) call).streamAllocation();
((RealCall) call).timeoutExit(e);
RealCall.newRealCall(client, originalRequest, true);

StreamAllocation再补充

看完这节的ConnectionPool,我就发现前面StreamAllocation有些比较难理解的东西,真就拨云见日了,下面再对StreamAllocation做一些补充。

reportedAcquired

reportedAcquired是个数据标志,前面我们跳过了,现在终于知道了,就是connection的allocations添加了StreamAllocation的弱引用嘛,上面有讲到。

releaseIfNoNewStreams方法

  private Socket releaseIfNoNewStreams() {
    assert (Thread.holdsLock(connectionPool));
    RealConnection allocatedConnection = this.connection;
    if (allocatedConnection != null && allocatedConnection.noNewStreams) {
      return deallocate(false, false, true);
    }
    return null;
  }

这里看不出什么,再看下deallocate方法:

// 释放此分配持有的资源。如果分配了足够的资源,连接将被分离或关闭。调用者必须在连接池上同步。
private Socket deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {
    assert (Thread.holdsLock(connectionPool));

    // 注意这里streamFinished将this.codec置空了,下面逻辑用到了
    if (streamFinished) {
      this.codec = null;
    }
    if (released) {
      this.released = true;
    }
    Socket socket = null;
    if (connection != null) {
      if (noNewStreams) {
        connection.noNewStreams = true;
      }
      // 不再使用或不能使用connection了
      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();
          }
        }
        // 注意这里对StreamAllocation持有的connection置空了
        connection = null;
      }
    }
    return socket;
  }

这里代码一大堆,实际就做了一件事,就是关闭流(分离流),release关闭了当前connection的当前StreamAllocation。后面判断了下connection里面是否还有流,空了的话,设置连接的最新空闲的时刻,并调用connectionBecameIdle方法,在ConnectionPool中清除连接。

这里清除成功会返回true,所以这里得到的socket就是一个没用使用到的socket了,回到releaseIfNoNewStreams被调用的findConnection方法,这里会把它关闭了

结合releaseIfNoNewStreams这个名字,再来理解下这个过程,也就是说StreamAllocation里面持有的一个connection不能新建流了,且没有其他的StreamAllocation在使用这个connection了,那就会关闭这个socket,这个过程在StreamAllocation创建connection的时候,目的时用于清除旧的且没有用了的connection.

streamFinished方法

public void streamFinished(boolean noNewStreams, HttpCodec codec, long bytesRead, IOException e) {
    eventListener.responseBodyEnd(call, bytesRead);

    Socket socket;
    Connection releasedConnection;
    boolean callEnd;
    synchronized (connectionPool) {
      // 这里保证了this.codec==codec!=null
      if (codec == null || codec != this.codec) {
        throw new IllegalStateException("expected " + this.codec + " but was " + codec);
      }
      
      // 要复用这个connection
      if (!noNewStreams) {
        connection.successCount++;
      }
      
      // 核心代码
      releasedConnection = connection;
      socket = deallocate(noNewStreams, false, true);
      
      // deallocate会将connection关闭并置空,但是条件如下:
      // if (this.codec == null && (this.released || connection.noNewStreams)) 
      if (connection != null) releasedConnection = null;
      callEnd = this.released;
    }
    // 这里socket只有在connection里面的流空了才会返回,否则是空的
    closeQuietly(socket);
    if (releasedConnection != null) {
      // releasedConnection就是为了做下记录?
      eventListener.connectionReleased(call, releasedConnection);
    }
    
    //eventListener代码 。。。
  }

这个方法主要是用在HttpCodec里面(endOfInput),用来关闭流,下面是Http1Codec调用的地方:

    // 关闭缓存条目并使套接字可供重用。这应该在到达主体末尾时调用。
    protected final void endOfInput(boolean reuseConnection, IOException e) 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) {
        // noNewStreams=!reuseConnection,所以复用socket就不传noNewStreams
        streamAllocation.streamFinished(!reuseConnection, Http1Codec.this, bytesRead, e);
      }
    }
  }

在回到streamFinished里面,实际它就是调用了下上面的deallocate方法,这里有个releasedConnection让我很懵逼,一般来说deallocate方法会让connection置空:

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

如果connection!=null那就是这里条件没符合,但是最后releasedConnection只是用eventListener记录了下,也就是connection==null记录二零关闭的那个connection,connection!=null就不记录,所以就是指上面的条件没关闭connection吗?好吧,这里就是为了记录下吧。

release和noNewStreams方法

讲完streamFinished方法,下面还有两个方法类似的,都是和deallocate有关系,一并讲了吧,就是传入deallocate的参数不一样,

  public void release() {
    Socket socket;
    Connection releasedConnection;
    synchronized (connectionPool) {
      releasedConnection = connection;
      // 这里会不会触发对connection的清理,取决于this.codec是不是null
      socket = deallocate(false, true, false);
      if (connection != null) releasedConnection = null;
    }
    // 这里socket只有在connection里面的流空了才会返回,否则是空的
    closeQuietly(socket);
    if (releasedConnection != null) {
      Internal.instance.timeoutExit(call, null);
      eventListener.connectionReleased(call, releasedConnection);
      eventListener.callEnd(call);
    }
  }

这里deallocate传进去的参数不会触发上一小节说的清理,还是要看release前面会不会对this.codec置空,看了下用的地方都是在RetryAndFollowUpInterceptor里面,那就只是关闭了当前streamAllocation,并没有关闭这个连接,还是可以复用的。

  /** Forbid new streams from being created on the connection that hosts this allocation. */
  public void noNewStreams() {
    Socket socket;
    Connection releasedConnection;
    synchronized (connectionPool) {
      releasedConnection = connection;
      socket = deallocate(true, false, false);
      if (connection != null) releasedConnection = null;
    }
    // 这里socket只有在connection里面的流空了才会返回,否则是空的
    closeQuietly(socket);
    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
  }

而noNewStreams传进去的参数,也是不能保证connection被置空,它会让connection变为noNewStreams,估计是让线程池的cleanup线程自行处理吧。

这里还要注意下这个socket的关闭,在deallocate里面清理了connection的流,还需要connection的流空了才会返回这个socket,还要复用呢,哪能随便关掉!

streamFailed方法

下面又是一个特别长的方法,streamFailed一听名字应该就是知道是对创建流失败后的操作,用在RetryAndFollowUpInterceptor里面,对releaseConnection的情况进行处理:

public void streamFailed(IOException e) {
    Socket socket;
    Connection releasedConnection;
    boolean noNewStreams = false;

    // 锁住连接池?为什么不锁住connection呢?不让其他地方get、put这个connection?
    synchronized (connectionPool) {
      if (e instanceof StreamResetException) {
        ErrorCode errorCode = ((StreamResetException) e).errorCode;
        // 重试一次
        if (errorCode == ErrorCode.REFUSED_STREAM) {
          // Retry REFUSED_STREAM errors once on the same connection.
          refusedStreamCount++;
          if (refusedStreamCount > 1) {
            noNewStreams = true;
            route = null;
          }
          
          // 不让再创建流
        } else if (errorCode != ErrorCode.CANCEL) {
          // Keep the connection for CANCEL errors. Everything else wants a fresh connection.
          noNewStreams = true;
          route = null;
        }
        
        // connection可以了,但是HTTP或者连接关闭异常?
      } else if (connection != null
          && (!connection.isMultiplexed() || e instanceof ConnectionShutdownException)) {
        // 不要忽略了这句,不再创建流  
        noNewStreams = true;

        // 是route就连接失败了吗?connection.successCount在上面的streamFinished增加的,就是没成功过吧
        // If this route hasn't completed a call, avoid it for new connections.
        if (connection.successCount == 0) {
          if (route != null && e != null) {
            routeSelector.connectFailed(route, e);
          }
          route = null;
        }
      }
      
      // 又来一遍deallocate,注意传入了streamFinished,会置空codec,可能触发对connection里面流的关闭
      releasedConnection = connection;
      socket = deallocate(noNewStreams, false, true);
      if (connection != null || !reportedAcquired) releasedConnection = null;
    }

    // 这里socket只有在connection里面的流空了才会返回,否则是空的
    closeQuietly(socket);
    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
  }

大致整理了下,就是操作了下noNewStreams和route,调用了一下deallocate,传入了streamFinished,会置空codec,可能触发对connection里面流的关闭。

为什么是可能,因为还要看noNewStreams啊,noNewStreams和released都传false就不会触发,第一个ErrorCode.REFUSED_STREAM重试的时候就不会触发。

releaseAndAcquire方法

看到这我也头疼了,还好这里是StreamAllocation最后一个重要方法了,前面已经讲到release方法了,是对当前StreamAllocation从对应connection中移除的功能,而下面这个这方法则是让StreamAllocation释放旧connection,更换新的connection:

  // 释放此连接持有的连接并改为获取 newConnection。只有在保持的连接是新连接但被 newConnection 复制的情况下调用它才是安全的。这通常发生在同时连接到 HTTP/2 网络服务器时。
  public Socket releaseAndAcquire(RealConnection newConnection) {
    assert (Thread.holdsLock(connectionPool));
    // 旧的connection里面只允许有一个allocation元素,且没有在使用流了,多个allocation元素也没法这样换啊!这里会release的。
    if (codec != null || connection.allocations.size() != 1) throw new IllegalStateException();

    // 直接把旧连接里面的StreamAllocation弱引用放到新的connection里面就行了
    // Release the old connection.
    Reference<StreamAllocation> onlyAllocation = connection.allocations.get(0);
    
    // 这里就传了noNewStreams=true,需要注意这里codec==null了,里面会触发对旧connection的清理,清理了就会得到socket
    Socket socket = deallocate(true, false, false);

    // Acquire the new connection.
    this.connection = newConnection;
    newConnection.allocations.add(onlyAllocation);

    return socket;
  }

这里就是deallocate最后一个调用地方了,和上面五个类似,上面注释写的很清楚了,deallocate应该能理解了吧。至于release和互换的逻辑比较简单。

这个方法唯一用的地方是在ConnectionPool的deduplicate方法,上面也有讲,再提一下,估计就好理解了,就是异步可能创建了相同的connection,调整一下。

    // 如果可能,将 streamAllocation 持有的连接替换为共享连接。当同时创建多个多路复用连接时,这会恢复
    // deduplicate在Internal.instance调用,最终用在StreamAllocation的findConnection中,防止异步创建两个相同地址的connection
    @Nullable
    Socket deduplicate(Address address, StreamAllocation streamAllocation) {
        assert (Thread.holdsLock(this));
        for (RealConnection connection : connections) {
            // isMultiplexed返回http2Connection!=null,只有HTTP2才复用连接
            if (connection.isEligible(address, null)
                    && connection.isMultiplexed()
                    && connection != streamAllocation.connection()) {
                return streamAllocation.releaseAndAcquire(connection);
            }
        }
        return null;
    }

小结

这篇文章写的又有点长了,写了ConnectionPool和StreamAllocation比较多的内容,也附带讲了下Internal,看完ConnectionPool回过头来再看StreamAllocation真就理解通畅了好多。

OkHttp源码中Dispatcher和connectionPool线程池分析 在前面《OkHttp原理流程源码分析》中梳理分析了OkHttp中同步发起网络请求和异步发起网络请求,最终的请求分发是有Dispatcher分发器来完成的,Dispatcher分发器的实现中涉及到的线程池的应用代码,并未做分析,今天我们先看第一个线程池应用,Dispatcher中的线程池实现。 一、OkHttp中Dispatcher的线程池实现 异步网络请求最终会被Dispatcher统一管理调度,client.dispatcher().enqueue(new AsyncCall(responseCallba 阅读详情

相关推荐

揭开 OkHttp3 高效处理 HTTP 请求的神秘面纱:池技术与拦截器

我们可以自定义拦截器,只需实现 Interceptor 接口,并在 intercept() 方法中添加自定义逻辑即可。例如:我们实现一个log的拦截器,类似于切面的角色,在请求前后分别打印一行日志。@OverrideOkHttp3 通过连接池和拦截器机制实现了高效、灵活的 HTTP 通信。连接池提升了性能,拦截器增强了代码的复用性、可扩展性和可维护性。

weixin_45593273的博客 1301

OkHttp3——连接池

ConnectionPool --> RealConnectionPool(里面包含List<Realconnection>) 连接池具有以下特点: 连接池管理所有Socket连接,当有新的请求时从池中分配一个链路。 默认支持5个并发keepalive,链路生命为5分钟(链路数据传输完成,保持5分钟的存活时间)。 自动清除线程,将超过5分钟的链路关闭socket。 OkHttp真正的连接是在ConnectInterceptor中通过 Transmitter(realC...

如若、然的专栏 5085

OkHttp-ConnectInterceptor源码解析

ConnectInterceptor源码解析 本文基于okhttp3.10.0 1. 概述 ConnectInterceptor主要是用于建立连接,并再连接成功后将流封装成对象传递给下一个拦截器CallServerInterceptor与远端进行读写操作。 这个过程中会涉及比较多类我们简述下每个类的作用 StreamAllocation:类似一个工厂用来创建连接RealConnection和...

朱利源的博客 632

OkHttp 工具包 创建了太多 ConnectionPool 对象导致内存爆炸

博文目录 文章目录问题说明问题解决 问题说明 OkHttp ConnectionPool 对象居然有近2000个, 占用了太多的内存, 导致宕机 理论上讲, 既然都使用连接池了, 那连接池对象有1个应该就够了, 为什么这里会有近2000个? 原来是自己封装的 OkHttp 工具包的设计和使用上出了问题 该工具包支持传入 OkHttpClient 对象, 也支持创建默认的 OkHttpClient 对象, 由于小伙伴们直接使用了不传参数的方式, 导致每次调用都会新建一个 OkHttpClient 对象.

mrathena 7593

OKHttp 3.10源码解析(四):连接机制

OKhttp的底层用的是Socket连接而不是URLConnection,所以整体来说还是比较复杂的,涉及到Http协议的封装和解封装、TLS/SSL安全协议的封装、Http2的封装等等,但还是非常值得我们去学习一下的,本篇文章不会对这些底层原理有详细的解析,但读完之后至少大概知道在网络请求中这些底层是如何实现的 一.HTTP请求的优化 1.keep-alive机制 我们知道,一个HTTP的...

相忘于江湖 1366

OkHttp源码解析()——连接池复用

1.StreamAllocation#newStream这个方法是在连接是返回stream对象的。我们看这个方法里的这段代码。 RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled,

_StriveG的专栏 3336

Okhttp的连接池ConnectionPool()

目录 1.get()方法 2.put() Okhttp3使用及解析:https://mp.csdn.net/postedit/83339916 okhttp系统拦截器:https://mp.csdn.net/postedit/83536609 Okhttp的连接池ConnectionPool:https://mp.csdn.net/postedit/83650740   Okhttp...

qq_37321098的博客 4482

OkHttp 源码解析(二)

简介 上本文主要看一下 RetryAndFollupInterceptor 并引出建立连接相关的分析。 RetryAndFollowUpInterceptor Interceptor 最主要的代码都在 intercept 中,下面是 RetryAndFollowUpInterceptor#intercept 中的部分代码: @Override public Response intercept(...

尤教授的博客 300

OKHttp源码解析 (复用连接池)

原文地址:https://www.jianshu.com/p/6166d28983a2 复用连接池 相关的主要三个类 RealConnection ConnectionPool StreamAllocation 对流的处理使用Okio,Okio是okhttp中用于处理io的模块,没接触过的读者可以参考这篇文章:https://www.jianshu.com/p/f5941bcf3a2d RealConnection 要点 代表着链接socket的链路,如果拥有了一个RealConnection.

许佳佳的博客 1002

okhttp源码学习笔记(二)-- 连接与连接管理

本篇文章为okhttp源码学习笔记系列的第二篇文章,本篇文章的主要内容为okhttp中的连接与连接的管理,因此需要重点介绍连接的概念。客户端通过HTTP协议与服务器进行通信,首先需要建立连接,okhttp并没有使用URLConnection, 而是对socket直接进行封装,在socket之上建立了connection的概念,代表这物理连接。同时,一对请求与响应对应着输出和输入流, okhttp中...

android开发笔记 782

OkHttp源码4:网络操作

主要侧重于整个过程的网络操作至此,分析完了OKHttp的网络请求部分,总结一下,在重试拦截器中获得Address和StreamAllocation(负责根据请求创建连接),在连接拦截器中获得连接,最后在网络进行发送请求头,请求体,获得响应头,获得响应体。值得注意的是连接的获得 即:如果有连接,直接用没有可用的连接,第一次去连接池中查找,找到后直接用没有找到,补充路由信息,在连接池中二次查找。依然没有找到,创建新连接,然后连接server,将其放入到连接池中。

weixin_50961942的博客 1844

OkHttp源码解析

文章目录一、请求流程二、同步、异步网络请求2.1 同步网络请求2.2 异步网络请求三、分发器Dispatcher四、责任链执行过程4.1 重试和重定向RetryAndFollowUpInterceptor4.2 桥拦截器BridgeInterceptor4.3 缓存拦截器CacheInterceptor4.4 连接拦截器ConnectInterceptor4.5 服务器请求拦截器CallServe...

墨玉浮白的博客 618

深入解析OkHttp3

OkHttp是一个精巧的网络请求库,有如下特性: 1)支持http2,对一台机器的所有请求共享同一个socket 2)内置连接池,支持连接复用,减少延迟 3)支持透明的gzip压缩响应体 4)通过缓存避免重复的请求 5)请求失败时自动重试主机的其他ip,自动重定向 6)好用的API...

chuanao8829的博客 168

okhttp网络框架源码解析十四okhttp框架连接池put()方法

1、ConnectionPool 不管是okhttp协议1.1也好,还是2.0也好,它们的keep-alive机制,或者2.0的多路复用机制,他们都需要引入一个连接池的概念,来维护整个okhttp的网络连接。okhttp会将客户端和服务端抽象为一个connection类,而realConnection就是它的实现类。为了管理所有的实现类,okhttp为我们提供了connectionPool这个类...

nicolelili1的专栏 316

Okhttp之连接池ConnectionPool简单分析(一)

ConnectionPool

菜鸟博客 2万+

OkHttp 源码解析(三)

简介 上一篇文章分析了 OkHttp 建立连接的过程,主要涉及到的几个类包括 StreamAllocation、RealConnection 以及 HttpCodec,其中 RealConnection 封装了底层的 Socket。Socket 建立了 TCP 连接,这是需要消耗时间和资源的,而 OkHttp 则使用连接池来管理这里连接,进行连接的重用,提高请求的效率。OkHttp 中的连接池由 ...

尤教授的博客 283

okhttp详解之连接池

本文,主要分析以下几个问题: 连接池ConnectionPool的定义及其详解。 连接connect复用详解。 connet清理。 访问同一个地址的socket复用详解。 .服务器重新定位, 包括IP地址重新定位、代理proxy重新定位。也就是如果当集群服务器存在多个代理,代理存在多个IP地址,那么访问一个IP地址失败的话,那么重新定位到另外一台服务器。 一套了解以上okhttp的特点,需...

wangqiubo2010的博客 2951

OkHttp3源码解析--拦截器ConnectInterceptor-处理流程

依旧再看一下ConnectInterceptor的intercept方法: public final class ConnectInterceptor implements Interceptor { public final OkHttpClient client;//已经创建的OkHttpClient对象 //构造器中传入OkHttpClient对象 public ConnectI...

猿诗人的博客 507
上一篇: okhttp3源码解析(5)-RealConnection、Http2Connection
下一篇: CSDN不再更新,后续文章在掘金发布
捡一晌贪欢
博客等级 码龄9年 70粉丝 156原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值