OKHttp解析

解析OKHttp首先走一遍正常的流程,然后將比較有意思的點拿出來說明

正常流程分析

1.OkHttpClient初始化
OkHttpClient mOkHttpClient = new OkHttpClient();

通過代碼查看,可以看到調用了內部的Builder構造

如下

public Builder() {
      //調度器
      dispatcher = new Dispatcher();
      //默認支持的協議列表
      protocols = DEFAULT_PROTOCOLS;
      //默認的連接規范
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      //默認的代理選擇器(直連)
      proxySelector = ProxySelector.getDefault();
      //默認不管理cookie
      cookieJar = CookieJar.NO_COOKIES;
      socketFactory = SocketFactory.getDefault();
      //主機驗證
      hostnameVerifier = OkHostnameVerifier.INSTANCE;
      //證書鎖,默認不開啟
      certificatePinner = CertificatePinner.DEFAULT;
      //默認不進行授權
      proxyAuthenticator = Authenticator.NONE;
      authenticator = Authenticator.NONE;
      //初始化連接池
      connectionPool = new ConnectionPool();
      //DNS
      dns = Dns.SYSTEM;
      followSslRedirects = true;
      followRedirects = true;
      retryOnConnectionFailure = true;
      //超時時間
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
      pingInterval = 0;
}

接下來介紹下關于上面的OkHttpClient配置需要用到的類

Dispatcher

調度器,里面包含了線程池和三個隊列(readyAsyncCalls:保存等待執行的異步請求;runningAsyncCalls:保存正在運行的異步請求;runningSyncCalls:保存正在執行的同步請求)

//保存準備運行的異步請求(當運行請求超過限制數時會保存在此隊列)
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//保存正在運行的異步請求
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//保存正在運行的同步請求
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

對于異步請求,調用Dispatcher的enqueue方法,在這個方法會將相關請求提交到線程池中操作,從而異步執行

synchronized void enqueue(AsyncCall call) {
    //檢查容量大小
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);//加入隊列
      executorService().execute(call);//執行
    } else {
      //超過容量大小后,加入準備隊列中
      readyAsyncCalls.add(call);
    }
  }

對于同步請求,不需要提交到線程池執行,通過Dispatcher的executed方法調用即可

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

當請求執行完畢后,調用finished將請求從runningAsyncCalls隊列中移除,并且檢查readyAsyncCalls以繼續提交在隊列中準備的請求。


//移除執行完畢的請求
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; //容量已滿,不提交新請求
    if (readyAsyncCalls.isEmpty()) return; // 沒有正在準備的請求,返回

   //從readyAsyncCalls中循環取出AsyncCall直到達到容量上限
    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; // 達到上限后返回
    }
  }

Protocal

協議類,用來表示使用的協議版本,比如http/1.0,http/1.1,spdy/3.1,h2等

ConnectionSpecs

連接規范,用于配置Socket連接層,對于HTTPS,還能配置安全傳輸層協議(TLS)版本與密碼套件(CipherSuite)

Proxy與ProxySelector

Proxy代理類,默認有三種代理模式DIRECT(直連),HTTP(Http代理),SOCKS(socks代理)
ProxySelector代理選擇類,默認不使用代理,即使用直連方式,當然,我們可以自定義配置,以指定URI使用某種代理,類似代理軟件的PAC功能。

CookieJar

用來管理cookie,可以根據url保存cookie,也可以通過url取出相應cookie。默認的不做cookie管理。該接口中有兩個抽象方法,用戶可以自己實現該接口以對cookie進行管理。

  //保存cookie
  void saveFromResponse(HttpUrl url, List<Cookie> cookies);

  //根據Url導入保存的Cookie
  List<Cookie> loadForRequest(HttpUrl url);

SocketFactory

Socket工廠,通過createSocket來創建Socket

HostnameVerifier

主機名驗證器,與HTTPS中的SSL相關,當握手時如果URL的主機名不是可識別的主機,就會要求進行主機名驗證

public interface HostnameVerifier {

     //通過session驗證指定的主機名是否被允許
    boolean verify(String hostname, SSLSession session);
}

CertificatePinner

證書鎖,HTTPS相關,用于約束哪些證書可以被信任,可以防止一些已知或未知的中間證書機構帶來的攻擊行為。如果所有證書都不被信任將拋出SSLPeerUnverifiedException異常。

其中用于檢查證書是否被信任的源碼如下:

//檢查證書是否被信任
 public void check(String hostname, List<Certificate> peerCertificates)
      throws SSLPeerUnverifiedException {
    List<Pin> pins = findMatchingPins(hostname);//獲取Pin(網址,hash算法,hash值)
    if (pins.isEmpty()) return;

    if (certificateChainCleaner != null) {
       //通過清潔器獲取信任的證書
       peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
    }

    for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
      //對證書進行比對hash值,如果配對失敗就拋出SSLPeerUnverifiedException異常
      X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);

      // Lazily compute the hashes for each certificate.
      ByteString sha1 = null;
      ByteString sha256 = null;

      for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
        Pin pin = pins.get(p);
        if (pin.hashAlgorithm.equals("sha256/")) {
          if (sha256 == null) sha256 = sha256(x509Certificate);
          if (pin.hash.equals(sha256)) return; // Success!
        } else if (pin.hashAlgorithm.equals("sha1/")) {
          if (sha1 == null) sha1 = sha1(x509Certificate);
          if (pin.hash.equals(sha1)) return; // Success!
        } else {
          throw new AssertionError();
        }
      }
    }

    // ...
 }

Authenticator

身份認證器,當連接提示未授權時,可以通過重新設置請求頭來響應一個新的Request。狀態碼401表示遠程服務器請求授權,407表示代理服務器請求授權。該認證器在需要時會被RetryAndFollowUpInterceptor觸發。

public interface Authenticator {

  Authenticator NONE = new Authenticator() {
    @Override public Request authenticate(Route route, Response response) {
      return null;
    }
  };

  Request authenticate(Route route, Response response) throws IOException;
}

關于授權的源碼實現如下:

class MyAuthenticator implements Authenticator {

        @Override
        public Request authenticate(Route route, Response response) throws IOException {
            String credential = Credentials.basic(...)

            Request.Builder builder=response.request().newBuilder();

            if(response.code()==401){
                builder .header("Authorization", credential);
            }else if(response.code()==407){
                builder .header("Proxy-Authorization", credential);
            }

            return  builder.build();

        }
    }

ConnectionPool

連接池,用于管理HTTP和SPDY連接的復用以減少網絡延遲,HTTP請求相同的Address時可以共享同一個連接。

DNS

DNS這里就不用介紹了,用于根據主機名來查詢對應的IP。

2.發起請求

使用OKHttp發送請求一般有兩種方式,一種是同步方式,一種是異步方式,如下

//異步方式
Request request = new Request.Builder()
                   .url("").build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});

//同步方式
try {
    Request request = new Request.Builder()
               .url("").build();
    Call call = mOkHttpClient.newCall(request);
    Response response = call.execute();
} catch (IOException e) {
    e.printStackTrace();
}

接下來我們分別從源碼角度分析下這兩種方式

首先是同步方式

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

這里收先調用了Dispatcher的executed方法,將這個請求加入runningAsyncCalls隊列中,然后調用getResponseWithInterceptorChain方法獲取Respone,這個就是我們請求后得到的回復,獲取后返回這個Respone,最后在finally調用了Dispatcher的finished方法,將請求從runningAsyncCalls隊列中移除

接下來是異步方式

  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

可以看到調用Dispatcher的enqueue方法傳遞了一個AsyncCall對象,注意這個AsyncCall對象繼承Runnable接口,所以在當在線程池中運行會調用AsyncCall中的execute方法,接下來我們看下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);
    }
  }
}

可以看到這里調用getResponseWithInterceptorChain方法獲取Respone,接下來通過回調傳遞出去,最后在finally調用了Dispatcher的finished方法,將請求從runningAsyncCalls隊列中移除

通過這兩個代碼分析,可以知道獲取Respone都是通過getResponseWithInterceptorChain方法,唯一的區別是一個是在主線程中,另外一個在線程池中的線程,接下來看一下getResponseWithInterceptorChain方法,如下

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    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, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
}

這里是真正發出網絡請求的地方,可以看到這里有很多個Interceptor,Interceptor是OkHttp中最核心的一個東西,它把實際的網絡請求、緩存、透明壓縮等功能都統一了起來,每一個功能都只是一個 Interceptor,它們再連接成一個 Interceptor.Chain,環環相扣,最終圓滿完成一次網絡請求。

從 getResponseWithInterceptorChain 函數我們可以看到,Interceptor.Chain 的分布依次是:

![](http://i4.buimg.com/519918/6d74ff7c4d531915.png =350x434)

流程如下:

1.在配置 OkHttpClient 時設置的 interceptors;

2.負責失敗重試以及重定向的 RetryAndFollowUpInterceptor;

3.負責把用戶構造的請求轉換為發送到服務器的請求、把服務器返回的響應轉換為用戶友好的響應的 BridgeInterceptor;

4.負責讀取緩存直接返回、更新緩存的 CacheInterceptor;

5.負責和服務器建立連接的 ConnectInterceptor;

6.配置 OkHttpClient 時設置的 networkInterceptors;

7.負責向服務器發送請求數據、從服務器讀取響應數據的 CallServerInterceptor。

這里很明顯的是使用了責任鏈模式,接下來就是分析一下每一個Interceptor究竟是干了什么事情

3.分析Interceptor

1.RetryAndFollowUpInterceptor 重試與重定向攔截器

這個攔截器主要用來實現重試與重定向的功能,核心代碼如下

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

  //初始化流分配器 
  streamAllocation = new StreamAllocation(
      client.connectionPool(), createAddress(request.url()));

  int followUpCount = 0;
  Response priorResponse = null;
  while (true) {//死循環
    //..
    //省略了部分源碼
    Response response = null;
    boolean releaseConnection = true;

    try {

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

    } catch (Exception e) {
      //..
    //省略了部分源碼
      releaseConnection = false;
      continue;
    } finally {

      if (releaseConnection) {
        streamAllocation.streamFailed(null);
        streamAllocation.release();
      }
    }


    //將上次的請求放入priorResponse中
    if (priorResponse != null) {
      response = response.newBuilder()
          .priorResponse(priorResponse.newBuilder()
              .body(null)
              .build())
          .build();
    }

    //檢查是否觸發重定向重試等條件,并返回Request
    Request followUp = followUpRequest(response);

    if (followUp == null) {//null表示無需重試
      if (!forWebSocket) {
        streamAllocation.release();
      }
      return response;//返回response
    }

    //..
    //省略了部分源碼

    request = followUp;
    priorResponse = response;
    //while循環進行下次請求
  }
}

通過代碼可以發現RetryAndFollowUpInterceptor內部通過while(true)死循環來進行重試獲取Response(有重試上限,超過會拋出異常)。followUpRequest主要用來根據響應碼來判斷屬于哪種行為觸發的重試和重定向(比如未授權,超時,重定向等),然后構建響應的Request進行下一次請求。當然,如果沒有觸發重新請求就會直接返回Response。

2.BridgeInterceptor 橋接攔截器

橋接攔截器,用于完善請求頭,比如Content-Type、Content-Length、Host、Connection、Accept-Encoding、User-Agent等等,這些請求頭不用用戶一一設置,如果用戶沒有設置該庫會檢查并自動完善。此外,這里會進行加載和回調cookie。

核心代碼如下:

@Override 
public Response intercept(Chain chain) throws IOException {
  Request userRequest = chain.request();
  Request.Builder requestBuilder = userRequest.newBuilder();

  RequestBody body = userRequest.body();
  //將用戶沒有寫入請求頭的內容自動補充進去,比如Content-Type、Content-Length、Host、Connection、Accept-Encoding、User-Agent等等
  if (body != null) {

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

    //..
  }
  //獲取cookie添加到請求頭中
  List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
  if (!cookies.isEmpty()) {
    requestBuilder.header("Cookie", cookieHeader(cookies));
  }
  //...
  Response networkResponse = chain.proceed(requestBuilder.build());

  //將響應cookie回調出去供用戶保存
  HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

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

    //...
    //省略了部分源碼
    responseBuilder.headers(strippedHeaders);
    responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
  return responseBuilder.build();
}

3.CacheInterceptor 緩存攔截器

緩存攔截器首先根據Request中獲取緩存的Response,然后根據用于設置的緩存策略來進一步判斷緩存的Response是否可用以及是否發送網絡請求(CacheControl.FORCE_CACHE因為不會發送網絡請求,所以networkRequest一定為空)。如果從網絡中讀取,此時再次根據緩存策略來決定是否緩存響應。

核心代碼如下:

@Override 
public Response intercept(Chain chain) throws IOException {
    //通過Request從緩存中獲取Response
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    //根據請求頭獲取用戶指定的緩存策略,并根據緩存策略來獲取networkRequest,cacheResponse。cacheResponse為null表示當前策略就算有緩存也不讀緩存
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;//表示發往網絡的request,不請求網絡應為null
    Response cacheResponse = strategy.cacheResponse;//返回從緩存中讀取的response

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


    if (cacheCandidate != null && cacheResponse == null) {
      //cacheResponse表示不讀緩存,那么cacheCandidate不可用,關閉它
      closeQuietly(cacheCandidate.body()); 
    }

    //..
    //省略了部分源碼
    //返回從緩存中讀取的Response
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

      Response networkResponse = null;
      //..
      //省略了部分源碼

      //獲取網絡Response
      networkResponse = chain.proceed(networkRequest);

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (HttpHeaders.hasBody(response)) {
      //如果可以緩存(用戶允許,響應也允許)就進行緩存到本地
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      response = cacheWritingResponse(cacheRequest, response);
    }

    return response;
}

配置緩存策略的方法如下:

Request request = new Request.Builder()
                .cacheControl(CacheControl.FORCE_NETWORK)
                .url("")
                .build();

4.ConnectInterceptor 連接攔截器

連接攔截器,用于打開一個連接到遠程服務器。

核心代碼如下

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

    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //獲取HttpStream
    HttpStream httpStream = streamAllocation.newStream(client, doExtensiveHealthChecks);
    //獲取RealConnection
    RealConnection connection = streamAllocation.connection();

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

實際上建立連接就是創建了一個HttpCodec對象,它將在后面的步驟中被使用,那它又是何方神圣呢?它是對HTTP協議操作的抽象,有兩個實現:Http1Codec和Http2Codec,顧名思義,它們分別對應HTTP/1.1和HTTP/2版本的實現。

在Http1Codec中,它利用Okio對Socket的讀寫操作進行封裝,我們對它們保持一個簡單地認識:它對java.io和java.nio進行了封裝,讓我們更便捷高效的進行IO操作。

5.CallServerInterceptor 調用服務攔截器

調用服務攔截器是攔截鏈中的最后一個攔截器,通過網絡與調用服務器。通過HttpStream依次次進行寫請求頭、請求頭(可選)、讀響應頭、讀響應體。

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

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

    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
    //寫請求體
      Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();
    }

    httpStream.finishRequest();

    //獲取Response。
    Response response = httpStream.readResponseHeaders()
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    //寫入Response的body
    if (!forWebSocket || response.code() != 101) {
      response = response.newBuilder()
          .body(httpStream.openResponseBody(response))
          .build();
    }

    //...
    return response;
}

這里主要做的事情:

1.向服務器發送request header;

2.如果有request body,就向服務器發送;

3.讀取response header,先構造一個Response對象;

4.如果有response body,就在3的基礎上加上body構造一個新的Response對象;

這里我們可以看到,核心工作都由HttpCodec對象完成,而HttpCodec實際上利用的是Okio,而Okio實際上還是用的Socket。

到這里,一個請求的流程就基本走完了。接下來說一下OKHttp中比較有意思的點。

OKHttp有意思的點

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,517評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,087評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,521評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,493評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,207評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,603評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,624評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,813評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,364評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,110評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,305評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,874評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,532評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,953評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,209評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,033評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,268評論 2 375

推薦閱讀更多精彩內容