OkHttp源碼分析(三)內(nèi)置攔截器解析

在上篇 OkHttp源碼分析(二)整體流程 中分析了OkHttp請求的整體流程滔迈,這接下來的這篇文章中將詳細(xì)分析OkHttp5個內(nèi)置的攔截器

思維導(dǎo)圖

OkHttp內(nèi)置攔截器.png

RetryAndFollowUpInterceptor

主要做了三件事

  • 創(chuàng)建了StreamAllocation刊殉,用于Socket管理
  • 處理重定向
  • 失敗重連

先看源碼

@Override 
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    //初始化一個socket連接對象
    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response = null;
      boolean releaseConnection = true;
      try {
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        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, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

      // 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();
      }

      Request followUp = followUpRequest(response);

      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }

      closeQuietly(response.body());

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

      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }

      if (!sameConnection(response, followUp.url())) {
        streamAllocation.release();
        streamAllocation = new StreamAllocation(
            client.connectionPool(), createAddress(followUp.url()), callStackTrace);
      } else if (streamAllocation.codec() != null) {
        throw new IllegalStateException("Closing the body of " + response
            + " didn't close its backing stream. Bad interceptor?");
      }

      request = followUp;
      priorResponse = response;
    }
}

1) 初始化StreamAllocation

StreamAllocation對象用于分配一個到特定的服務(wù)器地址的流出嘹,有兩個實現(xiàn):Http1Codec 和 Http2Codec啥刻,分別對應(yīng) HTTP/1.1 和 HTTP/2 版本的實現(xiàn)刺下。這個流可能是從ConnectionPool中取得的之前沒有釋放的連接共郭,也可能是重新分配的。這涉及到連接池復(fù)用及TCP建立連接糠馆、釋放連接的過程嘶伟。

streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);
  • 首先從OkHttpClient中獲取ConnectionPool對象(OkHttpClient構(gòu)建時創(chuàng)建)
  • 用請求的URL創(chuàng)建Address對象 (Address描述某一個特定的服務(wù)器地址)

創(chuàng)建好StreamAllocation后并未使用,而是交給后面CallServerInterceptor使用又碌。
隨后將Request交由下一個Interceptor處理并獲取響應(yīng)

2)出錯重試機(jī)制

重試和重定向偽代碼如下

while (true) {
     try {
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
      } catch (Exception e) {
       //判斷重試
        continue;
      }
    //判斷重定向
    request = followUp; //Request重新賦值
}

在獲取響應(yīng)過程中如果發(fā)生異常將Catch住九昧,根據(jù)不同的異常類型執(zhí)行不同的重試機(jī)制,重試機(jī)制主要在recover中完成

private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);

    // The application layer has forbidden retries.
    if (!client.retryOnConnectionFailure()) return false;

    // We can't send the request body again.
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    // This exception is fatal.
    if (!isRecoverable(e, requestSendStarted)) return false;

    // No more routes to attempt.
    if (!streamAllocation.hasMoreRoutes()) return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }
  • 如果用戶在OkHttpClient中設(shè)置了retryOnConnectionFailure = false,表示失敗請求失敗時不重試(默認(rèn)為true) 毕匀,那用戶不讓重試也沒辦法了铸鹰;
  • 請求Request不能重復(fù)發(fā)送,也不能重試皂岔;
  • 四種情況不能恢復(fù):
    • 協(xié)議錯誤(ProtocolException)
    • 中斷異常(InterruptedIOException)
    • SSL握手錯誤(SSLHandshakeException && CertificateException)
    • certificate pinning錯誤(SSLPeerUnverifiedException)
  • 沒有更多線路可供選擇

如判斷可恢復(fù)掉奄,將跳出該循環(huán),重新執(zhí)行

response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);

從而完成失敗重試凤薛。

3)處理重定向

RetryAndFollowUpInterceptor通過followUpRequest()從響應(yīng)的信息中提取出重定向的信息,并構(gòu)造新的Request

/**
   * Figures out the HTTP request to make in response to receiving {@code userResponse}. This will
   * either add authentication headers, follow redirects or handle a client request timeout. If a
   * follow-up is either unnecessary or not applicable, this returns null.
   */
  private Request followUpRequest(Response userResponse) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    Connection connection = streamAllocation.connection();
    Route route = connection != null
        ? connection.route()
        : null;
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      case HTTP_PROXY_AUTH:
        Proxy selectedProxy = route != null
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);

      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);

      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // "If the 307 or 308 status code is received in response to a request other than GET
        // or HEAD, the user agent MUST NOT automatically redirect the request"
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
        // fall-through
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        // Most redirects don't include a request body.
        Request.Builder requestBuilder = userResponse.request().newBuilder();
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // When redirecting across hosts, drop all authentication headers. This
        // is potentially annoying to the application layer since they have no
        // way to retain them.
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      case HTTP_CLIENT_TIMEOUT:
        // 408's are rare in practice, but some servers like HAProxy use this response code. The
        // spec says that we may repeat the request without modifications. Modern browsers also
        // repeat the request (even non-idempotent ones.)
        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
          return null;
        }

        return userResponse.request();

      default:
        return null;
    }
}
  • 根據(jù)followUpRequest的返回值诞仓,如果不是重定向,就返回response
  • 重定向的最大次數(shù)為20缤苫,超過20拋出異常
if (++followUpCount > MAX_FOLLOW_UPS) {//有最大次數(shù)限制20次
    streamAllocation.release();
    throw new ProtocolException("Too many follow-up requests: " + followUpCount);
  }

   request = followUp;//把重定向的請求賦值給request,以便再次進(jìn)入循環(huán)執(zhí)行
   priorResponse = response;

BridgeInterceptor

BridgeInterceptor緊隨RetryAndFollowUpInterceptor,主要的職責(zé)如下:

  • 在請求階段補(bǔ)全HTTP Header;
  • 響應(yīng)階段保存Cookie
  • 響應(yīng)階段處理Gzip解壓縮;
@Override
public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();

    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
}

1) 補(bǔ)全HTTP Header

包括Content-Type墅拭、Content-Length活玲、Transfer-Encoding、Host谍婉、Connection舒憾、Accept-Encoding、User-Agent穗熬、Cookie等
其中Cookie的加載由CookieJar提供镀迂,CookieJar可用OkHttpClient在初始化設(shè)置

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cookieJar(new CookieJar() {
                @Override
                public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                    // 可將cookie保存到SharedPreferences中
                }

                @Override
                public List<Cookie> loadForRequest(HttpUrl url) {
                  // 從保存位置讀取,注意此處不能為空唤蔗,否則會導(dǎo)致空指針
                    return new ArrayList<>();
                }
            })
    .build();

2)保存Cookie

HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) {
    if (cookieJar == CookieJar.NO_COOKIES) return;

    List<Cookie> cookies = Cookie.parseAll(url, headers);
    if (cookies.isEmpty()) return;
    //保存到SP中
    cookieJar.saveFromResponse(url, cookies);
  }

3)處理Gzip

if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
}

gzip由okio完成探遵,隨后將Content-Encoding窟赏、Content-Length從Header中移除

CacheInterceptor

Okhttp的網(wǎng)絡(luò)緩存是基于http協(xié)議 可參考 HTTP 協(xié)議緩存機(jī)制詳解

015353_P04w_568818.png

使用OkHttp緩存的前提是需要在構(gòu)建OkHttpClient時指定一個Cache

OkHttpClient httpClient = new OkHttpClient.Builder()
              .cache(new Cache(this.getCacheDir(), 10240 * 1024))
              .build();

攔截器整體代碼如下:

@Override 
public Response intercept(Chain chain) throws IOException {
   //讀取緩存
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    //CacheStrategy類似一個mapping操作,將request和cacheCandidate輸入箱季,得到兩個輸出networkRequest和cacheResponse
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    //緩存存在涯穷,進(jìn)行緩存監(jiān)控(命中次數(shù))
    if (cache != null) {
      cache.trackResponse(strategy);
    }
    //緩存存在,經(jīng)過CacheStrategy輸出的緩存無效藏雏,關(guān)閉原始緩存
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    //only-if-cached(表明不進(jìn)行網(wǎng)絡(luò)請求拷况,且緩存不存在或者過期,一定會返回504錯誤)
    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // If we don't need the network, we're done.
    //不需要網(wǎng)絡(luò)請求掘殴,返回緩存
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      //執(zhí)行網(wǎng)絡(luò)請求
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      //本地緩存有效赚瘦,服務(wù)器資源未修改,需要更新Header
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    //使用網(wǎng)絡(luò)響應(yīng)
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    //更新緩存
    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }
    return response;
}

1)讀取緩存

Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }
  • url作為輸入杯巨,md5\hex加密后得到key蚤告;
  • 根據(jù)key得到Snapshot,關(guān)聯(lián)起文件系統(tǒng)中的緩存文件服爷;
  • 根據(jù)snapshot生成Entry杜恰,根據(jù)Entry生成Response返回

2)緩存策略配置

緩存策略通過CacheStrategy來實現(xiàn),CacheStrategy構(gòu)建分為兩步

① Factory解析Header參數(shù)

public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
 }
  • Factory中主要解析緩存中與響應(yīng)有關(guān)的頭仍源,Date心褐、Expires、Last-Modified笼踩、ETag逗爹、Age等;
  • 注意Headers并不是一個Map嚎于,而是一個數(shù)組掘而,奇數(shù)位存key,偶數(shù)位存value于购;

② get返回CacheStrategy實例

public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();
      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

      return candidate;
}

get內(nèi)部主要是getCandidate實現(xiàn)

private CacheStrategy getCandidate() {
  //如果緩存沒有命中(即null),網(wǎng)絡(luò)請求也不需要加緩存Header了
  if (cacheResponse == null) {
    //`沒有緩存的網(wǎng)絡(luò)請求,查上文的表可知是直接訪問
    return new CacheStrategy(request, null);
  }

  // 如果緩存的TLS握手信息丟失,返回進(jìn)行直接連接
  if (request.isHttps() && cacheResponse.handshake() == null) {
    //直接訪問
    return new CacheStrategy(request, null);
  }

  //檢測response的狀態(tài)碼,Expired時間,是否有no-cache標(biāo)簽
  if (!isCacheable(cacheResponse, request)) {
    //直接訪問
    return new CacheStrategy(request, null);
  }

  CacheControl requestCaching = request.cacheControl();
  //如果請求報文使用了`no-cache`標(biāo)簽(這個只可能是開發(fā)者故意添加的)
  //或者有ETag/Since標(biāo)簽(也就是條件GET請求)
  if (requestCaching.noCache() || hasConditions(request)) {
    //直接連接,把緩存判斷交給服務(wù)器
    return new CacheStrategy(request, null);
  }
  //根據(jù)RFC協(xié)議計算
  //計算當(dāng)前age的時間戳
  //now - sent + age (s)
  long ageMillis = cacheResponseAge();
  //大部分情況服務(wù)器設(shè)置為max-age
  long freshMillis = computeFreshnessLifetime();

  if (requestCaching.maxAgeSeconds() != -1) {
    //大部分情況下是取max-age
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }

  long minFreshMillis = 0;
  if (requestCaching.minFreshSeconds() != -1) {
    //大部分情況下設(shè)置是0
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }

  long maxStaleMillis = 0;
  //ParseHeader中的緩存控制信息
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    //設(shè)置最大過期時間,一般設(shè)置為0
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }

  //緩存在過期時間內(nèi),可以使用
  //大部分情況下是進(jìn)行如下判斷
  //now - sent + age + 0 < max-age + 0
  if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    //返回上次的緩存
    Response.Builder builder = cacheResponse.newBuilder();
    return new CacheStrategy(null, builder.build());
  }

  //緩存失效, 如果有etag等信息
  //進(jìn)行發(fā)送`conditional`請求,交給服務(wù)器處理
  Request.Builder conditionalRequestBuilder = request.newBuilder();

  if (etag != null) {
    conditionalRequestBuilder.header("If-None-Match", etag);
  } else if (lastModified != null) {
    conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
  } else if (servedDate != null) {
    conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
  }
  //下面請求實質(zhì)還說網(wǎng)絡(luò)請求
  Request conditionalRequest = conditionalRequestBuilder.build();
  return hasConditions(conditionalRequest) ? new CacheStrategy(conditionalRequest,
      cacheResponse) : new CacheStrategy(conditionalRequest, null);
}

3)緩存監(jiān)控

if (cache != null) {
    cache.trackResponse(strategy);
}

synchronized void trackResponse(CacheStrategy cacheStrategy) {
    requestCount++;

    if (cacheStrategy.networkRequest != null) {
      // If this is a conditional request, we'll increment hitCount if/when it hits.
      networkCount++;
    } else if (cacheStrategy.cacheResponse != null) {
      // This response uses the cache and not the network. That's a cache hit.
      hitCount++;
    }
}

可見袍睡,緩存監(jiān)控主要是監(jiān)控請求次數(shù),細(xì)分為網(wǎng)絡(luò)請求次數(shù)和緩存命中次數(shù)肋僧。

ConnectInterceptor

ConnectInterceptor用來與服務(wù)器建立連接

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

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

ConnectInterceptor代碼很簡潔斑胜,邏輯處理交由其他類去實現(xiàn)了。
主要做了以下幾件事情:

  1. 獲取到StreamAllocation對象嫌吠;
  • 通過StreamAllocation對象創(chuàng)建RealConnection止潘;
  • 通過StreamAllocation對象創(chuàng)建HttpCodec;

RealInterceptorChain中的四個重要屬性將在ConnectInterceptor中全部創(chuàng)建完畢

  • Request
  • StreamAllocation
  • HttpCodec
  • Connection

其中辫诅,Request一開始就有凭戴,StreamAllocation在RetryAndFollowUpInterceptor創(chuàng)建,因此ConnectInterceptor中主要分析Connection和HttpCodec的創(chuàng)建過程

Connection和HttpCodec創(chuàng)建過程

HttpCodec用來編解碼HTTP請求和響應(yīng)炕矮,通過streamAllocation.newStream方法可以創(chuàng)建一個HttpCodec和RealConnection

//StreamAllocation
public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
}

HttpCodec的創(chuàng)建分為兩步:

  • 獲取連接RealConnection(能復(fù)用就復(fù)用簇宽,不能復(fù)用就新建)
  • 根據(jù)RealConnection創(chuàng)建HttpCodec

① 獲取RealConnection

 private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
}

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    boolean connectionRetryEnabled) throws IOException {
  Route selectedRoute;
  synchronized (connectionPool) {
    if (released) throw new IllegalStateException("released");
    if (codec != null) throw new IllegalStateException("codec != null");
    if (canceled) throw new IOException("Canceled");
    // 使用已存在的連接
    // Attempt to use an already-allocated connection.
    RealConnection allocatedConnection = this.connection;
    if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
      return allocatedConnection;
    }
    // 從緩存中獲取
    // Attempt to get a connection from the pool.
    Internal.instance.get(connectionPool, address, this);
    if (connection != null) {
      return connection;
    }
    selectedRoute = route;
  }
  // 線路的選擇勋篓,多ip的支持
  // If we need a route, make one. This is a blocking operation.
  if (selectedRoute == null) {
    selectedRoute = routeSelector.next();
  }
  // Create a connection and assign it to this allocation immediately. This makes it possible for
  // an asynchronous cancel() to interrupt the handshake we're about to do.
  // 以上都不符合,創(chuàng)建一個連接(RealConnection)
  RealConnection result;
  synchronized (connectionPool) {
    route = selectedRoute;
    refusedStreamCount = 0;
    result = new RealConnection(connectionPool, selectedRoute);
    acquire(result);
    if (canceled) throw new IOException("Canceled");
  }
  //Socket連接
  // Do TCP + TLS handshakes. This is a blocking operation.
  result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
  routeDatabase().connected(result.route());
  Socket socket = null;
  // 更新緩存
  synchronized (connectionPool) {
    // Pool the connection.
    Internal.instance.put(connectionPool, result);
    // If another multiplexed connection to the same address was created concurrently, then
    // release this connection and acquire that one.
    if (result.isMultiplexed()) {
      socket = Internal.instance.deduplicate(connectionPool, address, this);
      result = connection;
    }
  }
  closeQuietly(socket);
  return result;
}
讀取緩存
 Internal.instance.get(connectionPool, address, this, null);

Internal.instance在OkHttpClient靜態(tài)代碼塊創(chuàng)建

@Override 
public RealConnection get(ConnectionPool pool, Address address,
          StreamAllocation streamAllocation, Route route) {
     return pool.get(address, streamAllocation, route);
}

//ConnectionPool
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection);
        return connection;
      }
    }
    return null;
  }

遍歷所有的Connection魏割,Address或Route匹配則返回

建立Socket連接
public void connect(
      int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    }
    //建立連接
    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout);
        } else {
          //正常走這條邏輯
          connectSocket(connectTimeout, readTimeout);
        }
        establishProtocol(connectionSpecSelector);
        break;
      } catch (IOException e) {
        //異常處理省略
      }
    }
   //......
  }

private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    //獲得代理
    Proxy proxy = route.proxy();
    Address address = route.address();
    //根據(jù)代理類型創(chuàng)建Socket
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);
    //設(shè)置超時時間
    rawSocket.setSoTimeout(readTimeout);
    try { 
      //建立Socket連接
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
      ce.initCause(e);
      throw ce;
    }
    //okio讀取輸入流和輸出流
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));
  }
  • Sink可看做OutputStream譬嚣,Source可看做InputStream;
更新緩存
Internal.instance.put(connectionPool, result);

//ConnectionPool
void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

cleanup的邏輯后面分析

② 根據(jù)RealConnection創(chuàng)建HttpCodec

public HttpCodec newCodec(
      OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, streamAllocation, http2Connection);
    } else {
      //正常走下面
      socket.setSoTimeout(client.readTimeoutMillis());
      source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
      // source和sink是Socket連接后返回
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }

CallServerInterceptor

@Override 
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //取出在前面攔截器中創(chuàng)建的四個對象钞它,他們保存在RealInterceptorChain中
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();

    long sentRequestMillis = System.currentTimeMillis();
    //寫入請求頭
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return what
      // we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        //寫入請求體
        // Write the request body if the "Expect: 100-continue" expectation was met.
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } else if (!connection.isMultiplexed()) {
        // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection from
        // being reused. Otherwise we're still obligated to transmit the request body to leave the
        // connection in a consistent state.
        streamAllocation.noNewStreams();
      }
    }

    httpCodec.finishRequest();

    if (responseBuilder == null) {
      //讀取響應(yīng)頭
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    //構(gòu)建響應(yīng)
    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      //讀取響應(yīng)體
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
}

在前面ConnectInterceptor中建立Socket連接后拜银,okio會解析輸入輸出流,保存在source和sink中遭垛,此時只是建立了Socket連接尼桶,并未進(jìn)行數(shù)據(jù)傳輸,CallServerInterceptor的作用就是根據(jù)HTTP協(xié)議標(biāo)準(zhǔn)锯仪,對Request發(fā)送以及對Response進(jìn)行解析泵督。

在CallServerInterceptor中,首先會從RealInterceptorChain中取出在前面攔截器中創(chuàng)建的四個對象HttpCodec庶喜、StreamAllocation小腊、RealConnection、Request久窟。

過程分析如下:

1)發(fā)送HTTP請求數(shù)據(jù)(Header&Body)

首先在sink中寫入請求頭

httpCodec.writeRequestHeaders(request);

//Http1Codec
@Override 
public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    writeRequest(request.headers(), requestLine);
  }

/** Returns bytes of a request header for sending on an HTTP transport. */
public void writeRequest(Headers headers, String requestLine) throws IOException {
    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
    sink.writeUtf8(requestLine).writeUtf8("\r\n");
    for (int i = 0, size = headers.size(); i < size; i++) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    sink.writeUtf8("\r\n");
    state = STATE_OPEN_REQUEST_BODY;
}

  • 讀取請求行秩冈,這里返回 GET /api/data/Android/10/1 HTTP/1.1
  • 從Request中獲取Header,循環(huán)寫入到sink中

其次斥扛,如果http請求有body(POST請求)入问,再將body寫入sink,發(fā)送給server

if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return what
      // we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } else if (!connection.isMultiplexed()) {
        // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection from
        // being reused. Otherwise we're still obligated to transmit the request body to leave the
        // connection in a consistent state.
        streamAllocation.noNewStreams();
      }
}

最后稀颁,把sink中的數(shù)據(jù)刷出去

httpCodec.finishRequest();

2)讀取響應(yīng)數(shù)據(jù)

分為兩個步驟

① 讀取響應(yīng)頭
//CallServerInterceptor
if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
 }
Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

//Http1Codec
@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
      throw new IllegalStateException("state: " + state);
    }

    try {
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());

      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());

      if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
      }

      state = STATE_OPEN_RESPONSE_BODY;
      return responseBuilder;
    } catch (EOFException e) {
      // Provide more context if the server ends the stream before sending a response.
      IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
      exception.initCause(e);
      throw exception;
    }
  }
  • 首先解析響應(yīng)行芬失、協(xié)議、狀態(tài)嗎匾灶、響應(yīng)頭
  • 利用responseBuilder構(gòu)建Response
② 讀取響應(yīng)體

只要不是websocket并且狀態(tài)碼為101(服務(wù)器轉(zhuǎn)換協(xié)議:服務(wù)器將遵從客戶的請求轉(zhuǎn)換到另外一種協(xié)議)都會讀取響應(yīng)體

//CallServerInterceptor
int code = response.code();
  if (forWebSocket && code == 101) {
    // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
    response = response.newBuilder()
        .body(Util.EMPTY_RESPONSE)
        .build();
  } else {
    response = response.newBuilder()
        .body(httpCodec.openResponseBody(response))
        .build();
 }

//Http1Codec
@Override 
public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
}

private Source getTransferStream(Response response) throws IOException {
    if (!HttpHeaders.hasBody(response)) {
      return newFixedLengthSource(0);
    }

    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      return newChunkedSource(response.request().url());
    }

    long contentLength = HttpHeaders.contentLength(response);
    if (contentLength != -1) {
      return newFixedLengthSource(contentLength);
    }

    // Wrap the input stream from the connection (rather than just returning
    // "socketIn" directly here), so that we can control its use after the
    // reference escapes.
    return newUnknownLengthSource();
}

然后將ResponseBody更新到Response中的body中麸折。
至此,整個請求過程執(zhí)行完畢

總結(jié)

每個攔截器各司其職粘昨,環(huán)環(huán)相扣,非常優(yōu)雅地完成了網(wǎng)絡(luò)請求的流程窜锯。最后借piasy一張圖张肾,希望讀者對OkHttp能有一個更加清晰的認(rèn)知。

okhttp_full_process.png

參考

https://blog.piasy.com/2016/07/11/Understand-OkHttp/
http://lowett.com/2017/02/24/okhttp-4/
http://lowett.com/2017/03/02/okhttp-5/
http://lowett.com/2017/03/09/okhttp-6/
http://lowett.com/2017/03/21/okhttp-7/
http://lowett.com/2017/03/30/okhttp-8/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锚扎,一起剝皮案震驚了整個濱河市吞瞪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驾孔,老刑警劉巖芍秆,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惯疙,死亡現(xiàn)場離奇詭異,居然都是意外死亡妖啥,警方通過查閱死者的電腦和手機(jī)霉颠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來荆虱,“玉大人蒿偎,你說我怎么就攤上這事』扯粒” “怎么了诉位?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長菜枷。 經(jīng)常有香客問我苍糠,道長,這世上最難降的妖魔是什么啤誊? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任岳瞭,我火速辦了婚禮,結(jié)果婚禮上坷衍,老公的妹妹穿的比我還像新娘寝优。我一直安慰自己,他們只是感情好枫耳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布乏矾。 她就那樣靜靜地躺著,像睡著了一般迁杨。 火紅的嫁衣襯著肌膚如雪钻心。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天铅协,我揣著相機(jī)與錄音捷沸,去河邊找鬼。 笑死狐史,一個胖子當(dāng)著我的面吹牛痒给,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骏全,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼苍柏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了姜贡?” 一聲冷哼從身側(cè)響起试吁,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎楼咳,沒想到半個月后熄捍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烛恤,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年余耽,在試婚紗的時候發(fā)現(xiàn)自己被綠了缚柏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡宾添,死狀恐怖船惨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缕陕,我是刑警寧澤粱锐,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站扛邑,受9級特大地震影響怜浅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蔬崩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一恶座、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沥阳,春花似錦跨琳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至功炮,卻和暖如春溅潜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背薪伏。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工滚澜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嫁怀。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓设捐,卻偏偏與公主長得像饶火,于是被迫代替她去往敵國和親锋喜。 傳聞我的和親對象是個殘疾皇子糙箍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容