okhttp源碼學(xué)習(xí)筆記(二)-- 連接與連接管理

本篇文章為okhttp源碼學(xué)習(xí)筆記系列的第二篇文章,本篇文章的主要內(nèi)容為okhttp中的連接與連接的管理,因此需要重點(diǎn)介紹連接的概念。客戶端通過HTTP協(xié)議與服務(wù)器進(jìn)行通信,首先需要建立連接,okhttp并沒有使用URLConnection, 而是對socket直接進(jìn)行封裝,在socket之上建立了connection的概念,代表這物理連接。同時,一對請求與響應(yīng)對應(yīng)著輸出和輸入流, okhttp中同樣使用流的邏輯概念,建立在connection的物理連接之上進(jìn)行數(shù)據(jù)通信,(只不過不知道為什么okhttp中流的類名為HttpCodec)。在HTTP1.1以及之前的協(xié)議中,一個連接只能同時支持單個流,而SPDY和HTTP2.0協(xié)議則可以同時支持多個流,本文先考慮HTTP1.1協(xié)議的情況。connection負(fù)責(zé)與遠(yuǎn)程服務(wù)器建立連接, HttpCodec負(fù)責(zé)請求的寫入與響應(yīng)的讀出,但是除此之外還存在在一個連接上建立新的流,取消一個請求對應(yīng)的流,釋放完成任務(wù)的流等操作,為了使HttpCodec不至于過于復(fù)雜,okhttp中引入了StreamAllocation負(fù)責(zé)管理一個連接上的流,同時在connection中也通過一個StreamAllocation的引用的列表來管理一個連接的流,從而使得連接與流之間解耦。另外,熟悉HTTP協(xié)議的同學(xué)肯定知道建立連接是昂貴的,因此在請求任務(wù)完成以后(即流結(jié)束以后)不會立即關(guān)閉連接,使得連接可以復(fù)用,okhttp中通過ConnecitonPool來完成連接的管理和復(fù)用。

因此本文會首先從兩個攔截器開始引入StreamAllocation的概念,其次會依次介紹Connection和的一個實(shí)現(xiàn)類Connection,最后再通過分析ConnectionPool來分析連接的管理機(jī)制,期間還有涉及Rout, RoutSelector等一些為建立連接或建立流有關(guān)的輔助類。

0. 從兩個攔截器開始

在上一篇文章中我們了解到okhttp中的網(wǎng)絡(luò)請求過程其實(shí)就是一系列攔截器的處理過程,那么我們就可以以這些攔截器為主線看一下okhttp的實(shí)現(xiàn)中都做了哪些事情,首先我們再次貼出RealCall#getResponseWithInterceptor方法:

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

從方法中可以看出okhttp內(nèi)部定義了五個攔截器,分別負(fù)責(zé)重試(包括失敗重連,添加認(rèn)證頭部信息,重定向等),request和response的轉(zhuǎn)換(主要是將Request和Response對象轉(zhuǎn)換成Http協(xié)議定義的報(bào)文形式), 緩存以及連接處理,最后一個與其他稍有不同,它負(fù)責(zé)流的處理, 將請求數(shù)據(jù)寫入到socket的輸出流,并從輸入流中獲取響應(yīng)數(shù)據(jù),這個在第三篇文章中做分析。
本篇文章的重點(diǎn)是連接以及連接管理,可以看出與連接有關(guān)的時RetryAndFollowUpInterceptor和ConnectionInterceptor,那么我們就從這兩個攔截器開始分析。
在前面一篇文章中我們了解到攔截器會對外提供一個方法,即intercept(chain)方法,在該方法中處理請求,并調(diào)用chain.proceed()方法獲取響應(yīng),處理后返回響應(yīng)結(jié)果,那么我們首先來看RetryAndFollowUpInterceptor#intercept()方法:

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

    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);

    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 (... e) {
        ...
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
      ...

這里的代碼只是前一半,并且做了部分簡化,由于本篇文章分析的是連接與連接管理,因此這里我們只關(guān)注與連接相關(guān)的部分。這里我們看到在該方法中,首先創(chuàng)建了StreamAllocation對象,該對象是連接與流的橋梁,okhttp處理一個請求時,它負(fù)責(zé)為該請求找到一個合適的連接(找到是指從連接池中復(fù)用連接或者創(chuàng)建新連接), 并在該連接上創(chuàng)建流對象,通過該流對象完成數(shù)據(jù)通信。這里的流對象負(fù)責(zé)數(shù)據(jù)通信,即向socket的輸出流中寫請求數(shù)據(jù),并從socket的輸出流中讀取響應(yīng)數(shù)據(jù),而StreamAllocation則負(fù)責(zé)管理流,包括為請求查找連接,并在該連接上建立流對象,將連接封裝的socket對象中的輸入輸出流傳遞到okhttp流對象中,使他去完成數(shù)據(jù)通信任務(wù),這是一種功能或責(zé)任的分離,這一點(diǎn)有點(diǎn)像之前分析的Request對象和Call對象的關(guān)系,另外一個連接也是通過持有一個關(guān)于StreamAllocation的集合來管理一個連接上的多個流(只有在SPDY和HTTP2上存在一個連接上多個流,HTTP1.1及1.0則是在一個連接上同時只有一個流對象)。
從代碼中我們看到,創(chuàng)建的streamAllocation對象傳遞到proceed()方法中,之前對于攔截器鏈的分析中我們知道,在proceed()方法中遞歸地創(chuàng)建新的chain對象,并添加proceed()方法中傳遞進(jìn)來的對象,提供給后面的攔截器使用,這包括StreamAllocation, Connection, HttpCodec三個主要的類的對象,其中最后一個就是所謂的流對象(不太清楚okhttp為何如此命名)。
RetryAndFollowUpInterceptor#intercept()方法中其他的邏輯我們在之后的文章中再做分析,這里我們主要是需要了解StreamAllocation對象,以及繼續(xù)熟悉okhttp中攔截器鏈的執(zhí)行機(jī)制,下面我們再來分析Connection#intercept()方法:

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

這里沒有對代碼做簡化,可以看出ConnectionInterceptor的代碼很簡單,我們暫且忽略doExtensiveHealthChecks,這個跟具體的HTTP協(xié)議的規(guī)則有關(guān), 這里最關(guān)鍵的是streamAllocation的方法,即newStream()方法,該方法負(fù)責(zé)尋找連接,并在該連接上建立新的流對象,并返回,而connection()方法只不過是分會找到的連接而已,這里看到三個對象在這時都已經(jīng)分別創(chuàng)建出來,此時傳遞到InterceptorChain上,CallServerInterceptor就可以使用這些對象完成數(shù)據(jù)通信了,這一點(diǎn)在下一篇文章中分析流對象時再做分析,這里我們只是關(guān)注連接,那么我們就要StreamAllocation開始。

1. 連接與流的橋梁

首先我們明白HTTP通信執(zhí)行網(wǎng)絡(luò)請求需要在連接之上建立一個新的流執(zhí)行該任務(wù), 我們這里將StreamAllocation稱之為連接與流的橋梁, 它負(fù)責(zé)為一次請求尋找連接并建立流,從而完成遠(yuǎn)程通信,所以StreamAllocation與請求,連接,流都相關(guān),因此我們首先熟悉一下這三個概念,對于它們?nèi)齻€, StreamAllocation類之前的注釋已經(jīng)給出了解釋,這里我們首先看一下它的注釋:

/**
 * This class coordinates the relationship between three entities:
 *
 * <ul>
 *     <li><strong>Connections:</strong> physical socket connections to remote servers. These are
 *         potentially slow to establish so it is necessary to be able to cancel a connection
 *         currently being connected.
 *     <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on
 *         connections. Each connection has its own allocation limit, which defines how many
 *         concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
 *         at a time, HTTP/2 typically carry multiple.
 *     <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
 *         its follow up requests. We prefer to keep all streams of a single call on the same
 *         connection for better behavior and locality.
 * </ul>
 ...
 **/

注釋說明的很清楚,Connection時建立在Socket之上的物理通信信道,而Stream則是代表邏輯的HTTP請求/響應(yīng)對, 至于Call,是對一次請求任務(wù)或是說請求過程的封裝,在第一篇文章中我們已經(jīng)做出介紹,而一個Call可能會涉及多個流(如請求重定向,auth認(rèn)證等情況), 而Okhttp使用同一個連接完成這一系列的流上的請求任務(wù),這一點(diǎn)的實(shí)現(xiàn)我們將在介紹RetryInterceptor的部分中說明。

下面我們來思考StreamAllocation所要解決的問題,簡單來講就是在合適的連接之上建立一個新的流,這個問題劃分為兩步就是尋找連接和新建流。那么,StreamAllocation的數(shù)據(jù)結(jié)構(gòu)中應(yīng)該包含Stream(okhttp中將接口的名字定義為HttpCodec), Connction, 其次為了尋找合適的連接,應(yīng)該包含一個URL地址, 連接池ConnectionPool, 而方法則應(yīng)該包括我們前面提到的newStream()方法, 而findConnection則是為之服務(wù)的方法,其次在完成請求任務(wù)之后應(yīng)該有finish方法用來關(guān)閉流對象,還有終止和取消等方法, 以及釋放資源的方法。下面我們從StreamAllocation中尋找對應(yīng)的屬性和方法, 首先來看它的屬性域:

public final class StreamAllocation {
  public final Address address;
  private Route route;
  private final ConnectionPool connectionPool;
  private final Object callStackTrace;

  // State guarded by connectionPool.
  private final RouteSelector routeSelector;
  private int refusedStreamCount;
  private RealConnection connection;
  private boolean released;
  private boolean canceled;
  private HttpCodec codec;

  public StreamAllocation(ConnectionPool connectionPool, Address address, Object callStackTrace) {
    this.connectionPool = connectionPool;
    this.address = address;
    this.routeSelector = new RouteSelector(address, routeDatabase());
    this.callStackTrace = callStackTrace;
  }
...

這里我們看并沒有使用URL代表一個地址,而是使用Address對象, 如果查看其代碼可以看到它封裝了一個URL, 其中還包括dns, 代理等信息,可以更好地滿足HTTP協(xié)議中規(guī)定的細(xì)節(jié)。使用Address對象也可以直接ConnectionPool中查找對應(yīng)滿足條件的連接,同時Address對象可以用于在RoutSelector查詢一個合適的路徑Rout對象,該Route對象可以用于建立連接, 然后我們忽略標(biāo)志位和統(tǒng)計(jì)信息,剩下的則是Connection和HttpCodec對象,這里我們先忽略調(diào)用棧callStackTrace, 不考慮。以上就是它所有的屬性域,那么下面我們再來看它最重要的方法,即新建流的方法:

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;
      if (resultConnection.http2Connection != null) {
        resultCodec = new Http2Codec(client, this, resultConnection.http2Connection);
      } else {
        resultConnection.socket().setSoTimeout(readTimeout);
        resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
        resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
        resultCodec = new Http1Codec(
            client, this, resultConnection.source, resultConnection.sink);
      }

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

其流程很明確,我們先不考慮HTTP2.0的情況, 流程就是找到合適的良好連接,然后實(shí)例化HttpCodec對象,并設(shè)置對應(yīng)的屬性,返回該流對象即可,該流對象依賴連接的輸入流和輸出流,從而可以在流之上進(jìn)行請求的寫入和響應(yīng)的讀出。下面來開findConnection的代碼:

 /**
  * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
  * until a healthy connection is found.
  */
 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) {
       //successCount記錄該連接上執(zhí)行流任務(wù)的次數(shù),為零說明是新建立的連接
       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(); //在該方法中會設(shè)置該Allocation對應(yīng)的Connection對象的noNewStream標(biāo)志位,標(biāo)識這在該連接不再使用,在回收的線程中會將其回收
       continue;
     }

     return candidate;
   }
 }

 /**
  * Returns a connection to host a new stream. This prefers the existing connection if it exists,
  * then the pool, finally building a new connection.
  */
 private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
     boolean connectionRetryEnabled) throws IOException {
   Route selectedRoute;
   synchronized (connectionPool) {

   //一系列條件判斷
   ...

     RealConnection allocatedConnection = this.connection;
     if (allocatedConnection != null && !allocatedConnection.noNewStreams) {//noNewStream是一個標(biāo)識為,標(biāo)識該連接不可用
       return allocatedConnection;
     }

     // Attempt to get a connection from the pool.
     //可以在OkhttpClient中查看到該方法, 其實(shí)就是調(diào)用connnctionPool.get(address, streamAllocation);

     RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);  
     if (pooledConnection != null) {
       this.connection = pooledConnection;
       return pooledConnection;
     }

     selectedRoute = route;
   }

   if (selectedRoute == null) {
     selectedRoute = routeSelector.next(); //選擇下一個路線Rout
     synchronized (connectionPool) {
       route = selectedRoute;
       refusedStreamCount = 0;
     }
   }
   RealConnection newConnection = new RealConnection(selectedRoute);

   synchronized (connectionPool) {
     acquire(newConnection);   //1. 將該StreamAllocation對象,即this 添加到Connection對象的StreamAllocation引用列表中,標(biāo)識在建立新的流使用到了該連接
     Internal.instance.put(connectionPool, newConnection);  //2. 將新建的連接加入到連接池, 與get方法類型,也是在OkHttpClient調(diào)用的pool.put()方法
     this.connection = newConnection;
     if (canceled) throw new IOException("Canceled");
   }

   newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),
       connectionRetryEnabled);   //3. 連接方法,將在介紹Connection的部分介紹
   routeDatabase().connected(newConnection.route());  //4.新建的連接一定可用,所以將該連接移除黑名單

   return newConnection;
 }

 ...
 /**
  * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to
  * {@link #release} on the same connection.
  */
 public void acquire(RealConnection connection) {
   assert (Thread.holdsLock(connectionPool));
   connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
 }

這兩個方法完成了尋找合適的連接的功能,這里我們可以將其分成兩種類型:

  1. 從連接池中復(fù)用連接,這一點(diǎn)我們在連接管理部分中會有介紹,其實(shí)就是從維護(hù)的隊(duì)列中找到合適的連接并返回,查找的依據(jù)就是Address對象;
  2. 新建連接,新建連接需要首先查找一個合適的路徑,然后在該路徑上實(shí)例化一個新的connection對象,在建立一個新的連接以后需要執(zhí)行一系列的步驟,在代碼中已經(jīng)用注釋的方式分四步標(biāo)出。
    在或許到連接以后還需要執(zhí)行檢查過程,通常來說以上兩種類型選擇的連接都需要執(zhí)行檢查,只是這里新的連接一定可用,所以會跳過檢查,其實(shí)檢查的過程就是查看該連接的socket是否被關(guān)閉以及是否可以正常使用,有興趣的可以自行查看代碼。
    在建立新的連接以后需要處理一些事情,代碼中的中文注釋分了四步將其標(biāo)出,其中需要說明第一步,acquire方法,connection中維護(hù)這一張?jiān)谝粋€連接上的流的鏈表,該鏈表保存的是StreamAllocation的引用 Connction的該鏈表為空時說明該連接已經(jīng)可以回收了,這部分在連接管理部分會有說明。其實(shí)在調(diào)用ConnectionPool.get()方法時,傳入StreamAllocation對象也是為了調(diào)用StreamAllocation#acquire方法,將該StreamAllocation對象的引用添加到連接對應(yīng)的鏈表中,用于管理一個連接上的流。

此外還需要說明的兩點(diǎn),一是關(guān)于Internal, 這是一個抽象類,該類只有一個實(shí)現(xiàn)類,時HttpClient的一個匿名內(nèi)部類,該類的一系列方法都是一個功能,就是將okhttp3中一些包訪問權(quán)限的方法對外提供一個public的訪問方法,至于為什么這么實(shí)現(xiàn),目前還不太清楚,估計(jì)是在Okhttp的框架中方便使用包權(quán)限的方法, 如ConnectionPool的put和get方法,在Okhttp的框架代碼中通過Internal代理訪問, 而在外部使用時無法訪問到這些方法,至于還有沒有其他考慮有清楚的同學(xué)歡迎賜教。
需要說明的第二點(diǎn)是RouteDatabase是一個黑名單,記錄著不可用的路線,避免在同一個坑里栽倒兩次,connect()方法就是將該路線移除黑名單,這里為了不影響StreamAllocation分析的連貫性,本文將RoutSelector和RouteDatabase等與路線選擇相關(guān)的代碼放到附錄部分,這里我們暫且先了解它的功能而不關(guān)注它的實(shí)現(xiàn)。

至此就分析完了建立新流的過程,那么剩下的就是釋放資源的邏輯,由于這一部分很多地方都與連接的復(fù)用管理,流的操作以及RetryAndFollowUpInterceptor中的邏輯有關(guān),所以在這里暫且略過,后續(xù)部分再做分析,我們此處重點(diǎn)是了解如何查詢連接,如何新建連接以及如何新建流對象。

2. 連接

下面開始分析連接對象,在okhttp中定義了Connection的接口,而該接口在okhttp只有一個實(shí)現(xiàn)類,即RealConnetion,下面我們重點(diǎn)分析該類。該類的主要功能就是封裝Socket并對外提供輸入輸出流,那么它的內(nèi)部結(jié)構(gòu)也很容易聯(lián)想到,它應(yīng)該持有一個Socket, 并提供一個輸入流一個輸入流,在功能方法中對外提供connect()方法建立連接。這里由于okhttp是支持Https和HTTP2.0,如果不考慮這兩種情況,RealConnection的代碼將會比較簡單,下面首先來看它的屬性域:

public final class RealConnection extends Http2Connection.Listener implements Connection {
  private final Route route;

  /** The low-level TCP socket. */
  private Socket rawSocket;

  /**
   * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
   * {@link #rawSocket} itself if this connection does not use SSL.
   */
  public Socket socket;
...
  private Protocol protocol;
...
  public int successCount;
  public BufferedSource source;
  public BufferedSink sink;
  public int allocationLimit;
  public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
  public boolean noNewStreams;
  public long idleAtNanos = Long.MAX_VALUE;

  public RealConnection(Route route) {
    this.route = route;
  }
...

RealConnection通過一個Route路線(或者說路由或路徑)來建立連接,它封裝一個Socket, 由于考慮到Https的情況,socket有可能時SSLSocket或者RawSocket, 所以這里有兩個socket域, 一個在底層,一個在上層,由于我們不考慮Https的情況,那么兩個就是等價的,我們只需要明白內(nèi)部建立rawSocket, 對外提供socket就可以了(注釋中也已經(jīng)明白解釋)。另外除了輸入流和輸出流之外還有連接所使用到的協(xié)議,在連接方法中會用到,最后剩下的就是跟連接管理部分相關(guān)的統(tǒng)計(jì)信息,allocationLimit是分配流的數(shù)量上限,對應(yīng)HTTP1.1來說它就是1, allocations在StreamAllocation部分我們已經(jīng)熟悉,它是用來統(tǒng)計(jì)在一個連接上建立了哪些流,通過StreamAllocation的acquire方法和release方法可以將一個allocation對象添加到鏈表或者移除鏈表,(不太清楚這兩個方法放在connection中是不是更合理一些), noNewStream之前說過可以簡單理解為它標(biāo)識該連接已經(jīng)不可用,idleAtNanos記錄該連接處于空閑狀態(tài)的時間,這些將會在第三部分連接管理中做介紹,這里暫且略過不考慮。

下面就開始看它的連接方法, connect()方法

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) {
    if (protocol != null) throw new IllegalStateException("already connected");
    ...
    while (protocol == null) {
      try {
          buildConnection(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
      } catch (IOException e) {
        ...
    }
  }

這里對代碼做了最大的簡化,主要去掉了異常處理的部分以及Https需要考慮的部分,從代碼中可以看出,建立連接是通過判斷protocol是否為空來確定是否已經(jīng)建立 連接的, 下面就繼續(xù)看buildeConnection()方法:

 private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    connectSocket(connectTimeout, readTimeout);
    establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
  }

  private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    //根據(jù)是否需要代理建立不同的Socket
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    rawSocket.setSoTimeout(readTimeout);
    try {
      //內(nèi)部就是調(diào)用socket.connect(InetAddress, timeout)方法建立連接
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ...
    }

    //獲取輸入輸出流
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));
  }

  private void establishProtocol(int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    if (route.address().sslSocketFactory() != null) {
      ...
    } else {
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
    }

    if (protocol == Protocol.HTTP_2) {
      ...
    } else {
      //HTTP1.1及以下版本中, 每個連接只允許有一個流
      this.allocationLimit = 1;
    }
  }

這里同樣對代碼做了簡化,并在重要的地方做了注釋, 流程也很清楚,就是建立socket連接,并獲取輸入輸出流,然后設(shè)置正確的協(xié)議,此時connect()方法就可以跳出while循環(huán),完成連接。
至此,就完成了連接過程,所以如果除去HTTPs和HTTP2的部分,RealConnection的代碼很簡單,不過對于HTTPS和HTTP2的處理還是挺多,后續(xù)還會繼續(xù)學(xué)習(xí)。下面再看另外一個概念,流。

3. 連接管理

對于連接的管理主要是分析ConnectionPool,以連接池的形式管理連接的復(fù)用, okhttp中盡可能對于相同地址的遠(yuǎn)程通信復(fù)用同一個連接,這樣就節(jié)省了連接的代價。那么我們現(xiàn)在明白了ConnectionPool所要解決的問題就可以去思考它應(yīng)當(dāng)如何實(shí)現(xiàn),它應(yīng)當(dāng)具備的功能可以簡單分為三個方法,get, put, cleanup,即獲取添加和清理的方法。其中最重要也是最復(fù)雜的是清理,即在連接池放滿的情況下,如何選擇需要淘汰的連接。這里需要考慮的指標(biāo)包括連接的數(shù)量,連接的空閑時長等,那么下面我們來看ConnectionPool的代碼:

/**
 * Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that
 * share the same {@link Address} may share a {@link Connection}. This class implements the policy
 * of which connections to keep open for future use.
 */
public final class ConnectionPool {
  /**
   * Background threads are used to cleanup expired connections. There will be at most a single
   * thread running per connection pool. The thread pool executor permits the pool itself to be
   * garbage collected.
   */
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

  boolean cleanupRunning;
  private final Runnable cleanupRunnable = new Runnable() {
    ...
  };

  /** The maximum number of idle connections for each address. */
  private final int maxIdleConnections;
  private final long keepAliveDurationNs;

  private final Deque<RealConnection> connections = new ArrayDeque<>();
  final RouteDatabase routeDatabase = new RouteDatabase();


  /**
   * Create a new connection pool with tuning parameters appropriate for a single-user application.
   * The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
   * this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
   */
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
  }

首先來看它的屬性域,最主要的就是connections,可見在ConnectionPool內(nèi)部以隊(duì)列的方式存儲連接,而routeDatabase是一個黑名單,用來記錄不可用的route,但是看代碼目測ConnectionPool中并沒有使用它,可能后續(xù)還有其他用處,此處不做分析。剩下的就是與清理相關(guān)的,從名字則可以看出他們的用途,最開始的三個分別是執(zhí)行清理任務(wù)的線程池,標(biāo)志位以及清理任務(wù),而maxIdleConnections和keepAliveDurationNs則是清理中淘汰連接的指標(biāo),這里需要說明的是maxIdleConnections是值每個地址上最大的空閑連接數(shù)(如注釋所說),看來okhttp只是限制了與同一個遠(yuǎn)程服務(wù)器的空閑連接數(shù)量,對整體的空閑連接數(shù)量并沒有做限制,但是從代碼來看并不是如此,該值時標(biāo)識該連接池中整體的空閑連接的最大數(shù)量,當(dāng)一個連接數(shù)量超過這個值時則會觸發(fā)清理,這里留有疑問,不知注釋是何意, 另外還有一個疑問是okhttp并沒有限制一個連接池中連接的最大數(shù)量,而只是限制了連接池中的最大空閑連接數(shù)量,由于不懂HTTP協(xié)議,水平也有限,不太清楚這里需不需要限制,有了解的歡迎在評論中告知,不勝感激。
最后我們可以從默認(rèn)的構(gòu)造器中看出okhttp允許每個地址同時可以有五個連接,每個連接空閑時間最多為五分鐘。

下面我們首先來看較為簡單一些的get和put方法

/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
  RealConnection get(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.allocations.size() < connection.allocationLimit
          && address.equals(connection.route().address)
          && !connection.noNewStreams) {
        streamAllocation.acquire(connection);
        return connection;
      }
    }
    return null;
  }

獲取一個connection時按照Address來匹配,而建立連接也是通過Address查詢一個合適的Route來建立的,當(dāng)匹配到連接時,會將新建立的流對應(yīng)的StreamAllocation添加到connection.allocations中, 如果這里調(diào)用connection.allocations.add(StreamAllocation)或者Connection自定義的add方法會不會更清晰一些,不太明白為什么將該任務(wù)放在了StreamAllocation的acquire方法中,此處的streamAllocation的acquire方法其實(shí)也就是做了這件事情,用來管理一個連接上的流。

put方法

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

put方法更為簡單,就是異步觸發(fā)清理任務(wù),然后將連接添加到隊(duì)列中。那么下面開始重點(diǎn)分析它的清理過程,首先來看清理任務(wù)的定義:

private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };

邏輯也很簡單,就是調(diào)用cleanup方法執(zhí)行清理,并等待一段時間,持續(xù)清理,而等待的時間長度時有cleanup函數(shù)返回值指定的,那么我們繼續(xù)來看cleanup函數(shù)

/**
  * Performs maintenance on this pool, evicting the connection that has been idle the longest if
  * either it has exceeded the keep alive limit or the idle connections limit.
  *
  * <p>Returns the duration in nanos to sleep until the next scheduled call to this method. Returns
  * -1 if no further cleanups are required.
  */
 long cleanup(long now) {
   int inUseConnectionCount = 0;
   int idleConnectionCount = 0;
   RealConnection longestIdleConnection = null;
   long longestIdleDurationNs = Long.MIN_VALUE;

   // Find either a connection to evict, or the time that the next eviction is due.
   synchronized (this) {
     for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
       RealConnection connection = i.next();

       // If the connection is in use, keep searching.
       if (pruneAndGetAllocationCount(connection, now) > 0) {
         inUseConnectionCount++;
         continue;
       }
       //統(tǒng)計(jì)空閑連接的數(shù)量
       idleConnectionCount++;

       // If the connection is ready to be evicted, we're done.
       long idleDurationNs = now - connection.idleAtNanos;
       if (idleDurationNs > longestIdleDurationNs) {//找出空閑時間最長的連接以及對應(yīng)的空閑時間
         longestIdleDurationNs = idleDurationNs;
         longestIdleConnection = connection;
       }
     }


     if (longestIdleDurationNs >= this.keepAliveDurationNs
         || idleConnectionCount > this.maxIdleConnections) {
       // We've found a connection to evict. Remove it from the list, then close it below (outside
       // of the synchronized block).
       connections.remove(longestIdleConnection);  //在符合清理?xiàng)l件下,清理空閑時間最長的連接
     } else if (idleConnectionCount > 0) {
       // A connection will be ready to evict soon.
       return keepAliveDurationNs - longestIdleDurationNs;  //不符合清理?xiàng)l件,則返回下次需要執(zhí)行清理的等待時間
     } else if (inUseConnectionCount > 0) {
       // All connections are in use. It'll be at least the keep alive duration 'til we run again.
       return keepAliveDurationNs;  //沒有空閑的連接,則隔keepAliveDuration之后再次執(zhí)行
     } else {
       // No connections, idle or in use.
       cleanupRunning = false;  //清理結(jié)束
       return -1;
     }
   }

   closeQuietly(longestIdleConnection.socket());  //關(guān)閉socket資源

   // Cleanup again immediately.
   return 0;  //這里是在清理一個空閑時間最長的連接以后會執(zhí)行到這里,需要立即再次執(zhí)行清理
 }

這里的首先統(tǒng)計(jì)空閑連接數(shù)量,然后查找最長空閑時間的連接以及對應(yīng)空閑時長,然后判斷是否超出最大空閑連接數(shù)量或者超過最大空閑時長,滿足其一則執(zhí)行清理最長空閑時長的連接,然后立即再次執(zhí)行清理,否則會返回對應(yīng)的等待時間,代碼中中文注釋已做說明。方法中用到了一個方法來查看一個連接上分配流的數(shù)量,這里不再貼出,有興趣的可以自行查看。
接下來我們梳理一下清理的任務(wù),清理任務(wù)是異步執(zhí)行的,遵循兩個指標(biāo),最大空閑連接數(shù)量和最大空閑時長,滿足其一則清理空閑時長最大的那個連接,然后循環(huán)執(zhí)行,要么等待一段時間,要么繼續(xù)清理下一個連接,直到清理所有連接,清理任務(wù)才可以結(jié)束,下一次put方法調(diào)用時,如果已經(jīng)停止的清理任務(wù)則會被再次觸發(fā)開始。
ConnectionPool的主要方法就是這三個,其余的則是工具私有方法或者getter, setter方法,另外還有一個需要介紹一下我們在StreamAllocation中遇到的方法 connectionBecameIdle標(biāo)識一個連接處于了空閑狀態(tài),即沒有流任務(wù),那么就需要調(diào)用該方法,有ConnectionPool來決定是否需要清理該連接:

 /**
   * Notify this pool that {@code connection} has become idle. Returns true if the connection has
   * been removed from the pool and should be closed.
   */
  boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connection.noNewStreams || maxIdleConnections == 0) {
      connections.remove(connection);
      return true;
    } else {
      notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
      return false;
    }
  }

這里noNewStream標(biāo)志位之前說過,它可以理解為該連接已經(jīng)不可用,所以可以直接清理,而maxIdleConnections==0則標(biāo)識不允許有空閑連接,也是可以直接清理的,否則喚醒清理任務(wù)的線程,執(zhí)行清理方法。

至此則分析完了連接管理的邏輯,其實(shí)就是連接復(fù)用,主要包括get, put , cleanup三個方法,重點(diǎn)時清理任務(wù)的執(zhí)行,我們可以在OkHttpClient中配置

后記

關(guān)于okhttp的連接和連接管理,邏輯還是比較容易理解,但是StreamAllocation的概念在剛接觸時還是比較令人費(fèi)解,但是為了邏輯順序本篇文章還是從StreamAllocation開始分析,進(jìn)而引入了連接和連接復(fù)用管理部分,讀者可在理解了連接和連接復(fù)用管理部分的代碼以后在回頭再去讀StreamAllocation的代碼或許更容易理解一些。在最初的計(jì)劃中是將連接與流一起分析,正是因?yàn)镾treamAlloc

附錄

在前面的分析中,我們了解到Connection需要一個Route路徑對象來建立連接,在這一部分我們主要分析Route和RouteSelector,來學(xué)習(xí)okhttp中是如何通過Address對象選擇合理的路徑對象的,本部分可能比較雞肋,對整個網(wǎng)絡(luò)請求的流程并沒有太大影響,因此有興趣的同學(xué)可以繼續(xù)閱讀,沒有興趣可以略過,并不影響其他部分的分析。

首先我們先來看一下Address類,這個類用來表示需要連接遠(yuǎn)程主機(jī)的地址,它內(nèi)部包含url, proxy, dns等信息,還有一些關(guān)于Https會用到的驗(yàn)證信息,這里我們不再分析其源碼, 代碼很簡單,只是封裝了一些屬性域而已。但是它的注釋還是可以了解一下,可以更好地把握這個類的作用:

/**
 * A specification for a connection to an origin server. For simple connections, this is the
 * server's hostname and port. If an explicit proxy is requested (or {@linkplain Proxy#NO_PROXY no
 * proxy} is explicitly requested), this also includes that proxy information. For secure
 * connections the address also includes the SSL socket factory, hostname verifier, and certificate
 * pinner.
 *
 * <p>HTTP requests that share the same {@code Address} may also share the same {@link Connection}.
 */

通過閱讀類的注釋,我們就可以發(fā)現(xiàn),其實(shí)它就是對于連接對應(yīng)的遠(yuǎn)程連接的地址說明,一般情況下會是主機(jī)名和端口號,也就是url, 在有代理的情況下還會包含代理信息,而對于Https會包含一些其他的驗(yàn)證信息等。

下面我們再來看Route對象,我們還是來看它的注釋:

/**
 * The concrete route used by a connection to reach an abstract origin server. When creating a
 * connection the client has many options:
 *
 * <ul>
 *     <li><strong>HTTP proxy:</strong> a proxy server may be explicitly configured for the client.
 *         Otherwise the {@linkplain java.net.ProxySelector proxy selector} is used. It may return
 *         multiple proxies to attempt.
 *     <li><strong>IP address:</strong> whether connecting directly to an origin server or a proxy,
 *         opening a socket requires an IP address. The DNS server may return multiple IP addresses
 *         to attempt.
 * </ul>
 *
 * <p>Each route is a specific selection of these options.
 */

從注釋中我們可以看出,首先Route對象可以用于建立連接,而選擇一個合適的Route,有很多種選項(xiàng),第一個就是Proxy代理,代理可以明確指定,也可以由ProxySelector中返回多個可用的代理,第二個選項(xiàng)就是IP地址,在Java中表示就是InetAddress對象,dns會返回一個服務(wù)器對應(yīng)的多個IP地址,這兩個選項(xiàng)在RouteSelector中馬上就會看到,下面我們來分析RouteSelector是如何選擇Route對象的。

首先來看它的屬性域:

/**
 * Selects routes to connect to an origin server. Each connection requires a choice of proxy server,
 * IP address, and TLS mode. Connections may also be recycled.
 */
public final class RouteSelector {
  private final Address address;
  private final RouteDatabase routeDatabase;

  /* The most recently attempted route. */
  private Proxy lastProxy;
  private InetSocketAddress lastInetSocketAddress;

  /* State for negotiating the next proxy to use. */
  private List<Proxy> proxies = Collections.emptyList();
  private int nextProxyIndex;

  /* State for negotiating the next socket address to use. */
  private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList();
  private int nextInetSocketAddressIndex;

  /* State for negotiating failed routes */
  private final List<Route> postponedRoutes = new ArrayList<>();
  ...
}

注釋說的也很清楚,連接遠(yuǎn)程服務(wù)器,每一個連接都需要選擇一個代理和IP地址,這里我們忽略與HTTPs有關(guān)的東西,下面的屬性域也就很容易理解,address代表地址, routeDatabase是一個黑名單,存儲那些不可用的Route對象,接下來的六個屬性則是與Proxy和InetSocketAddress相關(guān)了,包括最近使用的,可以選擇的,以及下一個可選擇的索引值,最后postponedRoutes表示那些加入到黑名單中的且符合地址條件(即滿足條件但是之前有過不可用記錄的Route對象)所有Route對象集合,當(dāng)沒有可選擇余地時會選擇使用它們,總比沒有要好。

首先我們從構(gòu)造器開始:

  public RouteSelector(Address address, RouteDatabase routeDatabase) {
    this.address = address;
    this.routeDatabase = routeDatabase;

    resetNextProxy(address.url(), address.proxy());
  }

這里我們看到調(diào)用了resetNextProxy()方法,其實(shí)可以將其理解為一個初始化或者重置Proxy方法,我們來看起代碼:

/** Prepares the proxy servers to try. */
  private void resetNextProxy(HttpUrl url, Proxy proxy) {
    if (proxy != null) {
      // If the user specifies a proxy, try that and only that.
      proxies = Collections.singletonList(proxy);
    } else {
      // Try each of the ProxySelector choices until one connection succeeds.
      List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
      proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
          ? Util.immutableList(proxiesOrNull)
          : Util.immutableList(Proxy.NO_PROXY);
    }
    nextProxyIndex = 0;
  }

邏輯流程也容易理解,優(yōu)先使用在address對象中的指定的proxy對象,該方法由構(gòu)造器調(diào)用,傳遞進(jìn)來的,如果該P(yáng)roxy對象為空,則從address.proxySelector中選擇一個Proxy, 如果還沒有則使用Proxy.NO_PROXY,并將索引初始化為0,接下來我們一并看一下resetInetSocketAddress()方法:

  /** Prepares the socket addresses to attempt for the current proxy or host. */
  private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    // Clear the addresses. Necessary if getAllByName() below throws!
    inetSocketAddresses = new ArrayList<>();

    String socketHost;
    int socketPort;
    if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
      socketHost = address.url().host();
      socketPort = address.url().port();
    } else {
      SocketAddress proxyAddress = proxy.address();
      ...
      InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
      socketHost = getHostString(proxySocketAddress);
      socketPort = proxySocketAddress.getPort();
    }
    ...
    if (proxy.type() == Proxy.Type.SOCKS) {
      inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
      // Try each address for best behavior in mixed IPv4/IPv6 environments.
      List<InetAddress> addresses = address.dns().lookup(socketHost);
      for (int i = 0, size = addresses.size(); i < size; i++) {
        InetAddress inetAddress = addresses.get(i);
        inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
      }
    }

    nextInetSocketAddressIndex = 0;
  }

這里省略了部分條件判斷,邏輯流程同意很清楚,首先是獲取到主機(jī)地址和端口號,然后創(chuàng)建InetSocketAddress或者通過address中的dns查詢所有有可能的InetSocketAddress地址。在有了以上的了解以后我們就可以看RouteSelector中最重要的方法,即選擇一個Route對象,next()方法:

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

這段代碼第一次看可能有些迷惑,不過仔細(xì)去分析其實(shí)邏輯很清晰,首先我們假定還有可用的InetSocketAddress,那么通過nextInetSocketAddress()直接獲取并創(chuàng)建Route對象,然后檢查是否在黑名單中,在黑名單則加入候選隊(duì)列,其實(shí)就是最后迫于無奈才會使用。如果沒有了InetSocketAddress,則選擇下一個可用的代理Proxy,在nextProxy中會調(diào)用resetNextInetSocketAddress()方法重置InetSocketAddress的隊(duì)列,繼續(xù)查詢下個可用的InetSocketAddress對象,如果沒有可用的Proxy,則從候選隊(duì)列中postpone中選擇,即迫于無奈的選擇,如果候選隊(duì)列也為空那就無能為力了,只能拋異常。這樣就分析完了整個選擇路徑的過程,過程的邏輯很清晰也很容易理解,另外該類中還有一些其他的方法,如用于更新黑名單等,有興趣的可以自行查閱。

分析到這里,整個連接的過程就分析結(jié)束了,可以總結(jié)為在網(wǎng)絡(luò)請求過程中,首先創(chuàng)建一個StreamAllocation的對象,然后調(diào)用其newStream()方法,查找一個可用連接,要么復(fù)用連接,要么新建連接,復(fù)用連接則根據(jù)address從連接池中查找,新建連接則是根據(jù)address查找一個Route對象建立連接,建立連接以后會將該連接添加到連接池中,同時連接池的清理任務(wù)停止的情況下,添加新的連接進(jìn)去會觸發(fā)開啟清理任務(wù)。這是建立連接和管理連接的整個過程,當(dāng)擁有連接以后,StreamAllocation就會在連接上建立一個流對象,該流持有connection的輸入輸出流,也就是socket的輸入輸出流,通過它們最終完成數(shù)據(jù)通信的過程,所以下一節(jié)中將會重點(diǎn)分析流對象Http1Codec,以及數(shù)據(jù)通信的過程。

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

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