三.OkHttp

1.OkHttp介绍

1.1 Retrofit

Retrofit对我们的网络请求框架OkHttp进行了一次封装,从而可以实现一些别的功能,比如:设置BaseUrl、添加Gson解析、添加RxJava线程间切换、添加OkHttp网络请求客户端.

1.2 Http1.1与Http2.0区别

  • Http2.0特点:

    1. 头部压缩:
      对请求头进行压缩,减小数据大小
    2. ServerPush:
      服务器可以主动给客户端推送数据,正常请求下是客户端向服务器发送数据.
    3. 多路复用:
      由于客户端每次向服务器发送数据都需要经历如下流程:
      客户端发起请求->DNS域名解析->TCP/IP(建立Socket连接:3次握手和4次挥手)->服务器响应数据
      如果客户端向同一个主机请求数据,每次都需要经历Socket通信的3次握手操作,所以为了提高效率,在进行了一次Socket通信3次握手后,就不再进行这个重复操作了,直接复用用第一次Socket通信3次握手.
      多路复用这个功能是在Http1.1的时候引入的keep-alive机制,实现了不用重复建立Socket连接,提高了网络请求效率
    4. 支持并行请求
      因为有了二进制数据帧,里面包含了顺序标识,我们传输的数据就会带上这个顺序标识,因为无需再保证请求数据的有序性,所以就支持并行请求.
  • Http1.1特点:

    1. keep-alive:
      客户端在向服务器发起请求时,可以在请求头中设置Connectionkeep-alive,请求与服务器保持长连接,同时服务器响应的数据也会包含有Connection的数据,服务器同意与客户端保持长连接返回keep-alive,服务器不同意保持长链接返回close.
    2. 串行有序的请求:
      必须在上一次请求得到响应数据后,才能够发起第二次请求;因为每次网络请求发送的数据是文本形式,多次网络请求是分批次发送,为了保证请求与响应的数据正确性,必须要上一次请求服务器响应数据后,才能进行下一次网络请求.

1.3 OkHttp的特点

  1. 支持Http2.0并允许对同一个主机的所有请求共享同一个套接字;
    • 通过在Http的请求头中,设置了与服务器保持长连接,通过设置Connection值为keep-alive,减少请求时间.
  2. 如果不是Http2.0,则通过连接池,减少了请求延迟;
    • 建立一个连接池,连接池中存储了对各个服务器的连接对象(Deque<RealConnection>),下一次请求的服务器连接存在于连接池,则不需要重新去建立Socket连接,减少请求时间.
  3. 默认请求GZip压缩数据;
    • 允许服务器将返回的数据进行压缩,可以减少响应数据大小.
  4. 响应缓存,避免重复请求网络数据
    • 默认缓存是关闭的,同时只支持Get请求数据的缓存,可以避免用户重复向服务器请求相同的数据.
    • 可已通过如下方式来配置缓存:
      OkHttpClient.cache(Cache(缓存路径,缓存大小最大值)).build();

2.OkHttp的基本使用与请求流程

2.1 OkHttp的基本使用

使用举例,代码入下:

//缓存路径
var directory = File(cacheDir.absolutePath + "/OkHttpCahe")
//缓存最大值
var maxSize = 100 * 1024 * 1024L
//日志拦截 自定义日志拦截器
val loggingInterceptor = MyInterceptor()

//OkHttpClient对象创建,可以直接new,也可以使用建造者设计模式
var okHttpClient = OkHttpClient.Builder()
        .cache(Cache(directory, maxSize))//设置缓存
        .callTimeout(10, TimeUnit.SECONDS)//请求超时时间
        .readTimeout(10, TimeUnit.SECONDS)//读取数据超时时间
        .writeTimeout(10, TimeUnit.SECONDS)//写入数据超时时间
        .connectTimeout(10, TimeUnit.SECONDS)//连接数据超时时间
        .addInterceptor(loggingInterceptor)//添加自定义拦截,比如:请求参数、返回数据等
        .build()
//网络请求参数配置
var request = Request.Builder()
        .url("https://www.wanandroid.com/banner/json")//请求地址
        .get()//设置请求方式为Get
        .cacheControl(CacheControl.FORCE_CACHE)
        .build()
//创建一次网络请求,返回RealCall对象       
var call = okHttpClient.newCall(request)
//执行网络请求,execute是同步请求,enqueue是异步请求
val response = call.execute()
Log.e("TangKunLog", "" + response.isSuccessful)
response.close()

2.2 OkHttp请求流程

OkHttp调用流程如下:
OkHttpClient(通过Builder生成)->Request(通过Builder生成)->Call(通过okHttpClient.newCall(request)生成)->call.execute(同步请求)/call.enqueue(异步同步)->Dispatcher->Interceptors->Response

在执行同步请求或者异步请求后,服务器返回响应数据之前,还会经历分发器和拦截器两个阶段.所有的逻辑大部分集中在拦截器中,但是在进入拦截器之前,还需要依靠分发器来调配请求任务.

  • 分发器:内部维护队列与线程池,完成请求调配
  • 拦截器:完成整个请求过程

2.3 分发器分发流程

首先,okHttpClient.newCall(request)在源码中会创建一个RealCall并返回。因此,我们在执行异步请求call.equeue方法时,其实是执行的RealCall.enqueue方法,这个方法中就会通过分发器Dispatcher来执行enqueue方法。

okHttpClient.newCall(request)方法会返回RealCall对象,作为Call接口的实现类,OkHttpClient.java中源码如下:

public Call newCall(Request request) {
    
    
  return RealCall.newRealCall(this, request, false);
}

由于上面创建了Call接口的实现类RealCall,所以这里其实是调用的RealCall.enqueue()方法,RealCall.javaenqueue方法源码如下:

public void enqueue(Callback responseCallback) {
    
    
  //省略非核心代码

  //这里会通过分发器Diapatcher来调用enqueue方法,
  //分发器也会以同样的方式调用executed方法
  //这里AsyncCall参数是一个Runnable对象
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

由于上面最终是通过分发器Dispatcher调用的enqueue方法,所以我们查看Dispatcher.java中的相关源码:

//等待队列 异步请求
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

//运行队列 异步请求
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

//运行队列 同步请求
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

//将当前任务添加到等待队里,并判断等待队列任务和运行任务队列中是否有相同的执行域名
//如果有相同的执行域名,将该域名下执行的任务数量赋值给当前任务
void enqueue(AsyncCall call) {
    
    
  synchronized (this) {
    
    
    //这里会将这一次请求添加到等待队列中去
    readyAsyncCalls.add(call);

    // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
    // the same host.
    if (!call.get().forWebSocket) {
    
    
      //从等待队列和运行队列中查找是否存在有相同域名的任务
      AsyncCall existingCall = findExistingCallWithHost(call.host());
      //查找到相同域名的任务,则将已存在任务中的callsPerHost变量,赋值给当前的任务中的callsPerHost变量
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
    }
  }
  //核心代码:任务调度和执行任务
  promoteAndExecute();
}

//将等待队列任务向运行队列任务推进,并执行进入到运行队列的任务
private boolean promoteAndExecute() {
    
    
  assert (!Thread.holdsLock(this));

  List<AsyncCall> executableCalls = new ArrayList<>();
  boolean isRunning;
  synchronized (this) {
    
    
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
    
    
      AsyncCall asyncCall = i.next();

      //判断运行队列大小是否大于等于64,也就是说正在运行的异步请求数量不能超过64个
      if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
      //判断每个域名的执行任务数量是否大于等于5,也就是说同一个域名最多有5个异步任务在执行
      if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

      //将当前任务从等待队列中移除,因为稍后要把他添加到运行队列中去
      i.remove();
      //当前域名执行的任务数量加1
      asyncCall.callsPerHost().incrementAndGet();
      //统计从等待队列添加到运行队列的任务
      executableCalls.add(asyncCall);
      //将任务从等待队列添加到运行队列
      runningAsyncCalls.add(asyncCall);
    }
    isRunning = runningCallsCount() > 0;
  }

  //通过线程池ThreadPoolExecutor,来执行从等待队列添加到运行队列的任务,
  //所以会回调当前任务Runnable的run方法
  for (int i = 0, size = executableCalls.size(); i < size; i++) {
    
    
    AsyncCall asyncCall = executableCalls.get(i);
    //executorService()方法会返回创建的ThreadPoolExecutor对象
    asyncCall.executeOn(executorService());
  }

  return isRunning;
}

从上面源码中可以看到,异步请求存在两个队列:等待队列(readyAsyncCalls)和运行队列(runningAsyncCalls);而同步请求只有一个队列:运行队列(runningSyncCalls)。
当同时运行的异步任务没有超过64个,并且每一个域名同时执行任务数量不超过5个时,将等待队列中的任务添加到运行队列,同时统计从等待队列添加到运行队列的任务,通过线程池来执行。

从上面的源码中可以总结出,是通过线程池来执行的从等待队列进入到运行队列的任务,线程池对应ThreadPoolExecutor,而每一个执行的任务对应AsyncCall实现了Runnable,通过asyncCall.executeOn(executorService())方法来执行,接下来会执行这行代码RealCall.AsyncCall.java中的executorService.execute(this)这行代码来执行任务Runnable,最终会执行到RealCall.AsyncCall.javarun方法中去,当前任务没有实现run方法,由他的父类实现,父类中重写了execute抽象方法,在子类中实现该方法业务。

分析RealCall.AsyncCall.java中的execute方法源码:

@Override protected void execute() {
    
    
  //省略了非核心代码
  try {
    
    
  	//核心代码
    //通过拦截器发送请求并响应数据
    Response response = getResponseWithInterceptorChain();
    //核心代码
    //通过接口回调成功的响应数据
    responseCallback.onResponse(RealCall.this, response);
  } catch (IOException e) {
    
    
      //通过接口回调失败的响应数据
      responseCallback.onFailure(RealCall.this, e);
  } catch (Throwable t) {
    
    
    if (!signalledCallback) {
    
    
      //通过接口回调成功的响应数据
      responseCallback.onFailure(RealCall.this, canceledException);
    }
  } finally {
    
    
    //核心代码 
    //通过分发器执行任务完成方法,会将当前任务从运行队列中移除,并将当前域名执行任务数量减1
    client.dispatcher().finished(this);
  }
}

从上面代码中可以看出,通过线程池执行执行任务,会执行任务的run方法,经过各种转换,最终进入到当前任务RealCall.AsyncCall.execute()方法,这个方法中会将拦截器处理的结果返回,通过接口回调返回给用户,用户就可以知道这次请求是否成功或者失败服务器返回的数据。最终,还会通过分发器处理任务执行完成的逻辑,将完成的任务域名下任务数量减去1,同时还要将完成任务从运行队列中移除,同时再次执行从等待队列添加任务到运行队列并执行的操作。
分析Dispatcher.java中代码如下:

void finished(AsyncCall call) {
    
    
  //将完成任务域名下的任务数量减去1
  call.callsPerHost().decrementAndGet();
  finished(runningAsyncCalls, call);
}

private <T> void finished(Deque<T> calls, T call) {
    
    
  Runnable idleCallback;
  synchronized (this) {
    
    
    //核心代码
    //将执行完成的任务从运行队列中移除
    if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
    idleCallback = this.idleCallback;
  }
  //核心代码
  //由于运行队列中有任务执行完成,所以又会去遍历等待队列,从中取出任务添加到运行队列,并执行这些任务,这是一个循环的操作
  boolean isRunning = promoteAndExecute();

  if (!isRunning && idleCallback != null) {
    
    
    idleCallback.run();
  }
}

分发器:异步请求工作流程
AsyncCall->Dispathcer->readAsyncCalls->promoteAndExecute(判断同时运行的异步任务是否大于等于64,并且判断同一个域名执行的任务数量是否大于等于5)->runningAsyncCalls->ThreadPool->finished(从runningAsyncCalls队列中移除)->promoteAndExecute->readAsyncCall(这是一个循环操作,又会去遍历等待队列)

总结:
1.分发器完成了哪些功能?
首先,将我们的请求任务添加到等待队列,然后遍历等待队列和运行队列中是否有和当前请求任务相同的域名,有则将查询到的任务域名下执行的任务数量赋值给当前任务中的这个变量,然后遍历等待队列,将等待队列中的任务添加到运行队列(前提是同时运行的任务数量不大于64个并且同一个域名下执行的任务数量不超过5个),然后遍历从等待队列添加到运行队列的任务列表,利用线程池(ThreadPoolExecutor)去执行这些任务,执行结果中会通过拦截器返回,当任务执行完成后,会将该任务对应域名下的任务数量减去1,同时将该任务从运行队列中移除,然后循环执行从等待队列取出任务到运行队列执行的流程,直至没有任务需要执行为止。

2.扩展知识:
异步任务最多可以同时执行64个,而同一个域名执行的任务数量最多可以有5个.

2.4 拦截器拦截流程

在执行运行队列中的某个任务时(任务:Runnale),就会进入到RealCall.execute()方法,该方法会通过拦截器返回任务的执行结果,代码如下:
Response response = getResponseWithInterceptorChain();

拦截器源码分析,类名RealCall.java

Response getResponseWithInterceptorChain() throws IOException {
    
    
  // 构建一个完整的拦截器堆栈
  List<Interceptor> interceptors = new ArrayList<>();
  interceptors.addAll(client.interceptors());//用户自定义拦截器
  interceptors.add(new RetryAndFollowUpInterceptor(client));//重试重定向拦截器
  interceptors.add(new BridgeInterceptor(client.cookieJar()));//桥接拦截器,处理header和body
  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());

  boolean calledNoMoreExchanges = false;
  try {
    
    
    Response response = chain.proceed(originalRequest);
    if (transmitter.isCanceled()) {
    
    
      closeQuietly(response);
      throw new IOException("Canceled");
    }
    return response;
  } catch (IOException e) {
    
    
    calledNoMoreExchanges = true;
    throw transmitter.noMoreExchanges(e);
  } finally {
    
    
    if (!calledNoMoreExchanges) {
    
    
      transmitter.noMoreExchanges(null);
    }
  }
} 

拦截器执行流程:
采用了责任链的设计模式,我们的请求会按顺序经历每一个拦截器直到访问服务器拦截器。当服务器响应数据的时候,又会将数据从访问服务器拦截器依次返回到重试重定向拦截器,最终将响应的数据返回给这个方法。

拦截器作用:

  1. 重试重定向拦截器 RetryAndFollowUpInterceptor
    • 重试重定向拦截器在交给下一个拦截器之前,负责判断用户是否取消了请求;在获得了服务器响应数据之后,会根据判断是否需要重定向,如果满足条件那么就会重启执行所有的拦截器。
  2. 桥接拦截器 BridgeInterceptor
    • 桥接拦截器在交给下一个拦截器之前,负责将Http协议必备的请求头加入其中(如:Host),并添加一些默认的行为(如:GZip压缩);在获得了结果之后,调用保存的cookie接口并解析GZip数据。
      (Content-Type:请求体类型、Cookie:身份识别、Host:主机站点、User-Agent:用户信息、Accept-Encoding:gzip:接收响应体使用GZip压缩等)
  3. 缓存拦截器 CacheInterceptor
    • 缓存拦截器在交给下一个拦截器之前,判断是否使用缓存数据;获取到结果之后,判断是否对结果进行缓存。
  4. 连接拦截器 ConnectInterceptor
    • 连接拦截器在交给下一个拦截器之前,负责找到或建立一个连接,并获取对应的socket流;在获得结果后不进行额外的处理。
  5. 请求服务器拦截器 CallServerInterceptor
    • 请求服务器拦截器才是与服务器真正进行通信,向服务器发送数据,然后解析从服务器读取的响应数据。

总结:
因此,拦截器使用到了责任链模式,是一个从上往下把请求交给服务器,然后从下往上将服务器的数据返回一个流程。

面试题

1.OkHttp请求流程到底是什么样的?

  • 首先我们需要通过Builder模式创建OkHttpClient对象,这个对象包含了如下功能:设置超时时间、添加自定义拦截器、设置缓存路径和最大值等;
  • 然后通过Builder模式创建Request对象,这个对象包含了如下功能:设置请求url、Get/Post请求方式等功能;
  • 接着我们调用OkHttpClient.newCall(Request)方法,会返回一个实现了Call接口的实现类RealCall对象;
  • 然后我们调用RealCall的同步和异步请求方法:同步方法是execute,异步方法是enqueue,接着经历分发器和拦截器,最终返回服务器数据Response。

2.OkHttp分发器是如何工作的?

  • 同步请求execute
    • 将我们的请求任务加入到同步运行队列中去(runningSyncCalls),通过拦截器来执行这个任务并返回Response对象;任务执行完成后,将这个任务从同步运行队列中移除。
  • 异步请求enqueue
    • 创建AsyncCall对象,将AsyncCall对象添加到异步等待任务队列中去(readyAsyncCalls);
    • 然后遍历异步等待任务队列和异步运行任务队列,将和当前请求任务相同的主机名调用任务次数赋值给当前任务;
    • 然后遍历异步等待队列,将异步等待队列中的任务添加到异步运行队列中去(runningAsyncCalls),前提条件是同时运行的异步任务数量不超过64个,并且当前遍历的任务域名下同时执行的任务数量不超过5个;否则就遍历异步等待队列中的下一个任务;然后将满足条件的异步等待队列任务添加到异步运行队列,并且将这些任务的域名下执行的任务数量都加1;
    • 然后通过线程池(ThreadPoolExecutor)来执行这些从异步等待队列添加到异步运行队列的任务,在执行这些异步运行任务的时候,会通过拦截器来执行网络请求并得到服务器响应的数据,然后将这个执行的任务从异步运行队列中移除,同时将该任务域名下执行的任务数量减去1;
    • 重复上面遍历异步等待队列任务步骤,将异步等待队列任务添加到异步运行任务队列中执行的逻辑,直到所有的任务都执行完成。

3.OkHttp拦截器是如何工作的?

拦截器采用了责任链设计模式,实现了请求者与执行者解耦;依次经过重试重定向拦截器开始到请求服务器拦截器,将请求发送给服务器;然后将服务器响应的数据通过请求服务器拦截器到重试重定向拦截器,将数据返回。
自带的拦截器有5个,分别是重试重定向、桥接、缓存、连接和请求服务器拦截器。
5个拦截器分别实现了如下功能:

  • 重试重定向拦截器:主要是处理用户取消网络请求操作,然后在收到服务器返回数据后,解析出重定向url,重启所有拦截器,根据重定向url重新执行一遍。扩展:最多可以重定向20次。
  • 桥接拦截器:主要是对请求头做一些默认处理,比如:主机名Host、GZip允许服务器返回的压缩数据等。
  • 缓存拦截器:主要是将服务器返回的数据进行缓存,防止重复请求网络。
  • 连接拦截器:主要是创建网络请求连接,或者复用之前创建的网络连接对象,并获取对应的socket流。
  • 请求服务器拦截器:主要是负责向服务器发送数据,并解析从服务器响应的数据,通过流的方式。

4.自定义应用拦截器和网络拦截器区别?

  • 应用拦截器(interceptors)会添加到所有拦截器的第一位,因此,应用拦截器会最先收到发送的请求数据,最后收到从服务器返回的数据;
    网络拦截器(networkInterceptors)会添加到所有拦截器倒数第二位,也就是请求服务器拦截器的前面,因此,网络拦截器会接收到最完整的请求数据。
  • 应用拦截器一定会执行,而网络拦截器不一定会执行,因为可能使用了缓存数据。
  • 应用拦截器中打印的请求数据日志不一定完整,而网络拦截器中打印的请求数据日志是完整的,因为在应用拦截器后面的拦截器会对请求做一些处理,比如:设置请求头。

5.OkHttp是如何复用TCP连接的?

OkHttp中有一个连接池(RealConnectionPool),里面封装了一个存储连接对象的队列(Deque<RealConnection> connections),当我们的请求域名和连接池中连接对象域名相同时,就可以从连接池中取出连接对象,直接使用,而不需要重新创建连接对象。

若我们发起的网络请求请求头中Connection值为Keep-Alive,那么这个请求的连接对象会被存储到连接池中。连接池中最多可以存储5个不同域名连接对象,同时会根据LRU算法清理超过了5分钟没有被使用的连接对象。

猜你喜欢

转载自blog.csdn.net/tangkunTKTK/article/details/130861585
今日推荐