okhttp源碼分析(二)-RetryAndFollowUpInterceptor過濾器

1.okhttp源碼分析(一)——基本流程(超詳細)
2.okhttp源碼分析(二)——RetryAndFollowUpInterceptor過濾器
3.okhttp源碼分析(三)——CacheInterceptor過濾器
4.okhttp源碼分析(四)——ConnectInterceptor過濾器
5.okhttp源碼分析(五)——CallServerInterceptor過濾器

前言

緊接著上一篇基本流程分析完后,準備的是將過濾器分析分析,過濾器可以說是OkHttp的點睛之筆。這一篇主要分析RetryAndFollowUpInterceptor這個過濾器,首先從名字我們就可以明白這個過濾器的職責(zé)是重試和重定向。

分析

1.宏觀流程

讀過上一篇博客的人應(yīng)該都清楚看一個過濾器的功能,重點都在他重寫Interceptor的intercept(Chain chain)方法。
打開源碼突然發(fā)現(xiàn)這么長。。。不要絕望,我按照我的理解將代碼處理一下,我們先從流程看起,至少從宏觀上要對這個過濾器有個認識。

@Override public Response intercept(Chain chain) throws IOException {
    。。。
    while (true) {
    。。。
      try {
        response = realChain.proceed(request, streamAllocation, null, null);
      }
    。。。
    if(滿足條件){
        return response;
    }
    。。。
      //不滿足條件,一頓操作,賦值再來!
      request = followUp;
      priorResponse = response;
    }
  }

其實就流程來上,我認為宏觀上代碼縮減到這樣就夠了,甚至可以再刪點,這里先從流程上理解.
其實代碼成這樣,基本上大家都能理解了,一個while(true)表明這是個循環(huán)體,循環(huán)體主要做的事可以看到其實是遞歸的主要方法。

response = realChain.proceed(request, streamAllocation, null, null);

執(zhí)行了這個方法后,就會交給下一個過濾器繼續(xù)執(zhí)行,所以單從這里來看,我們可以簡單的理解為這個過濾器其實沒做什么。
但是當(dāng)出現(xiàn)了一些問題,導(dǎo)致不滿足條件的時候,就需要進行一系列的操作,重新復(fù)制Request,重新請求,這也就是while的功能,對應(yīng)的也就是這個過濾器的主要功能:重試和重定向。
這里我們宏觀上已經(jīng)對RetryAndFollowUpInterceptor有了一個基本的理解了。

2.過程細節(jié)

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();
    //streamAllocation的創(chuàng)建位置
    streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()),
        call, eventListener, callStackTrace);

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

      Response response;
      boolean releaseConnection = true;
      try {
        response = realChain.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.
        //先判斷當(dāng)前請求是否已經(jīng)發(fā)送了
        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.
      //這里基本上都沒有講,priorResponse是用來保存前一個Resposne的,這里可以看到將前一個Response和當(dāng)前的Resposne
      //結(jié)合在一起了,對應(yīng)的場景是,當(dāng)獲得Resposne后,發(fā)現(xiàn)需要重定向,則將當(dāng)前Resposne設(shè)置給priorResponse,再執(zhí)行一遍流程,
      //直到不需要重定向了,則將priorResponse和Resposne結(jié)合起來。
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }
      //判斷是否需要重定向,如果需要重定向則返回一個重定向的Request,沒有則為null
      Request followUp = followUpRequest(response);

      if (followUp == null) {
        //不需要重定向
        if (!forWebSocket) {
          //是WebSocket,釋放
          streamAllocation.release();
        }
        //返回response
        return response;
      }
      //需要重定向,關(guān)閉響應(yīng)流
      closeQuietly(response.body());
      //重定向次數(shù)++,并且小于最大重定向次數(shù)MAX_FOLLOW_UPS(20)
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      //是UnrepeatableRequestBody, 剛才看過也就是是流類型,沒有被緩存,不能重定向
      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }
      //判斷是否相同,不然重新創(chuàng)建一個streamConnection
      if (!sameConnection(response, followUp.url())) {
        streamAllocation.release();
        streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(followUp.url()), call, eventListener, 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;
    }
  }

現(xiàn)在就要從源碼上具體學(xué)習(xí)理解這個過濾器了。這里我具體一點一點分析。

//streamAllocation的創(chuàng)建位置
    streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()),
        call, eventListener, callStackTrace);

首先第一個點,這里可以看到這里對streamAllocation進行了初始化操作,其實在過濾器的鏈式調(diào)用的過程中會陸陸續(xù)續(xù)創(chuàng)建一系列對應(yīng)的參數(shù),這一點從最初的創(chuàng)建Chain的時候就可以看出來,可以看到一開始有很多參數(shù)是以null傳入的。

Response getResponseWithInterceptorChain() throws IOException {
    。。。
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    。。。
    return chain.proceed(originalRequest);
  }

這里先大概說一下StreamAllocation這個對象是干什么的。這個類大概可以理解為是處理Connections,Streams,Calls三者之間的關(guān)系,這一點其實從構(gòu)造函數(shù)的傳參也可以看出來。
接下來就要進入循環(huán)體中看了,首先可以看到當(dāng)請求被取消的時候,會跳出循環(huán)體(第一種跳出的情況)。

    boolean releaseConnection = true;
    try {
        response = realChain.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;
      }

接下來看try catch體中的內(nèi)容,try其實很簡單,就是執(zhí)行后續(xù)過濾器鏈中的東西,這里要稍微注意一下releaseConnection這個變量的,對后續(xù)的判斷理解是有影響的,可以看到初始化時將releaseConnection這個變量賦值為true。
下面是重點內(nèi)容了:
進入catch體中,可以看到會捕獲很多okHttp自定義的Exception,從名字上可以有一個大體上的理解,但是還是要從源碼上分析,這里先看第一個異常RouteException,先理解理解注釋:嘗試連接一個路由失敗,這個請求還沒有被發(fā)出,接下來執(zhí)行了一個方法recover(),這里注意一下false參數(shù),現(xiàn)在進入方法體中。

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

    // The application layer has forbidden retries.
    //如果OkHttpClient直接配置拒絕失敗重連,return false
    if (!client.retryOnConnectionFailure()) return false;

    // We can't send the request body again.
    //如果請求已經(jīng)發(fā)送,并且這個請求體是一個UnrepeatableRequestBody類型,則不能重試。
    //StreamedRequestBody實現(xiàn)了UnrepeatableRequestBody接口,是個流類型,不會被緩存,所以只能執(zhí)行一次,具體可看。
    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;
  }

可以看到這里面有很多的if判斷,這里先看第一個。

if (!client.retryOnConnectionFailure()) return false;

//==========OkHttpClient.java===========
public boolean retryOnConnectionFailure() {
    return retryOnConnectionFailure;
  }

代碼一放上來其實就很好理解了,如果我們在配置OkHttpClient中配置retryOnConnectionFailure屬性為false,表明拒絕失敗重連,那么這里返回false(第一種拒絕重連的方式)。這里另外說明一下如果我們默認方式創(chuàng)建OkHttpClient的話,retryOnConnectionFailure屬性是true

if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

下面一個判斷首先要明白參數(shù)的含義,這里requestSendStarted這個參數(shù)就是剛才在recover方法中的第二個參數(shù),是為了表明請求是否已經(jīng)被發(fā)送,這里這里為false,但是這個判斷我們需要了解清楚。
單單從判斷條件我們可以理解為:如果請求已經(jīng)發(fā)送,并且這個請求體是一個UnrepeatableRequestBody類型,則不能重試(第二種拒絕重連的方式)。
這里就要說明一下UnrepeatableRequestBody這個類了。

public interface UnrepeatableRequestBody {
}

這就是UnrepeatableRequestBody的源碼,沒有看錯...就是一個空的接口,作用就是標記那些不能被重復(fù)請求的請求體,這時候可能就想要了解一下那些請求是不能被重復(fù)請求的哪?看一下那些Request實現(xiàn)了這個接口,結(jié)果會發(fā)現(xiàn),到目前Okhttp源碼中,只有一種請求實現(xiàn)了這個接口,那就是StreamedRequestBody。

/**
 * This request body streams bytes from an application thread to an OkHttp dispatcher thread via a
 * pipe. Because the data is not buffered it can only be transmitted once.
 */
  final class StreamedRequestBody extends OutputStreamRequestBody implements UnrepeatableRequestBody {}

從這個類的注釋我們也可以理解,StreamedRequestBody實現(xiàn)了UnrepeatableRequestBody接口,是個流類型,不會被緩存,所以只能執(zhí)行一次。

if (!isRecoverable(e, requestSendStarted)) return false;

//=====================isRecoverable()=====================
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // If there was a protocol problem, don't recover.
    //如果是協(xié)議問題,不要在重試了
    if (e instanceof ProtocolException) {
      return false;
    }

    // If there was an interruption don't recover, but if there was a timeout connecting to a route
    // we should try the next route (if there is one).
    if (e instanceof InterruptedIOException) {
      //超時問題,并且請求還沒有被發(fā)送,可以重試
      //其他就不要重試了
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }

    // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
    // again with a different route.
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      //理解為如果是安全原因,就不要重試了
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    if (e instanceof SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      //安全原因
      return false;
    }

    // An example of one we might want to retry with a different route is a problem connecting to a
    // proxy and would manifest as a standard IOException. Unless it is one we know we should not
    // retry, we return true and try a new route.
    return true;
  }

下面一個判斷可以總體理解為:如果是一些嚴重的問題(協(xié)議,安全...),拒絕重試(第三種拒絕重連的方式)
這里可以看到判斷被歸結(jié)到一個isRecoverable()的方法中,注釋頁寫的很清楚,這里嚴重的情況主要由這幾種:

  • 1.協(xié)議問題,不能重試。
  • 2.如果是超時問題,并且請求沒有被發(fā)送,可以重試,其他的就不要重試了。
  • 3.安全問題,不要重試。
if (!streamAllocation.hasMoreRoutes()) return false;

//========================StreamAllocation.java=====================
public boolean hasMoreRoutes() {
    return route != null
        || (routeSelection != null && routeSelection.hasNext())
        || routeSelector.hasNext();
  }

下面這個判斷表明:沒有更多的可以使用的路由,則不要重試了(第四種拒絕重連的方式)
這里也列出了hasMoreRoutes()方法,可以看到,這里面當(dāng)游標在最末尾,也就是保存的路由的容器已經(jīng)遍歷完了,也就沒辦法繼續(xù)重試了。這里大概說明一下routeSelection是用List保存的。

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;
      }

所以當(dāng)以上判斷結(jié)束后,如果需要重試,則continue,重新執(zhí)行while循環(huán)體,也就是發(fā)揮了這個過濾器的作用,重試

catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        //先判斷當(dāng)前請求是否已經(jīng)發(fā)送了
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        //同樣的重試判斷
        if (!recover(e, requestSendStarted, request)) throw e;
        releaseConnection = false;
        //重試。。。
        continue;
      }

這時候看一下下一個異常IOException,首先可以看到,需要先判斷請求是否已經(jīng)發(fā)送了,緊接著繼續(xù)剛才分析的方法recover(),這時默認傳的就不是false,而是判斷得到的requestSendStarted。最后同樣當(dāng)需要重試時,continue。

finally {
        // We're throwing an unchecked exception. Release any resources.
        //沒有捕獲到的異常,最終要釋放
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

finally體中的內(nèi)容比較好理解,由于releaseConnection初始化為true,而當(dāng)正常執(zhí)行realChain.proceed或在執(zhí)行過程中捕捉到異常時設(shè)置為false,所以當(dāng)執(zhí)行過程中捕捉到?jīng)]有檢測到的異常時,需要釋放一些內(nèi)容。(此處感謝@messi_wpy的指正)

if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

接下來這段代碼一開始是我比較難以理解的,而且網(wǎng)上其他分析這個過濾器的都沒有分析這塊,最后自己分析,理解為priorResponse是用來保存前一個Resposne的,這里可以看到將前一個Response和當(dāng)前的Resposne結(jié)合在一起了。對應(yīng)的場景是:當(dāng)獲得Resposne后,發(fā)現(xiàn)需要重定向,則將當(dāng)前Resposne設(shè)置給priorResponse,再執(zhí)行一遍流程,直到不需要重定向了,則將priorResponse和Resposne結(jié)合起來。

   Request followUp = followUpRequest(response);

//=========================followUpRequest()==============================
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");
        }
        //重新構(gòu)造了一個Request
        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 (!client.retryOnConnectionFailure()) {
          // The application layer has directed us not to retry the request.
          return null;
        }

        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
          return null;
        }

        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        return userResponse.request();

      default:
        return null;
    }
  }

下面這行代碼主要是對followUpRequest()這個方法的理解,代碼我也粘出來了,這里其實沒必要在意每一行代碼,這樣反而影響我們閱讀,這里主要可以觀察發(fā)現(xiàn),其實這個方法的主要操作就是,當(dāng)返回碼滿足某些條件時就重新構(gòu)造一個Request,不滿足就返回null,所以接下來的代碼就很容易理解了。

if (followUp == null) {
        //不需要重定向
        if (!forWebSocket) {
          //是WebSocket,釋放
          streamAllocation.release();
        }
        //返回response
        return response;
      }

當(dāng)不需要重定向,也就是返回的為null,直接返回response。

      //需要重定向,關(guān)閉響應(yīng)流
      closeQuietly(response.body());
      //重定向次數(shù)++,并且小于最大重定向次數(shù)MAX_FOLLOW_UPS(20)
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
      //是UnrepeatableRequestBody, 剛才看過也就是是流類型,沒有被緩存,不能重定向
      if (followUp.body() instanceof UnrepeatableRequestBody) {
        streamAllocation.release();
        throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
      }
      //判斷是否相同,不然重新創(chuàng)建一個streamConnection
      if (!sameConnection(response, followUp.url())) {
        streamAllocation.release();
        streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(followUp.url()), call, eventListener, 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;

//==========================sameConnection()======================
private boolean sameConnection(Response response, HttpUrl followUp) {
    HttpUrl url = response.request().url();
    return url.host().equals(followUp.host())
        && url.port() == followUp.port()
        && url.scheme().equals(followUp.scheme());
  }

下面的代碼當(dāng)然就是當(dāng)返回的不為空,也就是重新構(gòu)造了一個Request,需要重定向。

  • 1.首先關(guān)閉響應(yīng)流。
  • 2.增加重定向的次數(shù),保證小于最大重定向次數(shù)MAX_FOLLOW_UPS(20)
  • 3.不能是UnrepeatableRequestBody類型,剛才也分析過,是一個空接口,用于標記那些只能請求一次的請求。
  • 4.判斷是否相同,如果不相同,則需要重新創(chuàng)建一個streamConnection。
  • 5.重新賦值,結(jié)束當(dāng)前循環(huán),繼續(xù)while循環(huán),也就是執(zhí)行重定向請求。

總結(jié)

到此,RetryAndFollowUpInterceptor這個過濾器已經(jīng)大體分析完了,總體流程下來可以發(fā)現(xiàn),這個過濾器的主要作用就是用于對請求的重試和重定向的。
其中拒絕重試的判斷條件有如下幾種:

  • 1.如果我們在配置OkHttpClient中配置retryOnConnectionFailure屬性為false,表明拒絕失敗重連,那么這里返回false
  • 2.如果請求已經(jīng)發(fā)送,并且這個請求體是一個UnrepeatableRequestBody類型,則不能重試
  • 3.如果是一些嚴重的問題(協(xié)議,安全...),拒絕重試
  • 4.沒有更多的可以使用的路由,則不要重試了
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,554評論 2 379

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