精华内容
下载资源
问答
  • OKHttp连接池

    2020-01-11 16:49:27
    OKHttp连接池 OkHttp单例模式连接池配置,有效防止线程阻塞 public class OKHttpClientUtil { private volatile static OkHttpClient client = null; public static OkHttpClient getOkHttpClirnt(){ if (client...

    OKHttp连接池

    OkHttp单例模式连接池配置,有效防止线程阻塞

    public class OKHttpClientUtil {
        private volatile static OkHttpClient client = null;
    
        public static OkHttpClient getOkHttpClirnt(){
            if (client==null){
                synchronized (OKHttpClientUtil.class){
                    if (client==null){
                        client = new OkHttpClient.Builder()
                                .followRedirects(true)
                                .connectTimeout(1, TimeUnit.SECONDS)
                                .readTimeout(1, TimeUnit.SECONDS)
                                .connectionPool(new ConnectionPool(5,1,TimeUnit.SECONDS))
                                .writeTimeout(1, TimeUnit.SECONDS)
                                .cookieJar(new LocalCookieJar())
                                .build();
                    }
                }
            }
            return client;
        }
    
    }
    
    展开全文
  • 本系列文章: OkHttp源码彻底解析(一)OkHttp请求流程 OkHttp源码彻底解析(二)OkHttp架构及API源码 ...OkHttp源码彻底解析(五)OkHttp连接池 目录 OkHttp连接池 连接池的意义——KeepAl...

    本系列文章:

    OkHttp源码彻底解析(一)OkHttp请求流程

    OkHttp源码彻底解析(二)OkHttp架构及API源码

    OkHttp源码彻底解析(三)OkHttp3.0拦截器原理——责任链模式

    OkHttp源码彻底解析(四)OkHttp拦截器的作用

    OkHttp源码彻底解析(五)OkHttp连接池

    目录

    OkHttp连接池

    连接池的意义——KeepAlive机制

    从拦截器流程了解连接池

    连接池ConnectionPool的创建

    连接池的缓存操作


    连接池是用来管理和复用网络连接对象的,而网络连接的主角就是Connection/RealConnection.

    OkHttp连接池

    OkHttp3将客户端与服务器之间的连接抽象为Connection/RealConnection,为了管理这些连接的复用而设计了ConnectionPool。共享相同Address的请求可以复用连接

    连接池的意义——KeepAlive机制

    复用连接,减少了频繁的网络请求导致性能下降的问题。我们知道,Http是基于TCP协议的,而TCP建立连接需要经过三次握手,断开需要经过四次挥手,因此,Http中添加了一种KeepAlive机制,当数据传输完毕后仍然保持连接,等待下一次请求时直接复用该连接。

    一次响应的流程

    在高并发的请求连接情况下或者同个客户端多次频繁的请求操作,无限制的创建会导致性能低下。

    如果使用keep-alive

    timeout空闲时间内,连接不会关闭,相同重复的request将复用原先的connection,减少握手的次数,大幅提高效率。

    并非keep-alive的timeout设置时间越长,就越能提升性能。长久不关闭会造成过多的僵尸连接和泄露连接出现。

    从拦截器流程了解连接池

    在讲解连接池之前,先了解OkHttp的拦截器

    OkHttp拦截器流程图

    上面的拦截器分别是:

    1.失败重连拦截器:
    一个循环来不停的获取response。每循环一次都会获取下一个request,如果没有,则返回response,退出循环。而获取下一个request的逻辑,是根据上一个response返回的状态码,分别作处理。

    2.桥接拦截器:

    请求从应用层数据类型类型转化为网络调用层的数据类型。将网络层返回的数据类型 转化为 应用层数据类型。(补足缺失的请求头等)

    3.缓存拦截器:

    CacheInterceptor主要作用是将请求 和 返回 关连得保存到缓存中。客户端与服务端根据一定的机制,在需要的时候使用缓存的数据作为网络请求的响应,节省了时间和带宽。

    4.连接拦截器 

    与请求服务器的拦截器是网络交互的关键。为请求服务器拦截器建立可用的连接,创建用于网络IO流  的RealConnection对象

    5.请求服务器的拦截器:

    完成了最后发起网络请求的工作。将HTTP请求写入网络IO流,从IO流读取网络数据

    与连接拦截器要划分为两个拦截器,除了解耦之外,更重要的是在这两个流程之间还可以插入一个专门为WebSocket服务的拦截器( WebSocket一种在单个 TCP 连接上进行全双工通讯的协议,本文不做详解)。
    关于这部分不具体展开,感兴趣可以看我的另外两篇博客,可以说是非常详细地介绍了拦截器的原理及应用

    OkHttp拦截器

    拦截器原理

    其中,与连接池相关的是失败重连拦截器连接拦截器

    1.RetryAndFollowUpInterceptor将创建的StreamAllocation对象传递给后面执行的Interceptor

    2.ConnectInterceptorRealInterceptorChain获取前面的Interceptor传过来的StreamAllocation对象,执行 streamAllocation.newStream() 完成前述所有的连接建立工作,创建的用于网络IO的RealConnection对象,以及对于与服务器交互最为关键的HttpCodec等对象传递给后面的Interceptor,也就是CallServerInterceptor

    streamAllocation.newStream()新建了IO流,将请求序列化发到网络,将网络数据反序列化并接收(请求服务器的拦截器)

    RealConnection底层连接着Socket,就是实现了跨进程与网络上其他设备交互的底层实现。

    连接池ConnectionPool的创建

    OkHttp3的用户可以自行创建ConnectionPool,对最大空闲连接数及连接的保活时间进行配置,并在OkHttpClient创建期间,将其传给OkHttpClient.Builder,在OkHttpClient中启用它。没有定制连接池的情况下,则在OkHttpClient.Builder构造过程中以默认参数

    默认情况下,ConnectionPool 最多保存 5个 处于空闲状态的连接,且连接的默认保活时间为 5分钟。也就是默认支持5个并发Socket连接,默认的keepAlive时间为5分钟,当然我们可以在构建OkHttpClient时设置不同的值

    连接池的缓存操作

    ConnectionPool提供对Deque<RealConnection>进行操作的方法分别为putgetconnectionBecameIdleevictAll几个操作。分别对应放入连接、获取连接、移除连接、移除所有连接操作。

    遍历connections缓存列表,当某个连接计数的次数小于限制的大小以及request的地址和缓存列表中此连接的地址完全匹配。则直接复用缓存列表中的connection作为request的连接。

    展开全文
  • no-no-no,这次我会整理点精华部分,让大家学习点东西,如标题所示,这次要讨论的话题是Okhttp连接池怎么工作的,以及它工作的原理,为什么要整理这篇文章呢,因为okhttp连接池在面试过程中很大可能被问到,因此...

    背景

    最近把Okhttp的源码又整理了下,之前也写过Okhttp源码的文章,我觉得那会对Okhttp的认识不够深入,所以这次还是像炒咸饭一样吗?no-no-no,这次我会整理点精华部分,让大家学习点东西,如标题所示,这次要讨论的话题是Okhttp的连接池怎么工作的,以及它工作的原理,为什么要整理这篇文章呢,因为okhttp的连接池在面试过程中很大可能被问到,因此在这里总结出来,供大家参考。

    为了大家更好的理解发起同步和异步的过程,画了张草图给大家,如果有不正确的地方望指出:

    • 我们知道Okhttp中通过okhttpClient对象是通过Builder对象初始化出来的,此处Builder的用法是建造者模式,建造者模式主要是分离出外部类的属性初始化,而初始化属性交给了内部类Buidler类,这么做的好处是外部类不用关心属性的初始化。 而在初始化的时候有interceptorsnetworkInterceptors两种拦截器的初始化,还有dispatcher(分发器)的初始化,以及后面需要讲到的cache(缓存)初始化等。
    • 初始化完了后通过builder的build方法构造出okhttpClient对象,该类被称作客户端类,通过它的newCall方法返回RealCall对象,在newCall过程的过程中需要request的信息,request信息包装了url、method、headers、body等信息。最后通过RealCall的同步或异步方法交给了okhttpClientdispatcher来处理,在处理同步或异步之前都会判断有没有正在executed,所以我们不能对同一个RealCall调用异步或同步方法。
    • 在异步的时候会把RealCall给包装成一个AsyncCall,它是一个runnable对象。接着就来到了分发器异步处理部分,首先会把AsyncCall加入到readyAsyncCalls的集合中,该集合表示准备阶段的请求集合,紧接着从runningAsyncCalls(该集合装的都是要即将请求的集合)readyAsyncCalls集合中找相同host的AsyncCall,如果找到了会把当中记录的相同host的个数给该AsyncCall注意这里保存host个数用的原子性的AtomicInteger来记录的
    • 接着会去判断最大的请求是否大于64以及相同host是否大于5个,这里也是okhttp面试高频知识点,如果都通过的话,会把当前的AsyncCall的相同host记录数加一,接着会加入到runningAsyncCalls集合中,接着循环遍历刚符合条件的AsyncCall,通过线程池去执行AsyncCall,注意此处的线程池的配置是没有核心线程,总的线程个数是没有限制的,也就是说都是非核心线程,并且个数没有限制,非核心线程等待的时间是60秒,并且使用的任务队列是SynchronousQueue,它是一个没有容量的阻塞队列,只会当里面没有任务的时候,才能往里面放任务,当放完之后,只能等它的任务被取走才能放,这不就是jdk里面提供的Executors.newCachedThreadPool线程池吗,可能是okhttp想自己定义线程工厂的参数吧,定义线程的名字。
    • 所以到这里才会进入到子线程,由于AsyncCall是一个runnable,因此最终执行来到了它的run方法吧,run方法最终会走到execute方法,该方法来到了okhttp最有意思的单链表结构的拦截器部分,它会把所有的拦截器组装成一个集合,然后传给RealInterceptorChainprocess方法,在该方法中,会先把下一个RealInterceptorChain初始化出来,然后把下一个RealInterceptorChain传给当前Interceptor的intercept方法,最终一个个的response返回到AsyncCallexecute方法。
    • 处理完当前的AsyncCall后,会交给dispatcher,它会将该AsyncCall的host数减一,并且把它从runningAsyncCalls集合中移除,接着再从readyAsyncCalls集合中拿剩下的AsyncCall继续执行,直到执行完readyAsyncCalls里面的AsyncCall

    关于okhttp发起异步和同步请求可以看这里OkHttp 源码解析

    或者看我之前分析的okhttpOkHttp大流程分析

    这就是整个okhttp的执行流程,而最重要的是拦截器部分,我会在这章介绍拦截器的连接池部分,先通过源码的形式介绍它的来龙去脉:

    连接池的意义

    • 频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源和浪费时间的,HTTP中的keepalive连接对于 降低延迟和提升速度有非常重要的作用。
    • 复用连接就需要对连接进行管理,这里就引入了连接池的概念。
    • Okhttp支持5个并发KeepAlive,默认链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。

    概要

    连接池部分主要是在RealConnectionPool类中,该类用connections(双端队列)存储所有的连接,cleanupRunnable是专门用来清除超时的RealConnection,既然有清除的任务,那肯定有清除的线程池,没错,该线程池(executor)跟okhttp处理异步时候的线程池是一样的,keepAliveDurationNs表示每一个连接keep-alive的时间,默认是5分钟,maxIdleConnections连接池的最大容量,默认是5个。RealConnection中有transmitters字段,用来保存该连接的transmitter个数,通过里面的transmitter个数来标记该RealConnection有没有在使用中。

    注:如果大家只是想关心面试过程中怎么针对面试官问连接池的问题,可以直接看文章结尾哟

    源码分析

    1.RealCall#getResponseWithInterceptorChain

    拦截器的组装是在RealCall的getResponseWithInterceptorChain方法中放到集合里面了:

    Response getResponseWithInterceptorChain() throws IOException {
        //所有拦截器的组装集合
        List<Interceptor> interceptors = new ArrayList<>();
        interceptors.addAll(client.interceptors());
        interceptors.add(new RetryAndFollowUpInterceptor(client));
        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, transmitter, null, 0,
            originalRequest, this, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());
        try {
          Response response = chain.proceed(originalRequest);
          return response;
        } catch (IOException e) {
    
        } finally {
    
        }
      }
    }
    

    2.ConnectInterceptor#intercept

    连接池在ConnectInterceptor中包装起来了,我们进去瞄一眼:

    @Override
    public Response intercept(Chain chain) throws IOException {
      RealInterceptorChain realChain = (RealInterceptorChain) chain;
      Request request = realChain.request();
      //拿到chain的Transmitter,里面包装了RealCall、okhttpClient、connectionPool等信息
      Transmitter transmitter = realChain.transmitter();
      //是否不是get请求
      boolean doExtensiveHealthChecks = !request.method().equals("GET");
      Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
    
      return realChain.proceed(request, transmitter, exchange);
    }
    

    3.Transmitter#newExchange

    拿到chain的Transmitter对象,该对象是RealCallokhttpClientconnectionPool等信息的包装类,将是否不是get请求的标识传给了transmitternewExchange方法:

    Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
      //exchangeFinder是对connectionPool、connectionPool、request等信息的包装,它是在RetryAndFollowUpInterceptor拦截器中初始化出来的
      ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
      Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);
      synchronized (connectionPool) {
        this.exchange = result;
        return result;
      }
    }
    

    4.ExchangeCodec#find

    该方法中将ExchangeCodec的获取交给了exchangeFinder对象,ExchangeCodec是一个接口,实现类有Http2ExchangeCodecHttp1ExchangeCodec,这两个类表示http1和http2的建立连接的类,里面实现了writeRequestHeaderscreateRequestBody等方法,这两个方法是在CallServerInterceptor拦截器中使用的。exchangeFinder是对connectionPool、connectionPool、request等信息的包装,它是在RetryAndFollowUpInterceptor拦截器中初始化出来的。我们接着看exchangeFinder的find方法:

    public ExchangeCodec find(
        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);
        return resultConnection.newCodec(client, chain);
      } catch (RouteException e) {
      } catch (IOException e) {
    
      }
    }
    

    5.ExchangeCodec#findHealthyConnection

    这个方法没什么好说的,就一句,看findHealthyConnection方法:

    private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
        int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
        boolean doExtensiveHealthChecks) throws IOException {
      //开启循环找符合条件的RealConnection
      while (true) {
        //找当前keep-alive有效时间内的连接
        RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
            pingIntervalMillis, connectionRetryEnabled);
    
        // If this is a brand new connection, we can skip the extensive health checks.
        synchronized (connectionPool) {
          //如果该连接没有出现过连接失败才会返回,连接失败的标识是在Exchange中处理的,而Exchange中的处理是交给了CallServerInterceptor的
          if (candidate.successCount == 0) {
            return candidate;
          }
        }
    
        //如果socket连接断开了或者失败了也认为不是有效的连接
        if (!candidate.isHealthy(doExtensiveHealthChecks)) {
          candidate.noNewExchanges();
          continue;
        }
    
        return candidate;
      }
    }
    

    6.ExchangeCodec#findConnection

    该方法会开启循环来找符合keep-alive有效时间内的连接,紧接着判断它是否在CallServerInterceptor拦截器处理过程中出现连接失败或者是该连接的socket连接断开了也认为不是有效的连接。我们主要看findConnection怎么处理keep-alive的有效时间的连接:

      private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
          int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
        boolean foundPooledConnection = false;
        //定义最终找到的那个RealConnection
        RealConnection result = null;
        Route selectedRoute = null;
        //用来标识有没有分配的连接
        RealConnection releasedConnection;
        Socket toClose;
        synchronized (connectionPool) {
          if (transmitter.isCanceled()) throw new IOException("Canceled");
          hasStreamFailure = false; // This is a fresh attempt.
          //获取当前分配的连接的路由信息
          Route previousRoute = retryCurrentRoute()
              ? transmitter.connection.route()
              : null;
    
          //获取有没有分配过的连接
          releasedConnection = transmitter.connection;
          toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
              ? transmitter.releaseConnectionNoEvents()
              : null;
    
          //看当前正在分配的连接有没有
          if (transmitter.connection != null) {
            result = transmitter.connection;
            releasedConnection = null;
          }
    
          //如果上面都没找到从连接池中找连接
          if (result == null) {
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
              foundPooledConnection = true;
              result = transmitter.connection;
            } else {
              selectedRoute = previousRoute;
            }
          }
        }
        closeQuietly(toClose);
    
        //有的话直接返回
        if (result != null) {
          return result;
        }
    
        //检查有没有新的路由
        boolean newRouteSelection = false;
        if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
          newRouteSelection = true;
          routeSelection = routeSelector.next();
        }
    
        List<Route> routes = null;
        synchronized (connectionPool) {
          //如果有新的路由信息
          if (newRouteSelection) {
            routes = routeSelection.getAll();
            //再次从连接池中找
            if (connectionPool.transmitterAcquirePooledConnection(
                address, transmitter, routes, false)) {
              foundPooledConnection = true;
              result = transmitter.connection;
            }
          }
    
          // 没有从连接池中找到连接
          if (!foundPooledConnection) {
            //创建连接
            result = new RealConnection(connectionPool, selectedRoute);
            connectingConnection = result;
          }
        }
    
        // 从连接池中找到返回连接
        if (foundPooledConnection) {
          eventListener.connectionAcquired(call, result);
          return result;
        }
    
        // 准别TCP连接
        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
            connectionRetryEnabled, call, eventListener);
        connectionPool.routeDatabase.connected(result.route());
    
        Socket socket = null;
        synchronized (connectionPool) {
          connectingConnection = null;
          //继续从连接池中找一次看有没有符合条件的连接池
          if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
            result.noNewExchanges = true;
            socket = result.socket();
            result = transmitter.connection;
          } else {
            //如果不符合条件则把当前连接放到连接池里面
            connectionPool.put(result);
          }
        }
        closeQuietly(socket);
        return result;
    }
    

    首先会先判断有没有被分配的连接,如果没有,从连接池中找符合条件的连接,通过connectionPool.transmitterAcquirePooledConnection去找,如果返回true说明找到了,找到后会把connection放到transmitter里面,如果连接池里面都没获取到连接,则创建连接,创建完了之后准备TCP连接,接着又从连接池中再次获取一次,如果再获取不成功,就把创建的连接放到连接池里面。 这里面涉及到okhttp的代理和路由的获取,这块笔者也不是很懂,所以不在这里叙述

    7.RealConnectionPool#transmitterAcquirePooledConnection

    上面我们知道,通过connectionPool.transmitterAcquirePooledConnection获取连接,通过connectionPool.put往连接池中放连接,我们先来看如何获取有效的连接的:

    boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
        @Nullable List<Route> routes, boolean requireMultiplexed) { 
      //connections是一个Deque的队列,也就是双端队列
      for (RealConnection connection : connections) {
        //默认requireMultiplexed为false
        if (requireMultiplexed && !connection.isMultiplexed()) continue;
        //判断当前连接是不是合格的
        if (!connection.isEligible(address, routes)) continue;
        //给transmitter的connection对象赋值
        //向connection的transmitters中添加transmitter对象
        transmitter.acquireConnectionNoEvents(connection);
        return true;
      }
      return false;
    }
    

    okhttp中连接池存储空间是一个队列,并且该队列是一个ArrayDeque,它是一个双端队列:

    private final Deque<RealConnection> connections = new ArrayDeque<>();
    

    8.RealConnection#isEligible

    如果连接不是合格的则直接跳过该连接,接着操作了transmitteracquireConnectionNoEvents方法,我们分别看isEligible(合格判断)acquireConnectionNoEvents方法:

    boolean isEligible(Address address, @Nullable List<Route> routes) {
    
      // 如果url的host和当前连接的路由中的地址host相同则认为是合格的,说白了就是比较两个连接的host是否相同
      //所以连接池复用的连接是以host相同为合格的
      if (address.url().host().equals(this.route().address().url().host())) {
        return true; 
      }
      return true;
    }
    

    关于是否合格的判断我去掉了其他的判断,只留下了host比较的代码,如果两个连接host相同,则认为是要找的连接,否则就不是。

    9.Transmitter#acquireConnectionNoEvents

    void acquireConnectionNoEvents(RealConnection connection) {
      //把当前的连接给Transmitter中
      this.connection = connection;
      //给当前的连接的transmitters添加一个transmitter,注意这里是通过弱引用来包装的,注意该集合会在后面clean的时候有用到
      connection.transmitters.add(new TransmitterReference(this, callStackTrace));
    }
    

    这里给当前Transmitter的connection附上当前的connection,因为后面要返回它给result。接着给当前的连接的transmitters集合添加一个弱引用的transmitters对象,在开篇已经说了 realConnection的transmitters集合的个数是在clean的时候用来判断该realConnection有没有正在被用到。

    如果上面条件都满足,则transmitterAcquirePooledConnection返回true,表示从连接池中寻找realConnection成功了。

    10.RealConnectionPool#put

    说完了从连接池中查找过程,我们接着来到连接池存储realConnection过程,上面已经分析了存储是在RealConnectionPool的put方法:

    void put(RealConnection connection) {
      assert (Thread.holdsLock(this));
      //如果没有正在清除的工作
      if (!cleanupRunning) {
        cleanupRunning = true;
        //这就是开篇说的清除realConnection的线程池
        executor.execute(cleanupRunnable);
      }
      //清除完了后,把连接加入到队列中
      connections.add(connection);
    }
    

    11.RealConnectionPool#cleanupRunnable

    put过程很简单,先是做清除工作,然后做添加操作,我们着重看下清除怎么实现的,直接看cleanupRunnable的定义:

    private final Runnable cleanupRunnable = () -> {
      //开启了死循环进行删除操作
      while (true) {
        //真正实现清除的方法,并且返回要等多久才能进行下次的清除
        long waitNanos = cleanup(System.nanoTime());
        //如果返回-1说明没有要清除的realConnection
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (RealConnectionPool.this) {
            try {
              //这里让当前线程直接在同步代码块里面实现等待,等待时间是waitMillis然后继续执行清除操作
              RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    };
    

    开启死循环进行删除操作,删除操作是在cleanup中,该方法会返回要等待的时间才能执行下次的清除操作,如果返回-1说明没有要清除的realConnection了,通过同步代码块指定该线程要等待多少秒才能继续下次清除操作。

    12.RealConnectionPool#cleanup

    ok,我们把重心放在cleanup方法中:

    long cleanup(long now) {
      int inUseConnectionCount = 0;
      int idleConnectionCount = 0;
      RealConnection longestIdleConnection = null;
      long longestIdleDurationNs = Long.MIN_VALUE;
      synchronized (this) {
        for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
          RealConnection connection = i.next();
    
          //判断当前连接有没有正在使用
          if (pruneAndGetAllocationCount(connection, now) > 0) {
            //如果真在使用了inUseConnectionCount加1,并且直接跳出该次循环
            inUseConnectionCount++;
            continue;
          }
          //如果不是正在使用的连接,则把该变量加1
          idleConnectionCount++;
    
          //算出当前连接已经存活的时间
          long idleDurationNs = now - connection.idleAtNanos;
          //找出存活最久的那个连接
          if (idleDurationNs > longestIdleDurationNs) {
            longestIdleDurationNs = idleDurationNs;
            longestIdleConnection = connection;
          }
        }
        //如果最大存活的连接超过了keep-alive设置的时间或者没有正在使用的连接数超过了maxIdleConnections则从连接池中移除
        if (longestIdleDurationNs >= this.keepAliveDurationNs
            || idleConnectionCount > this.maxIdleConnections) {
          connections.remove(longestIdleConnection);
        } else if (idleConnectionCount > 0) {//如果还存在没有正在使用的连接,但是存活时间没超过keep-alive设置的时间并且个数没超过最大的个数,返回还要等多久的时间才能进行下次清除操作
          return keepAliveDurationNs - longestIdleDurationNs;
        } else if (inUseConnectionCount > 0) {//如果都是正在使用的连接,则直接返回keep-alive设置的时间,也就是5分钟
          return keepAliveDurationNs;
        } else {
          //如果连正在使用的连接都没有,则直接返回-1,说明不需要下次的清除操作
          cleanupRunning = false;
          return -1;
        }
      }
      closeQuietly(longestIdleConnection.socket());
      return 0;
    }
    

    清除操作的方法还是蛮清晰的:

    • 首先根据pruneAndGetAllocationCount方法判断该连接有没有被正在使用,如果是正在使用,则直接跳出该次循环,如果不是则把idleConnectionCount数加1。
    • 算出当前连接的已经存活的时间,然后找出存活最久的连接。
    • 如果最久的连接大于设置的keep-alive设定的时间或者没有正在使用的连接数超过了maxIdleConnections则从连接池中移除,注意此时方法直接返回0了,继续下一次的清除操作。
    • 如果还存在没有正在使用的连接,但是存活时间没超过keep-alive设置的时间并且个数没超过最大的个数,返回还要等多久的时间才能进行下次清除操作。
    • 如果都是正在使用的连接,则直接返回keep-alive设置的时间,也就是5分钟。
    • 如果连正在使用的连接都没有,则直接返回-1,说明不需要下次的清除操作。

    13.RealConnectionPool#pruneAndGetAllocationCount

    在清除的时候用pruneAndGetAllocationCount方法判断该连接是不是正在使用的,还记得上面我们一直讲到了realConnection的transmitters集合了吗,它是存储该连接的transmitter对象,下面我们来看看:

    private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List<Reference<Transmitter>> references = connection.transmitters;
    for (int i = 0; i < references.size(); ) {
      Reference<Transmitter> reference = references.get(i);
      //如果Transmitter对象不为空,说明该连接在被使用着
      if (reference.get() != null) {
        i++;
        continue;
      }
      //如果为空则说明要把当前的弱引用从集合中移除
      references.remove(i);
      connection.noNewExchanges = true;
      //如果RealConnection的transmitters集合为空的,说明它没有被使用,并且设置它的死亡时间,设置返回值为0,说明没有正在使用
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }
    

    14.Transmitter#releaseConnectionNoEvents

    在上面分析中我们发现只要RealConnection中的transmitters的Transmitter都为空的时候则说明该连接没有在使用中,那是在什么时候都为空呢,我们可以在releaseConnectionNoEvents方法中找到移除Transmitter的操作:

    Socket releaseConnectionNoEvents() {
      int index = -1;
      //如果当前的连接和里面的任何一个Transmitter是同一个的时候,则找到了要移除的Transmitter
      for (int i = 0, size = this.connection.transmitters.size(); i < size; i++) {
        Reference<Transmitter> reference = this.connection.transmitters.get(i);
        if (reference.get() == this) {
          index = i;
          break;
        }
      }
    
      RealConnection released = this.connection;
      //删除当前Transmitter
      released.transmitters.remove(index);
      this.connection = null;
    
      if (released.transmitters.isEmpty()) {
        released.idleAtNanos = System.nanoTime();
        if (connectionPool.connectionBecameIdle(released)) {
          return released.socket();
        }
      }
    
      return null;
    }
    

    该方法是确认realConnection的transmitters的删除Transmitter的方法,它是在每次进入到关闭连接的时候才可能触发删除Transmitter。所以这里可以看出来在关闭连接的时候connection中的transmitters个数变少。如果connection中的transmitters个数减少到0的时候说明该连接没有被使用了,也就印证了上面在清除过程中可以从连接池中被清除了。而在我们创建RealConnection和获取到连接池中的连接的时候会调用TransmitteracquireConnectionNoEvents方法,该方法会向RealConnection中的transmitters集合添加一个Transmitter对象的弱引用,这也正好和releaseConnectionNoEvents方法呼应。

    总结

    关于连接池中的源码部分就介绍这么多,下面我们再来总结下连接池中相关问题:

    • 连接池是为了解决频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)。
    • Okhttp的连接池支持最大5个链路的keep-alive连接,并且默认keep-alive的时间是5分钟。
    • 连接池实现的类是RealConnectionPool,它负责存储与清除的工作,存储是通过ArrayDeque的双端队列存储,删除交给了线程池处理cleanupRunnable的任务。
    • 在每次创建RealConnection或从连接池中拿一次RealConnection会给RealConnection的

    transmitters集合添加一个若引用的transmitter对象,添加它主要是为了后面判断该连接是否在使用中

    • 在连接池中找连接的时候会对比连接池中相同host的连接。
    • 如果在连接池中找不到连接的话,会创建连接,创建完后会存储到连接池中。
    • 在把连接放入连接池中时,会把清除操作的任务放入到线程池中执行,删除任务中会判断当前连接有没有在使用中,有没有正在使用通过RealConnection的transmitters集合的size是否为0来判断,如果不在使用中,找出空闲时间最长的连接,如果空闲时间最长的连接超过了keep-alive默认的5分钟或者空闲的连接数超过了最大的keep-alive连接数5个的话,会把存活时间最长的连接从连接池中删除。保证keep-alive的最大空闲时间和最大的连接数。

    推荐OKHttp相关讲解视频https://www.bilibili.com/video/BV18f4y1R7Jc

    大家如果还想了解更多Android 相关的更多知识点可以点进我的GitHub项目中,里面记录了许多的Android 知识点。

    展开全文
  • okhttp连接池实现

    2019-08-29 16:14:56
    代码中包含okhhtp中连接池的设计,包含连接对象的添加,连接对象何时被移除。
  • OkHttp3连接池原理:OkHttp3使用ConnectionPool连接池来复用链接,其原理是:当用户发起请求是,首先在链接池中检查是否有符合要求的链接(复用就在这里发生),如果有就用该链接发起网络请求,如果没有就创建一个...

    OkHttp3连接池原理:OkHttp3使用ConnectionPool连接池来复用链接,其原理是:当用户发起请求是,首先在链接池中检查是否有符合要求的链接(复用就在这里发生),如果有就用该链接发起网络请求,如果没有就创建一个链接发起请求。这种复用机制可以极大的减少网络延时并加快网络的请求和响应速度。

    源码分析

       // 最多保存 5个 处于空闲状态的连接,连接的默认保活时间为 5分钟
        public ConnectionPool() {
            this(5, 5, TimeUnit.MINUTES);
        }
    
        public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
            this.maxIdleConnections = maxIdleConnections;
            this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
    
            if (keepAliveDuration <= 0) {
                throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
            }
        }

     

    //连接池,其是一个双端链表结果,支持在头尾插入元素,且是一个后进先出的队列
        private static final Executor executor = new ThreadPoolExecutor(0  /* 核心线程数 */,
                Integer.MAX_VALUE /* 线程池可容纳的最大线程数量 */, 60L /* 线程池中的线程最大闲置时间 */, TimeUnit.SECONDS,
                /* 闲置时间的单位 */
                new SynchronousQueue<Runnable>()
                /*线程池中的任务队列,通过线程池的execute方法提交的runnable会放入这个队列中*/
                , Util.threadFactory("OkHttp ConnectionPool", true));
    
        //每个地址最大的空闲连接数
        private final int maxIdleConnections;
        private final long keepAliveDurationNs;

     ConnectionPool中会创建一个线程池,这个线程池的作用就是为了清理掉闲置的链接(Socket)。ConnectionPool利用自身的put方法向连接池中添加链接(每一个RealConnection都是一个链接)

     

        /**
        保存连接以复用*/
        void put(RealConnection connection) {
            assert (Thread.holdsLock(this));
         
            if (!cleanupRunning) {
                cleanupRunning = true;
                executor.execute(cleanupRunnable);
            }
            connections.add(connection);
        }

    向线程池中添加一个链接(RealConnection)其实是向连接池connections添加RealConnection。并且在添加之前需要调用线程池的execute方法区清理闲置的链接。并且在添加之前需要调用线程池的execute方法区清理闲置的链接 ,在来看cleanup 如何做清理闲置的链接

     private final Runnable cleanupRunnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    // 最快多久后需要再清理
                    long waitNanos = cleanup(System.nanoTime());
                    if (waitNanos == -1) return;
                    if (waitNanos > 0) {
                      
                        long waitMillis = waitNanos / 1000000L;
                        waitNanos -= (waitMillis * 1000000L);
                        synchronized (ConnectionPool.this) {
                            try { 
                                //根据下次返回的时间间隔来释放wait锁  参数多一个纳秒,制更加精准 
                                ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                            } catch (InterruptedException ignored) {
                            }
                        }
                    }
                }
            }
        };

    这里面具体就是GC回收算法,类似于标记清除算法,顾名思义,就是先标记处最不活跃的连接,然后清除。

     long cleanup(long now) {
    
            int inUseConnectionCount = 0; //正在使用的链接数量
            int idleConnectionCount = 0; //闲置的链接数量
            //长时间闲置的链接
            RealConnection longestIdleConnection = null;
            long longestIdleDurationNs = Long.MIN_VALUE;
    
            // 用for循环来遍历连接池
            synchronized (this) {
                for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
                    RealConnection connection = i.next();
    
                    //检查连接是否正在被使用
                    
                    if (pruneAndGetAllocationCount(connection, now) > 0) {
                        inUseConnectionCount++;
                        continue;
                    }
                    //否则记录闲置连接数
                    idleConnectionCount++;
    
        
                    //获得这个连接已经闲置多久
                    long idleDurationNs = now - connection.idleAtNanos;
                    if (idleDurationNs > longestIdleDurationNs) {
                        longestIdleDurationNs = idleDurationNs;
                        longestIdleConnection = connection;
                    }
                }
                // 超过了保活时间(5分钟) 或者池内数量超过了(5个) 马上移除,然后返回0,表示不等待,马上再次检查清理
                if (longestIdleDurationNs >= this.keepAliveDurationNs
                        || idleConnectionCount > this.maxIdleConnections) {
           
                    connections.remove(longestIdleConnection);
                } else if (idleConnectionCount > 0) {
                    // 
                    //池内存在闲置连接,就等待 保活时间(5分钟)-最长闲置时间 =还能闲置多久 再检查
                    return keepAliveDurationNs - longestIdleDurationNs;
                } else if (inUseConnectionCount > 0) {
                  
                    //有使用中的连接,就等 5分钟 再检查
                    return keepAliveDurationNs;
                } else {
                    .
                    //都不满足,可能池内没任何连接,直接停止清理(put后会再次启动)
                    cleanupRunning = false;
                    return -1;
                }
            }
             //关闭闲置时间最长的那个socket
            closeQuietly(longestIdleConnection.socket());
            return 0;
        }

    cleanup主要逻辑是 

    链接的限制时间如果大于用户设置的最大限制时间或者闲置链接的数量已经超出了用户设置的最大数量,则就执行清除操作。其下次清理的时间间隔有四个值:

      1.如果闲置的连接数大于0就返回用户设置的允许限制的时间-闲置时间最长的那个连接的闲置时间。

      2.如果清理失败就返回-1,

      3.如果清理成功就返回0,

      4.如果没有闲置的链接就直接返回用户设置的最大清理时间间隔。

     

    现在看pruneAndGetAllocationCount是如何判断当前循环到的链接是正在使用的链接

     

    private int pruneAndGetAllocationCount(RealConnection connection, long now) {
            // 这个连接被使用就会创建一个弱引用放入集合,这个集合不为空就表示这个连接正在被使用
        
            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;
                }
    
             
                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);
    
                references.remove(i);
                connection.noNewStreams = true;
    
                if (references.isEmpty()) {
                    connection.idleAtNanos = now - keepAliveDurationNs;
                    return 0;
                }
            }
    
            return references.size();
        }

     通过返回的 references的数量>0表示RealConnection活跃,如果<=0则表示RealConnection空闲。也就是用这个来方法来判断当前的链接是不是空闲的链接。

     

    下面看看连接的使用以及连接的复用是如何实现的

      RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
            assert (Thread.holdsLock(this));
            for (RealConnection connection : connections) {
                // 要拿到的连接与连接池中的连接  连接的配置(dns/代理/域名等等)一致 就可以复用
        
                if (connection.isEligible(address, route)) {
                    streamAllocation.acquire(connection, true);
                    return connection;
                }
            }
            return null;
        }

    获取连接池中的链接的逻辑非常的简单,利用for循环循环遍历连接池查看是否有符合要求的链接,如果有则直接返回该链接使用.判断是否有符合条件的链接:connection.isEligible(address,route)

     

    public boolean isEligible(Address address, @Nullable Route route) {
        //1、负载超过指定最大负载,不可复用 
        if (allocations.size() >= allocationLimit || noNewStreams) return false;
    
        //2、Address对象的非主机部分不相等,不可复用 
        if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
    
        //3、非主机部分不相等,不可复用 
     if(address.url().host().equals(this.route().address().url().host())) {
         //这个链接完美的匹配
          return true; // This connection is a perfect match.
        }
    
       
    
        // 4. This connection must be HTTP/2.
        if (http2Connection == null) return false;
    
        // The routes must share an IP address. This requires us to have a DNS address for both
        // hosts, which only happens after route planning. We can't coalesce connections that use a
        // proxy, since proxies don't tell us the origin server's IP address.
        //5
        if (route == null) return false;
        //6
        if (route.proxy().type() != Proxy.Type.DIRECT) return false;
        //7
        if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
        //8
        if (!this.route.socketAddress().equals(route.socketAddress())) return false;
    
        // 9
        if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
        if (!supportsUrl(address.url())) return false;
    
        // 10. Certificate pinning must match the host.
        try {
          address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
        } catch (SSLPeerUnverifiedException e) {
          return false;
        }
        //最终可以复用
        return true;
      }

    连接池已经分析完毕了,下面来总结一下

      1.创建一个连接池

        创建连接池非常简单只需要使用new关键字创建一个对象向就行了。new ConnectionPool(maxIdleConnections,keepAliveDuration,timeUnit)

      2.向连接池中添加一个连接

        a.通过ConnectionPool的put(realConnection)方法加入链接,在加入链接之前会先调用线程池执行cleanupRunnable匿名内部类来清理空闲的链接,然后再把链接加入Deque队列中,

        b.在cleanupRunnable匿名内部类中执行死循环不停的调用cleanup来清理空闲的连接,并返回一个下次清理的时间间隔,调用ConnectionPool.wait方法根据下次清理的时间间隔

        c.在cleanup的内部会遍历connections连接池队列,移除空闲时间最长的连接并返回下次清理的时间。

        d.判断连接是否空闲是利用RealConnection内部的List<Reference<StreamAllocation> 的size。如果size>0就说明不空闲,如果size<=0就说明空闲。

      3.获取一个链接

        通过ConnectionPool的get方法来获取符合要求的RealConnection。如果有服务要求的就返回RealConnection,并用该链接发起请求,如果没有符合要求的就返回null,并在外部重新创建一个RealConnection,然后再发起链接。判断条件:1.如果当前链接的技术次数大于限制的大小,或者无法在此链接上创建流,则直接返回false 2.如果地址主机字段不一致直接返回false3.如果主机地址完全匹配我们就重用该连接

     

    我们知道okhttp是可以通过连接池来减少请求延时的,那么这一点是怎么实现的呢?

    相关文章: okhttp连接池复用机制

    提高网络性能优化,很重要的一点就是降低延迟和提升响应速度。

    通常我们在浏览器中发起请求的时候header部分往往是这样的

    keep-alive 就是浏览器和服务端之间保持长连接,这个连接是可以复用的。在HTTP1.1中是默认开启的。

    连接的复用为什么会提高性能呢? 
    通常我们在发起http请求的时候首先要完成tcp的三次握手,然后传输数据,最后再释放连接。三次握手的过程可以参考这里 TCP三次握手详解及释放连接过程

    一次响应的过程

    在高并发的请求连接情况下或者同个客户端多次频繁的请求操作,无限制的创建会导致性能低下。

    如果使用keep-alive

    timeout空闲时间内,连接不会关闭,相同重复的request将复用原先的connection,减少握手的次数,大幅提高效率。

    并非keep-alive的timeout设置时间越长,就越能提升性能。长久不关闭会造成过多的僵尸连接和泄露连接出现。

    那么okttp在客户端是如果类似于浏览器客户端做到的keep-alive的机制呢?

    在BridgeInterceptor的intercept()方法中可以看到:
     

     
    1. if (userRequest.header("Connection") == null) {

    2. requestBuilder.header("Connection", "Keep-Alive");

    3. }

    4.  
    5. public Builder header(String name, String value) {

    6. headers.set(name, value);

    7. return this;

    8. }

    可以看到我们在request的请求头添加了("Connection", "Keep-Alive")的键值对。

     

     

    展开全文
  • 首先我们需要明白,okhttp比其他网络请求框架的优势在哪里?当然最重要的就是他对传输层的Socket进行了进一步的封装...所以okhttp就引出了连接池的概念。所谓的连接池就是为了复用socket,比如我们请求一个地址,如...
  • okHttp3连接池简单使用

    万次阅读 2018-11-18 23:05:55
    一、概述: ...这就是我们交换数据和媒体的方式。...连接池减少了请求延迟(如果HTTP / 2不可用)。 透明GZIP缩小了下载大小。 响应缓存完全避免网络重复请求。 当网络很麻烦时,OkHttp坚持不懈:它将从常见的连...
  • Okhttp连接池ConnectionPool(三)

    千次阅读 2018-11-02 10:47:15
    目录 1.get()方法 2.put() Okhttp3使用及解析:https://mp.csdn.net/postedit/83339916 okhttp系统拦截器:...Okhttp连接池ConnectionPool:https://mp.csdn.net/postedit/83650740   Okhttp...
  • okhttp的网络请求是基于socket请求,面不是原始的HttpConnection来操作的,但是Socket是很耗性能的,为什么Okhttp性能好速度快,是因为他有做网络请求缓存,还有一个就是他有一个连接池 连接池:主要目的就是把闲置一定...
  • okhttp详解之连接池

    千次阅读 2018-11-22 10:54:58
    连接池ConnectionPool的定义及其详解。 连接connect复用详解。 connet清理。 访问同一个地址的socket复用详解。 .服务器重新定位, 包括IP地址重新定位、代理proxy重新定位。也就是如果当集群服务器存在多个代理...
  • OkHttp3的连接池及连接建立过程分析

    千次阅读 2018-10-23 13:55:14
    OkHttp Flow 这些Interceptor中每一个的职责,这里不再赘述。 在OkHttp3中,StreamAllocation是用来建立执行HTTP请求所需网络设施的组件,如其名字所显示的那样,分配Stream。但它具体做的事情根据是否设...
  • okhttp 长连接_最大化OkHttp连接重用

    千次阅读 2020-08-22 23:54:24
    okhttp连接Debugging a 3rd party library 调试第三方库 介绍 (Introduction) At Booking.com we know that performance is important for our users, and this includes networking. Recently we investigated...
  • OkHttp3.7源码分析文章列表如下: ...接下来讲下OkHttp连接池管理,这也是OkHttp的核心部分。通过维护连接池,最大限度重用现有连接,减少网络连接的创建开销,以此提升网络请求效率。 1. 背景 1.1 keep-a
  • OKHTTP自动次数重试方案与连接池

    千次阅读 2019-06-20 18:28:34
    最近在工作中开始使用okhttp,详细的研究了一下源码,发现了关于连接池的部分与我之前的理解有些不符 new OkHttpClient().newBuilder().connectionPool(new ConnectionPool(100,1,TimeUnit.MILLISECONDS)) 使用过...
  • OkHttp3源码分析[综述]OkHttp3源码分析[复用连接池]OkHttp3源码分析[缓存策略]OkHttp3源码分析[DiskLruCache]OkHttp3源码分析[任务队列] 1. 概述 HTTP中的keepalive连接在网络性能优化中,对于延迟降低与...
  • 本篇博文将对OkHttp使用过程遇到的问题进行总结记录。 正文 同步请求SyncRequest 异步请求AsyncRequest 通过简单示例了解OkHttp如何进行http请求: SyncRequest: private static void syncRequest(String url) ...
  • #连接池的最大连接数,0代表不限;如果取0,需要考虑连接泄露导致系统崩溃的后果 maxTotalConnect: 1000 #每个路由的最大连接数,如果只调用一个地址,可以将其设置为最大连接数 maxConnectPerRoute: 200 # 指...
  •   OkHttp系列文章如下 OkHttp3源码分析[综述] OkHttp3源码分析[复用连接池] OkHttp3源码分析[缓存策略] OkHttp3源码分析[DiskLruCache] OkHttp3源码分析[任务队列] 1. 概述 HTTP中的keep...
  • OkHttp内部通过ConnectionPool来管理连接池,首先来看下ConnectionPool的主要成员: public final class ConnectionPool { private static final Executor executor = new ThreadPoolExecutor( ...
  • } public static class HttpTemplateBuilder { /** * 连接池的总连接限制 */ private Integer maxTotal; /** * 每个HTTP路由的连接限制 */ private Integer maxPerRoute; /** * 读超时 */ private Integer socket...
  • 从智联官网首页中通过查看源码获取其数据源,通过okhttp获得数据源。 (kHttp是一个优秀的网络请求框架) 2、将网络数据源转换为本地数据 通过json将网络数据转化为本地的数据 (json是一种轻量级的数据交换...
  • 最近在连接池上面栽了个跟头(参见这里),引起我对池技术的强烈关注,这几天总结了一下很多场景都会使用的池技术; 池概念 pool,中文翻译为水池,但是在英文中,还有一种解释是 an organization of people or ...
  • SpringBoot 配置 okhttp3

    万次阅读 2019-06-29 14:25:25
    SpringBoot 配置 okhttp3

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,809
精华内容 1,523
关键字:

okhttp连接池配置