Okhttp解析(一)请求的分发,拦截

Okhttp特性

Okhttp是一个高效的,请求速度更快,更节省流量的http库。拥有以下特性。

  1. 支持SPDY和http2,对同一服务器的所有请求共享同一个socket。
  2. 拥有自动维护的socket连接池,减少握手次数。
  3. socket自动选择最好路线,并支持自动重连。
  4. 拥有队列线程池,轻松写并发。
  5. 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)。
  6. 无缝的支持GZIP来减少数据流量。
  7. 支持基于Headers的缓存策略,缓存响应数据来减少重复的网络请求。
  8. 支持服务器多IP重连,支持从常用的连接问题中自动恢复,还处理了代理服务器问题和SSL握手失败问题。
注:SPDY是什么?
SPDY(读作“SPeeDY”)是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。
SPDY并不是一种用于替代HTTP的协议,而是对HTTP协议的增强。新协议的功能包括数据流的多路复用、请求优先级以及HTTP报头压缩。
谷歌表示,引入SPDY协议后,在实验室测试中页面加载速度比原先快64%。

OkHttpClient分析版本

Okhttp3(3.2.0版本)

OkHttpClient的简单调用

Get请求

public void doGet(String url){
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder().url(url).build();
    Response response = client.newCall(request).execute();
    Log.i("输出:" + response.body().string());
}

Post请求

public void doPost(String url){
    MediaType json = MediaType.parse("application/json; charset=utf-8")
    RequestBody body = RequestBody.create(JSON, json);
    
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder().url(url).post(body).build();
    Response response = client.newCall(request).execute();
    Log.i("输出:" + response.body().string());
}

OkHttpClient介绍

OkHttpClient顾名思义是一个http请求的客户端,实现了Call.Factory接口,提供newCall方法用于创建一个请求调用Call。

@Override public Call newCall(Request request) {
    return new RealCall(this, request);
}

OkHttpClient包含很多模块,包括Dispatcher(请求分发器),ProxySelector(代理服务器选择),InternalCache(内部缓存)等其他的模块,由它对外提供这些模块的访问,是典型的外观模式,同时OkHttpClient设计为建造者模式,提供Builder方便为不同模块进行配置。

Request,Response,Call,RealCall,AsynCall介绍

  • Request作为请求信息的封装,内部包含了请求url,请求方法method,请求头headers,请求体RequestBody,tag标签等。
  • Response作为相应信息的封装,内部包含对应的请求信息request,http协议protocol,响应码code,响应头headers,响应消息message,响应体ResponseBody等。
  • Call作为一个请求的接口,提供获取对应请求信息息request,执行请求execute,异步请求入队enqueue,取消请求cancel等方法的定义。
  • RealCall是Call请求的具体实现,实现了同步请求执行,异步请求入队,请求取消等操作。同时内部提供ApplicationInterceptorChain负责请求和响应的拦截处理。
  • AsynCall是一个Runnable异步请求任务,分发器Dispatcher负责管理这些异步请求,在可请求数量满足的情况下会交给线程池执行它的execute方法。

Dispatcher请求调用分发器

Dispatcher是请求Call的分发器,用于管理请求的等待,执行,取消。分为同步请求RealCall和异步请求AsyncCall的管理。

  • 针对同步请求,用runningSyncCalls队列记录正在执行的同步请求。
  • 针对异步请求,用readyAsyncCalls(待执行的异步请求队列)和runningAsyncCalls(正在执行的异步请求队列)记录异步请求。
  • 内部提供线程池,负责执行异步请求,查看executorService()。
public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
}

这个线程池,我们看参数解析:

1. 参数0,代表核心线程的数量,即线程池中最少线程时的数量(当所有线程都空闲时),为0的话,说明线程空闲时,不保留任何线程,做到线程低占用。
2. 参数Integer.MAX_VALUE,代表最大线程数,即线程池中最多线程时的数量,为Integer.MAX_VALUE的话,说明可以开启无限多的线程进行工作,线程工作完了再关闭。
3. 参数60,和参数TimeUnit.SECONDS,表示当线程池的数量比核心线程的数量大时,等待60秒之后就会去关闭空闲线程,使总的线程数量不会大于核心线程数量。
4. 参数new SynchronousQueue<Runnable>(), 代表这个线程等待队列是同步队列,当有一个线程进来时,就同时会有一个线程出去,也就是说线程不会停留在其中,一进入就立马出去执行,这种方式在高频请求时是很合适的。
5. 参数Util.threadFactory("OkHttp Dispatcher", false),表示提供一个线程工厂,用于创建新的线程。

这个线程池的设计,是需要时可以创建无限多的线程,不需要时不保留任何线程,空闲线程60秒后如果还是空闲就会回收,以保证高阻塞低占用的使用。

  • 设置有maxRequests(最大异步请求的数量)和maxRequestsPerHost(单个服务器的最大异步请求数量),这是为了限制同时执行的总的请求数量和针对同一个服务器访问的请求数量,如果有超过了这两个的限制,就将异步请求AsyncCall添加到readyAsyncCalls(待执行的异步请求队列)去等待执行。这两个参数可以配置。
  • enqueue方法,AsyncCall入队操作
synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      //未超过限制,执行加入到线程池执行异步请求
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      //超过限制,将行异步请求加入等待队列,等待执行
      readyAsyncCalls.add(call);
    }
}
  • finished方法,标记当前AsyncCall已经完成,这时会考虑从异步等待队列取出请求去执行。
//标记请求完成
synchronized void finished(AsyncCall call) {
    if (!runningAsyncCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
    promoteCalls();
}

//从异步等待队列取出请求去执行,同时记录到正在执行的异步队列中
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);
      }
    
      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
}
  • cancelAll方法,取消所有请求,包括所有同步请求,所有正在执行的异步请求,所有等待执行的异步请求。
public synchronized void cancelAll() {
    for (AsyncCall call : readyAsyncCalls) {
      call.cancel();
    }
    
    for (AsyncCall call : runningAsyncCalls) {
      call.cancel();
    }
    
    for (RealCall call : runningSyncCalls) {
      call.cancel();
    }
}

RealCall 真正执行请求调用的地方

RealCall是真正执行请求调用的入口,无论是同步请求的execute,还是异步请求的enqueue(由Dispatcher进行分发),最终都会到RealCall的getResponseWithInterceptorChain。

QQ截图20171102182751.png

getResponseWithInterceptorChain里创建了一个ApplicationInterceptorChain拦截链来处理当前RealCall中的Request请求数据。接下来讲讲Okhttp中拦截器是这样的一种运行模式。

Interceptors拦截器原理

Interceptors拦截器采用了责任链模式一层一层的处理请求响应信息。这里我们从同步请求去分析添加了HttpLoggingInterceptor日志打印拦截器的运行原理。

final class RealCall implements Call {

  //执行请求,这里创建了ApplicationInterceptorChain拦截链,负责对所有拦截器进行调用,调用proceed开始处理拦截
  private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
    Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
    return chain.proceed(originalRequest);
  }

  class ApplicationInterceptorChain implements Interceptor.Chain {
    private final int index;
    private final Request request;
    private final boolean forWebSocket;

    ApplicationInterceptorChain(int index, Request request, boolean forWebSocket) {
      this.index = index;
      this.request = request;
      this.forWebSocket = forWebSocket;
    }

    @Override public Connection connection() {
      return null;
    }

    @Override public Request request() {
      return request;
    }

    //这里遍历拦截器,通过index在拦截链中找到对应的拦截器,然后调用intercept进行拦截处理,返回加工后的Response响应信息
    @Override public Response proceed(Request request) throws IOException {
      // If there's another interceptor in the chain, call that.
      if (index < client.interceptors().size()) {
        Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
        Interceptor interceptor = client.interceptors().get(index);
        Response interceptedResponse = interceptor.intercept(chain);

        if (interceptedResponse == null) {
          throw new NullPointerException("application interceptor " + interceptor
              + " returned null");
        }

        return interceptedResponse;
      }

      //当所有拦截器都遍历处理后,开始执行真正请求,返回Response,这里才是真正产生Response响应的地方。
      // No more interceptors. Do HTTP.
      return getResponse(request, forWebSocket);
    }
  }

}

看了以上代码之后,我们知道是通过index一个一个遍历拦截链的拦截器,去拦截处理,这是一个递归的过程,当所有的拦截器处理了Request请求信息之后(也就是真正请求之前的预处理,比如打日志,修改请求头什么的),才真正的交给网络请求引擎去执行请求,返回响应信息,然后又按相反顺序对Response响应信息进行拦截处理(也就是对响应信息的再加工,比如打印响应信息等)。那么我们分析HttpLoggingInterceptor日志打印拦截器会更加的的清晰这个过程。

public final class HttpLoggingInterceptor implements Interceptor {

    //拦截链上的
    @Override public Response intercept(Chain chain) throws IOException {
    Level level = this.level;

    //获取请求信息
    Request request = chain.request();
    
    //如果不打印日志,那就直接调用chain.proceed执行下一个拦截操作,不做其他操作。
    if (level == Level.NONE) {
      return chain.proceed(request);
    }

    //这部分是打印请求信息,对请求进行预处理(这里可以对请求信息进行修改或做其他操作)
    ---------------------------------------
    boolean logBody = level == Level.BODY;
    boolean logHeaders = logBody || level == Level.HEADERS;

    RequestBody requestBody = request.body();
    boolean hasRequestBody = requestBody != null;

    Connection connection = chain.connection();
    Protocol protocol = connection != null ? connection.protocol() : Protocol.HTTP_1_1;
    String requestStartMessage = "--> " + request.method() + ' ' + request.url() + ' ' + protocol;
    if (!logHeaders && hasRequestBody) {
      requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
    }
    logger.log(requestStartMessage);

    if (logHeaders) {
      if (hasRequestBody) {
        // Request body headers are only present when installed as a network interceptor. Force
        // them to be included (when available) so there values are known.
        if (requestBody.contentType() != null) {
          logger.log("Content-Type: " + requestBody.contentType());
        }
        if (requestBody.contentLength() != -1) {
          logger.log("Content-Length: " + requestBody.contentLength());
        }
      }

      Headers headers = request.headers();
      for (int i = 0, count = headers.size(); i < count; i++) {
        String name = headers.name(i);
        // Skip headers from the request body as they are explicitly logged above.
        if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
          logger.log(name + ": " + headers.value(i));
        }
      }

      if (!logBody || !hasRequestBody) {
        logger.log("--> END " + request.method());
      } else if (bodyEncoded(request.headers())) {
        logger.log("--> END " + request.method() + " (encoded body omitted)");
      } else {
        Buffer buffer = new Buffer();
        requestBody.writeTo(buffer);

        Charset charset = UTF8;
        MediaType contentType = requestBody.contentType();
        if (contentType != null) {
          charset = contentType.charset(UTF8);
        }

        logger.log("");
        logger.log(buffer.readString(charset));

        logger.log("--> END " + request.method()
            + " (" + requestBody.contentLength() + "-byte body)");
      }
    }
    //请求预处理完成
    ----------------------------------
    
    //这里调用chain.proceed执行下一个拦截操作,返回Response响应信息,结合ApplicationInterceptorChain的proceed方法,很容易看出是一个责任链的递归调用模式
    long startNs = System.nanoTime();
    Response response = chain.proceed(request);
    long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);

    //接下来打印响应信息,对Response响应进行再加工(这里可以对响应信息进行修改或做其他操作)
    ---------------------------------------
    ResponseBody responseBody = response.body();
    long contentLength = responseBody.contentLength();
    String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
    logger.log("<-- " + response.code() + ' ' + response.message() + ' '
        + response.request().url() + " (" + tookMs + "ms" + (!logHeaders ? ", "
        + bodySize + " body" : "") + ')');

    if (logHeaders) {
      Headers headers = response.headers();
      for (int i = 0, count = headers.size(); i < count; i++) {
        logger.log(headers.name(i) + ": " + headers.value(i));
      }

      if (!logBody || !HttpEngine.hasBody(response)) {
        logger.log("<-- END HTTP");
      } else if (bodyEncoded(response.headers())) {
        logger.log("<-- END HTTP (encoded body omitted)");
      } else {
        BufferedSource source = responseBody.source();
        source.request(Long.MAX_VALUE); // Buffer the entire body.
        Buffer buffer = source.buffer();

        Charset charset = UTF8;
        MediaType contentType = responseBody.contentType();
        if (contentType != null) {
          try {
            charset = contentType.charset(UTF8);
          } catch (UnsupportedCharsetException e) {
            logger.log("");
            logger.log("Couldn't decode the response body; charset is likely malformed.");
            logger.log("<-- END HTTP");

            return response;
          }
        }

        if (contentLength != 0) {
          logger.log("");
          logger.log(buffer.clone().readString(charset));
        }

        logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
      }
    }
    //响应再加工完成
    ----------------------------------

    //这里返回响应信息
    return response;
  }
}

明白拦截器的运行模式之后,我们知道真正的请求是在RealCall的getResponse方法中开始的。

/**
* Performs the request and returns the response. May return null if this call was canceled.
*/
Response getResponse(Request request, boolean forWebSocket) throws IOException {
    //这里负责执行请求,然后返回响应数据
    ...
}

具体请求我们下节分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容