okHttp 原理解析(一)

OkHttp的优点

  • 支持HTTP/2 协议,允许连接到同一个主机地址的所有请求共享Socket
  • 在HTTP/2协议不可用的情况下,通过连接池减少请求的延迟。
  • GZip透明压缩减少传输的数据包大小。
  • 响应缓存,避免同一个重复的网络请求。

用法

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

这就是OkHttp 的简单用法,我们看到只要有 HttpClient、Request、Response

流程图

当我们 execute 的时候,我们是委托的 Dispatcherexecute

  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

也就是添加到 DispatcherrunningSyncCalls

  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  // 异步请求队列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  // 同步请求队列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

这三个都是双向队列,添加到队列后,RealCall 就要 getResponseWithInterceptorChain()

 Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //添加开发者应用层自定义的Interceptor
    interceptors.addAll(client.interceptors());
    //这个Interceptor是处理请求失败的重试,重定向    
    interceptors.add(retryAndFollowUpInterceptor);
    //这个Interceptor工作是添加一些请求的头部或其他信息
    //并对返回的Response做一些友好的处理(有一些信息你可能并不需要)
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //这个Interceptor的职责是判断缓存是否存在,读取缓存,更新缓存等等
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //这个Interceptor的职责是建立客户端和服务器的连接
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      //添加开发者自定义的网络层拦截器
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));
    //一个包裹这request的chain
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    //把chain传递到第一个Interceptor手中
    return chain.proceed(originalRequest);
  }

因为每一个interceptor的intercept方法里面都会调用chain.proceed()从而调用下一个interceptor的intercept(next)方法,这样就可以实现遍历getResponseWithInterceptorChain里面interceptors的item,实现遍历循环.

HTTP简介

OSI 七层模型 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

TCP 头部样式
TCP 头部
三次握手和四次挥手
三次握手和四次挥手

HTTPS

八次握手


HTTPS 的8次握手
  1. 发起请求。需要提供 协议版本、随机数(第一个随机数)、支持的加密算法、支持的压缩算法
  2. 服务器确定加密协议的版本,以及加密的算法,然后也生成一个随机数。
  3. 服务器把加密协议的版本,加密的算法,随机数(第二个随机数)以及自己的证书一起发送给客户端
  4. 客户端收到服务器回应以后,首先验证服务器证书。
    如果证书不是可信任机构颁布、或者证书中的域名和实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
    验证证书主要根据服务端发过来的证书名称,在本地寻找其低级证书,并一级一级直到根证书,验证各级证书的合法性。
  5. 客户端验证证书后,再次产生一个随机数(第三个随机数),然后使用证书中的公钥加密这个随机值,再放一个ChangCipherSpec消息(用于告知服务器,客户端已经切换到之前协商好的加密套件(Cipher Suite)的状态,准备使用之前协商好的加密套件加密数据并传输了),还有整个前面所有消息的hash值,进行服务器验证,然后用新密钥加密一段数据一并发送到服务器,确保正式通信前无误。
  6. 服务器根据证书解密第三个随机数,这样就生成了 "会话密钥"
  7. 服务器像客户端发送 “ChangCipherSpec消息” 、 握手结束通知
  8. 客户端用之前生成的私钥解密服务器传过来的信息。
    至此,整个握手阶段全部结束了。接下来,客户端与服务器进入加密通信,就完全是使用普通HTTP协议,只不过用"会话密钥"加密内容。

HTTP2.0

  • 新的二进制格式。HTTP 1.x的解析是基于文本。
  • 多路复用。一个request对应一个id,这样一个连接上可以有多个requst,每个连接的request可以随机的混杂在一起,接受方可以根据request的id将request再归属到各自不同的服务端请求里面


    多路复用
  • 请求优先级。把HTTP消息分解为很多独立的帧后,就可以通过优化这些帧的交错和传输顺序,进一步提供性能。
  • header压缩。HTTP1.x的header带有大量的信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一个header field表,既避免了重复的header传输,又减少了需要传输的大小。
  • 服务端推送
  • 新增的二进制分帧层。它定义了如何封装HTTP消息并在客户端与服务器之间传输


    二进制分帧层

    所有HTTP 2.0通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。每个数据流以消息的形式发送。而消息由一或多个帧组成,而这些帧可以乱序发送,然后再根据每个帧首部流标识符重新组装。

异步请求

如果当前还可以执行异步任务,则入队,并立即执行,否则加入readyAsyncCalls队列,当一个请求执行完毕后,会调用promoteCalls(),来把readyAsyncCalls队列中的Async移出来并加入到runningAsyncCalls,并开始执行。然后在当前线程中去执行Call的getResponseWithInterceptorChain()方法,直接获取当前的返回数据Response.

对比同步和异步任务,我们会发现:同步请求和异步请求原理都是一样的,都是在getResponseWithInterceptorChain()函数通过Interceptor链条来实现网络请求逻辑,而异步任务则通过ExecutorService来实现的。PS:在Dispatcher中添加一个封装了Callback的Call的匿名内部类AsyncCall来执行当前 的Call。这个AsyncCall是Call的匿名内部类。AsyncCall的execute方法仍然会回调到Call的 getResponseWithInterceptorChain方法来完成请求,同时将返回数据或者状态通过Callback来完成。

连接

Address

newRealCall 的时候配置了 transmitter

  static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.transmitter = new Transmitter(client, call);
    return call;
  }
  public Transmitter(OkHttpClient client, Call call) {
    this.client = client;
    this.connectionPool = Internal.instance.realConnectionPool(client.connectionPool());
    this.call = call;
    this.eventListener = client.eventListenerFactory().create(call);
    this.timeout.timeout(client.callTimeoutMillis(), MILLISECONDS);
  }

Transmitter 中有 connectionPool 还有 exchangeFinderRetryAndFollowUpInterceptor会在intercept() 中调用 prepareToConnect(), 它的参数之一 Address 则是通过 createAddress()产生的.
Address 的url字段仅仅包含HTTP请求的url的schema+host+port三部分的信息,而不包含path和query等信息。它还有一个重要的方法 equalsNonHost (), 这个方法会在连接池复用的时候调用,如果返回 true, 那么就可以使用 RealConnection 的复用

RouteSelector

ExchangeFinder 初始化的时候 new 一个 RouteSelector
这个类主要是选择连接到服务器的路由,选择的连接需要是代理服务器、IP地址、TLS模式 三者中的一种。这个选择的连接是可以被回收的。

因为HTTP请求连接到服务器的时候,需要找到一个Route,然后依据代理协议规则与特定目标建立TCP连接。如果是无代理的情况,是与HTTP服务器建立TCP连接,对于SOCKS代理和http代理,是与代理服务器建立tcp连接,虽然都是与代理服务器建立tcp连接,但是SOCKS代理协议和http代理协议又有一定的区别。
有的网站会借助于域名做负均衡,常常会有域名对应不同IP地址的情况。在OKHTTP中,对Route连接有一定的错误处理机制。OKHTTP会逐个尝试找到Route建立TCP连接,直到找到可用的哪一个。这样对Route信息有良好的管理。OKHTTP中借助RouteSelector类管理所有路由信息,并帮助选择路由。

public final class Route {
  final Address address;
  final Proxy proxy;
  final InetSocketAddress inetSocketAddress;
}

在构造函数中就会调用 resetNextProxy() 来收集路由,分为两种情况:1.收集所有的代理;2.收集特定的代理服务器的目标地址。

它们的实现也是通过两种方式:

  1. 通过外部address传入代理。因为是来自 OkHttpClient,我们可以指定代理
  2. 借助于ProxySelectory获得多个代理。默认收集的所有代理保存在列表proxies中

RouteSelector有两个重要的成员函数 hasNext()next()
hasNext() 表明是否还有可用的路由

  public boolean hasNext() {
    return hasNextInetSocketAddress()
        || hasNextProxy()
        || hasNextPostponed();
  }

 //是否还有代理
  private boolean hasNextProxy() {
    return nextProxyIndex < proxies.size();
  }

  //是否还有socket地址
  private boolean hasNextInetSocketAddress() {
    return nextInetSocketAddressIndex < inetSocketAddresses.size();
  }

  //是否还有延迟路由
  private boolean hasNextPostponed() {
    return !postponedRoutes.isEmpty();
  }

next() 方法就是用来获取可能的连接地址

  1. 对于没有配置代理的情况,会对HTTP服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建连接的目标地址
  2. 对于SOCKS代理,直接以HTTP的服务器的域名以及协议端口创建连接目标地址
  3. 对于HTTP代理,则会对HTTP代理服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址
  public Route next() throws IOException {
    // Compute the next route to attempt.
    if (!hasNextInetSocketAddress()) {
      if (!hasNextProxy()) {
        if (!hasNextPostponed()) {
          throw new NoSuchElementException();
        }
        return nextPostponed();
      }
      lastProxy = nextProxy();
    }
    lastInetSocketAddress = nextInetSocketAddress();

    Route route = new Route(address, lastProxy, lastInetSocketAddress);
    if (routeDatabase.shouldPostpone(route)) {
      postponedRoutes.add(route);
      // We will only recurse in order to skip previously failed routes. They will be tried last.
      return next();
    }

    return route;
  }

对应的是 hasNextPostponed(), hasNextProxy(), hasNextInetSocketAddress()

RetryAndFollowUpInterceptor

上面我们分析了 AddressRouteSelector,现在我们看它们是怎么用的

public final class RetryAndFollowUpInterceptor implements Interceptor {

  private static final int MAX_FOLLOW_UPS = 20;

  private final OkHttpClient client;

  public RetryAndFollowUpInterceptor(OkHttpClient client) {
    this.client = client;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Transmitter transmitter = realChain.transmitter();

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      // 根据连接池、Address,构建出了 exchangeFinder
      transmitter.prepareToConnect(request);

      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        // 将配置好的 transmitter 传递个下一个拦截器,即 BridgeIntercepter
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      Exchange exchange = Internal.instance.exchange(response);
      Route route = exchange != null ? exchange.connection().route() : null;
      Request followUp = followUpRequest(response, route);

      if (followUp == null) {
        if (exchange != null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }

      RequestBody followUpBody = followUp.body();
      if (followUpBody != null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;
      priorResponse = response;
    }
  }
}

执行过程如下:

  1. 先是获取 Call 的transmitter, transmitter中是有 connectionPool 的
  2. 开启 while 循环
  3. 执行 prepareToConnect,判断是否是相同连接、是否需要maybeReleaseConnection(),并重置 exchangeFinder,这个 finder 就是用来寻找可用 Connection
  4. 执行下一个拦截器
  5. 如果 priorResponse 不为空,说明得到了 response
  6. 获取从 RouteSelector 中得到的 Route
  7. 执行 followUpRequest()查看响应是否需要重定向,如果不需要重定向则返回当前请求
  8. 重定向次数+1,同时判断是否达到最大限制数量。是:退出
  9. 重置request,并把当前的Response保存到priorResponse,进入下一次的while循环

总的来说:
就是不停的循环来获取response,每循环一次都会获取下一个request,如果没有request,则返回response,退出循环。而获取的request 是根据上一个response 的状态码确定的。

BridgeInterceptor

主要负责对Request和Response报文进行加工

  1. 在发送阶段补全了一些header,如Content-Type、Content-Length、Transfer-Encoding、Host、Connection、Accept-Encoding、User-Agent 等。
  2. 如果需要gzip压缩则进行gzip压缩
  3. 加载Cookie
  4. 随后创建新的request并交付给后续的interceptor来处理,以获取响应。
  5. 保存Cookie
  6. 如果服务器返回的响应content是以gzip压缩过的,则会先进行解压缩,移除响应中的header Content-Encoding和Content-Length,构造新的响应返回。
  7. 否则直接返回 response
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容