继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

OKHTTP异步和同步请求简单分析

PIPIONE
关注TA
已关注
手记 921
粉丝 147
获赞 701

OKHTTP异步和同步请求简单分析
OKHTTP拦截器缓存策略CacheInterceptor的简单分析
OKHTTP拦截器ConnectInterceptor的简单分析
OKHTTP拦截器CallServerInterceptor的简单分析
OKHTTP拦截器BridgeInterceptor的简单分析
OKHTTP拦截器RetryAndFollowUpInterceptor的简单分析
OKHTTP结合官网示例分析两种自定义拦截器的区别

同步请求就是执行请求的操作是阻塞式,直到 HTTP 响应返回。它对应 OKHTTP 中的 execute 方法。

异步请求就类似于非阻塞式的请求,它的执行结果一般都是通过接口回调的方式告知调用者。它对应 OKHTTP 中的 enqueue 方法。

示例代码

下面的代码演示了如何进行同步和异步请求的操作。

OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder()
        .url("http://www.qq.com")
        .build();
Call call = okHttpClient.newCall(request);//1.异步请求,通过接口回调告知用户 http 的异步执行结果call.enqueue(new Callback() {    @Override
    public void onFailure(Call call, IOException e) {
        System.out.println(e.getMessage());
    }    @Override
    public void onResponse(Call call, Response response) throws IOException {        if (response.isSuccessful()) {
            System.out.println(response.body().string());
        }
    }
});//2.同步请求//Response response = call.execute();//if (response.isSuccessful()) {//    System.out.println(response.body().string());//}

异步请求的基本原理

OKHTTP异步任务执行图解.png

Call

负责准备去执行一个 request 请求,一个 call 只能负责去执行一个请求,不能被执行两次。因为 OkHttpClient 是实现了 Call.Factory 因此它具备创建 Call 对象的功能,内部创建的就是 RealCall 对象。Call 是封装 request 的,它表示一个可以执行的请求。

Call 的实现类 RealCall

因为 Call 是接口,内部定义了同步与异步的请求,以及取消请求等操作,这些操作是由 RealCall 真正去实现的。

在 RealCall 中关键的几个属性:

  • client 就是我们在外界创建的 OkHttpClient 对象,通过这个 client 就是调用 Dispatcher 去分发请求任务。

  • executed 它是 boolean 类型,上面介绍 Call 时已经说明了,一个 Call 只能被执行一次,在内部就是通过这个属性进行判断的。

  • originalRequest Request 对象,它就是通过 okHttpClient.newCall(request) 传入的 Request 对象,这个 Request 在整个网络请求起到非常重要的作用,它会被传入到各个 Interceptor 中去。例如用户创建的 request 对象,只是简单的设置了 url ,method,requestBody 等参数,但是想要发送一个网络请求这样简单地配置还是不够的,系统提供的拦截器 BridgeInterceptor 就是负责做这件事,它会为该请求添加请求头,例如 gzip,cookie,content-length 等,简单说它会将用户创建的 request 添加一些参数从而使其更加符合向网络请求的 request 。其他拦截器的功能也是对 request 进行操作,具体看源码。

Dispatcher 相关知识点

异步任务分发器,它会内部指定线程池去执行异步任务,并在执行完毕之后提供 finish 方法结束异步请求之后从等待队列中获取下一个满足条件的异步任务去执行。

1、在 Dispatcher 有几个比较重要的属性,这几个属性会影响异步请求的执行。

  • int maxRequests = 64 会去指定并发 call 的最大个数。

  • maxRequestsPerHost = 5: 每个主机最大请求数为5 ,也就是最多 5 个call公用一个 host。这个host 就是在 RealCall 中通过 originalRequest.url().host() 去获取的,例如 www.baidu.com

  • executorService 就是执行异步任务的线程池,在内部中已经指定好了线程池,当然也可以在 Dispacther 中通过构造方法去指定一个线程池。

  • Deque<AsyncCall> readyAsyncCalls 表示在队列中已经准备好的请求。

  • Deque<AsyncCall> runningAsyncCalls 正在执行的异步请求,包括已经取消的请求(还没有执行finish操作的请求。)

  • Deque<RealCall> runningSyncCalls 正在运行的同步请求,包括已经取消的请求(还没有执行finish操作的请求。)

2、关于 Dispatcher 的功能在下面的异步和同步请求中我们再一一探索。

Call 的实现者 RealCall

它具备有异步和同步请求,还有取消请求的功能,它内部有一个 AsyncCall 内部类,在 Dispatcher 中分发的异步请求任务就是 AsyncCall 。这里分发的任务指的是异步任务,而不是同步任务。AsyncCall 就是用表示一个异步任务的,在 Dispatcher 内部有维护了两个队列来存储 AsyncCall,分别是 readyAsyncCalls 和
runningAsyncCalls 它们分别表示准备要执行的 AsyncCall 队列和正在执行的 AsycnCall 队列。当然还有一个 runningSyncCalls 这个队列,但是它适用于存放 RealCall ,也就是用于存储同步请求的任务。

  • 在 RealCall 实现异步请求 call.enqueue(new Callback())

@Override public void enqueue(Callback responseCallback) {  synchronized (this) {    //检测该 call 是否被执行过了,如果已经执行了,那么就抛出异常
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  } //关键代码:将 AsycnCall 添加到队列中。将任务交给 Dispatcher 去执行。
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
  • 使用 Dispatcher 去分发一个异步任务

合理性的校验操作,我们在介绍 Dispacther 的相关属性时已经说明,在 OKHTTP 中正在执行的请求不能超过 64 个,并且同一个主机不能超过 5 请求,当满足这两个条件,即可将任务添加到正在执行的队列 runningAsyncCalls 中,并且通知线程池安排线程去执行这个异步任务,否则就会被添加到等待队列中 readyAsyncCalls。

synchronized void enqueue(AsyncCall call) {  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {    //当正在执行的请求小于64个&&该 call 对应的主机少于5个 Call 时
    //将任务添加到 runningAsycnCalls 中,标记为正在执行的任务。
    runningAsyncCalls.add(call);    //在线程池中执行这个任务。
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}

真正的异步执行者 AsyncCall

在前面提到了 AsyncCall 表示的是一个异步任务,在使用 Dispatcher  会将 AsyncCall 交给指定的线程去执行,而 AsyncCall 是 NamedRunnable 的子类,因此它也具备 Runnble 的特性,换句话说,在线程池中执行的任务就是 AsyncCall 了。

当线程池执行这个异步任务时,那么该 Runnable 的 run 方法就会被执行,我们查阅了源码,在 run 方法内部会去调用 AsyncCall 的 execute 方法。

@Override protected void execute() {  boolean signalledCallback = false;  try {
    Response response = getResponseWithInterceptorChain();    if (retryAndFollowUpInterceptor.isCanceled()) {
      signalledCallback = true;
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
      signalledCallback = true;
      responseCallback.onResponse(RealCall.this, response);
    }
  } catch (IOException e) {    if (signalledCallback) {      // Do not signal the callback twice!
      Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
    client.dispatcher().finished(this);
  }
}

总结该方法中它主要做了这 3 件事:

  • 得到 HTTP 请求的响应 Response :Response response = getResponseWithInterceptorChain();得到一个 response 响应。(这个是一个递归的调用过程,具体在其他博客中再分析具体实现。)

  • 给调用进行接口回调异步任务执行的结果:responseCallback.onResponse 或者 responseCallback.onFailure

  • 结束该请求,并且执行下一个等待的异步任务:client.dispatcher().finished(this);

对于 AsyncCall 中所做的这 3 步中,前面两步都比较好理解,下面主要看看它是如何结束一个请求的并且开启下一个异步请求的?

我们在前面介绍 Dispacther 已经了解了它的作用,这里再次强调一下,它是负责去分发一个异步任务给指定的线程池去执行,并且可以在执行完毕之后去等待队列中获取下一个请求去执行。

在 AsyncCall 中的 execute 中执行一个异步请求,注意在 finally 块内部调用了 client.dispatcher().finished(this);它的作用是通知 Dispatcher 我的任务执行完毕了,你可以将我从集合中移除了,开启下一个异步任务吧。下面就是 finish 的源码:

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {  int runningCallsCount;
  Runnable idleCallback;  synchronized (this) {    if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");    if (promoteCalls) promoteCalls();
    runningCallsCount = runningCallsCount();
    idleCallback = this.idleCallback;
  }  if (runningCallsCount == 0 && idleCallback != null) {
    idleCallback.run();
  }
}

总结该方法中它主要做了这 3 件事:

  • 从 calls 中移除该 AysyncCall 对象,而这个 calls 就是 Dispatcher 中的 runningAsyncCalls。

  • promoteCalls() 调用该方法就可以实现从等待队列中取出下一个异步任务去执行。

  • idleCallback.run(); 没有正在执行的任务时,那么就回调这个接口,该回调接口需要通过 setIdleCallback 方法传递进来。它可以通知当没有任务正在执行时,通知外界。runningCallsCount  这个是同步执行的任务数和异步执行任务数之和。

通过 promoteCalls() 去执行下一个异步任务

该方法是用于在等待队列中获取下一个异步任务去执行。在内部会还是会对 Dispatcher 内部的几个属性进行判断,例如对正在执行的请求数量是否超过了 64 个,还有遍历等待队列里的所有的 AsyncCall ,每遍历出一个 AsycnCall 都校验它的主机是否有超过 5 个正在执行的异步请求在使用了,在满足条件的情况下,就马上会线程池去执行这个任务,以此类推,任务就这样一个一个的被执行。

private void promoteCalls() {  if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
  if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
  for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
    AsyncCall call = i.next();    if (runningCallsForHost(call) < maxRequestsPerHost) {      //满足下一个要执行的任务的要求。
      i.remove();      //添加到正在请求队列中
      runningAsyncCalls.add(call);      //由线程池去执行这个任务。
      executorService().execute(call);
    }    //超过了 64 个请求那么就直接 return
    if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
  }
}

使用 RealCall 实现的同步请求

上面所描述的都是异步请求,现在来看看同步请求。

同步请求调用的是 execute 方法,在内部会调用 client.dispatcher().executed(this); 方法,进去看源码可知道它实际就是将 RealCall 添加到  Dispatcher 的 runningSyncCalls 中,表示当前正在执行的同步队列中。在这里使用 Dispacther 的中 execute 仅仅只是将其添加到集合中而已,没有作别的操作,而真正执行同步任务的核心代码是 getResponseWithInterceptorChain(); ,该方法负责去网络请求,并且得到一个响应,具体内部怎么实现日后再分析。在最后的 finally 代码块执行的功能跟异步任务一样,也是通过 Dispatcher 去 finish 该请求。

在 finish 中虽然同步和异步执行的方法是一样的,但是执行流程并不一样,异步任务需要通过 promoteCalls 去执行下一个异步任务,而同步请求是不需要的,这个的判断标记就 private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {...}  就是第三个参数,当该 promoteCalls 为 false 表示同步请求,true 表示异步请求,其他操作都是和异步请求是一样的。

@Override public Response execute() throws IOException {  synchronized (this) {    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }  try {
    client.dispatcher().executed(this);
    Response result = getResponseWithInterceptorChain();    if (result == null) throw new IOException("Canceled");    return result;
  } finally {
    client.dispatcher().finished(this);
  }
}



作者:六号表哥
链接:https://www.jianshu.com/p/3214ef86a52d


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP