Okhttp源碼學習四(連接攔截器的內部實現(xiàn))

Okhttp的5個內置攔截器可以說是Okhttp的核心,因為整個請求的過程都被封裝在這5個攔截器里面。而5個攔截器里面的核心就是這篇要分析的ConnectInterceptor,因為ConnectInterceptor才是真正發(fā)起請求黍析,建立連接地方

ConnectInterceptor

public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

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

 @Override 
  public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    //從攔截器鏈里面拿到流分配管理類StreamAllocation對象
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //通過流分配管理類創(chuàng)建一個流
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    //通過通過流分配管理類建立連接
    RealConnection connection = streamAllocation.connection();

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

整個ConnectInterceptor類的代碼也就這么幾行,但從這幾行中可以看出,真正創(chuàng)建流和建立連接的邏輯其實都在StreamAllocation里面. StreamAllocation對象是從RealInterceptorChain獲取的. 通過之前的幾篇對Okhttp的源碼的學習福澡,我們知道攔截器的攔截方法intercept(Chain chain)中的chain最開始是在RealCallgetResponseWithInterceptorChain()中初始化的:

 Response getResponseWithInterceptorChain() throws IOException {
........
//第二個參數(shù)就是StreamAllocation 類型,但是傳的是null
  Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
      originalRequest, this, eventListener, client.connectTimeoutMillis(),
      client.readTimeoutMillis(), client.writeTimeoutMillis());

   return chain.proceed(originalRequest);
 }

可以看到第一個攔截器鏈創(chuàng)建的時候驹马,StreamAllocation 傳的是null革砸,那么StreamAllocation 是什么時候賦值的呢,其實是在第一個攔截器RetryAndFollowUpInterceptor的攔截方法里面賦值的:

 @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 streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;
    .......

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

第一個攔截器給StreamAllocation賦值后糯累,后面的攔截器中的攔截器鏈的StreamAllocation就都是這個值. 前面說StreamAllocation很重要算利,具體的建立連接過程,還有創(chuàng)建流都是在這個類里面泳姐,下面就看看StreamAllocation

StreamAllocation

先看一下StreamAllocation的成員和構造函數(shù)

public final class StreamAllocation {
  public final Address address;      //請求的url地址
  private RouteSelector.Selection routeSelection;    //選擇的路由
  private Route route;
  private final ConnectionPool connectionPool;    //連接池
  public final Call call;
  public final EventListener eventListener;
  private final Object callStackTrace;

  // State guarded by connectionPool.
  private final RouteSelector routeSelector;    //路由選擇器
  private int refusedStreamCount;    //拒絕的次數(shù)
  private RealConnection connection;  //連接
  private boolean reportedAcquired;
  private boolean released;
  private boolean canceled;
  private HttpCodec codec;    //負責寫入請求數(shù)據或讀出響應數(shù)據的IO流

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

StreamAllocation的成員和構造函數(shù)可以看到效拭,StreamAllocation里面主要包含了連接池,連接胖秒,還有流. 為了更好理解這個類缎患,先屢一下基本概念:

  1. HTTP請求網絡的時候,首先需要通過一個Socket與服務端建立TCP連接阎肝,Socket中還需要有主機名host和端口號port
  2. 建立好連接后挤渔,就可以使用流在這個連接上向服務端寫入數(shù)據和讀取服務端返回的數(shù)據
  3. HTTP/1.1提出了Keep-Alive機制:當一個HTTP請求的數(shù)據傳輸結束后,TCP連接不立即釋放风题,如果此時有新的HTTP請求判导,且其請求的Host同上次請求相同嫉父,則可以直接復用未釋放的TCP連接,從而省去了TCP的釋放和再次創(chuàng)建的開銷眼刃,減少了網絡延時
  4. HTTP2.0的多路復用:允許同時通過單一的 HTTP/2 連接發(fā)起多重的請求-響應消息

OkHttp為了解耦熔号,對請求中的各個概念進行了封裝, RealConnection就對應著請求中的連接鸟整,HttpCodec對應流引镊,為了HTTP1.1的連接復用以及HTTP2.0的多路復用,就需要將請求連接保存下來篮条,以便復用弟头,所以就有了ConnectionPool. 而為了執(zhí)行一次網絡請求,需要從連接池找到可用的的連接涉茧,然后創(chuàng)建流赴恨,所以就需要一個分配管理流的角色,這個角色就是StreamAllocation

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

  try {
    //尋找一個健康的連接
    RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
        writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
    //通過找到的連接獲取流
    HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

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

調用了findHealthyConnection()resultConnection.newCodec()伴栓,先看findHealthyConnection()

//StreamAllocation.findHealthyConnection()
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
  int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
  boolean doExtensiveHealthChecks) throws IOException {
  while (true) {
    //找到可用的連接
    RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
        pingIntervalMillis, connectionRetryEnabled);

    // If this is a brand new connection, we can skip the extensive health checks.
    synchronized (connectionPool) {
      //successCount為0伦连,說明是新建立的連接,沒有用過钳垮,默認可用惑淳,直接返回
      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.
    //如果找到的連接不健康,就不讓創(chuàng)建流饺窿,并關閉該連接歧焦,繼續(xù)找
    if (!candidate.isHealthy(doExtensiveHealthChecks)) {
      noNewStreams();
      continue;
    }
    
    return candidate;
  }
}

又調用了findConnection去查找連接

//StreamAllocation.findConnection()
 private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
  int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  boolean foundPooledConnection = false;
  RealConnection result = null;
  Route selectedRoute = null;
  Connection releasedConnection;
  Socket toClose;
  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. We need to be careful here because our
    // already-allocated connection may have been restricted from creating new streams.
    releasedConnection = this.connection;
    toClose = releaseIfNoNewStreams();
    //如果當前StreamAllocation持有的連接不為空
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      //將這個持有的連接賦值給result
      result = this.connection;
      releasedConnection = null;
    }
    if (!reportedAcquired) {
      // If the connection was never reported acquired, don't report it as released!
      releasedConnection = null;
    }
    //當前StreamAllocation持有的連接為空,reuslt在這里就會為空肚医,說明還沒找到可用連接
    if (result == null) {
      // Attempt to get a connection from the pool.
      //從連接池中找一下绢馍,如果找到了會給持有的連接賦值
      Internal.instance.get(connectionPool, address, this, null);
      //如果從連接池中找到了可用的連接
      if (connection != null) {
        foundPooledConnection = true;
        //賦值給result
        result = connection;
      } else {
        //如果沒找到,將StreamAllocation持有的路由賦值給已找到的路由
        selectedRoute = route;
      }
    }
  }
  closeQuietly(toClose);

  if (releasedConnection != null) {
    eventListener.connectionReleased(call, releasedConnection);
  }
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
  }
  if (result != null) {  //result不為空意味著當前連接可以用肠套,或者從連接池中找到了可以復用的連接
    // If we found an already-allocated or pooled connection, we're done.
    return result;      //直接返回
  }

  // If we need a route selection, make one. This is a blocking operation.
  boolean newRouteSelection = false;
  //如果沒找到路由舰涌,并且路由選擇區(qū)為空
  if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
    newRouteSelection = true;
    //就切換路由
    routeSelection = routeSelector.next();
  }

  synchronized (connectionPool) {
    if (canceled) throw new IOException("Canceled");

    if (newRouteSelection) {
      // Now that we have a set of IP addresses, make another attempt at getting a connection from
      // the pool. This could match due to connection coalescing.
      //遍歷路由選擇區(qū)的所有路由
      List<Route> routes = routeSelection.getAll();
      for (int i = 0, size = routes.size(); i < size; i++) {
        Route route = routes.get(i);
        //根據新的路由地址,再從連接池找一遍
        Internal.instance.get(connectionPool, address, this, route);
        if (connection != null) {          //如果找到了
          foundPooledConnection = true;
          result = connection;
          this.route = route;
          break;
        }
      }
    }

    if (!foundPooledConnection) {    //如果還沒找到可用連接
      if (selectedRoute == null) {
        selectedRoute = routeSelection.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.
      route = selectedRoute;
      refusedStreamCount = 0;
      //就新建一個連接
      result = new RealConnection(connectionPool, selectedRoute);
      //關聯(lián)到流管理引用列表connection.allocations你稚,并用this.connection記錄當前連接
      acquire(result, false);
    }
  }

  // If we found a pooled connection on the 2nd time around, we're done.
  if (foundPooledConnection) {
    eventListener.connectionAcquired(call, result);
    return result;
  }

  // Do TCP + TLS handshakes. This is a blocking operation.
  //與服務端建立TCP連接
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);
  routeDatabase().connected(result.route());

  Socket socket = null;
  synchronized (connectionPool) {
    reportedAcquired = true;

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

  eventListener.connectionAcquired(call, result);
  return result;
 }

從源碼可以看到瓷耙,OkHttp尋找可用連接的過程如下:

1. 如果是重定向請求,就使用StreamAllocation持有的連接
    releasedConnection = this.connection;
    //如果當前連接不能創(chuàng)建新的流入宦,就釋放
    toClose = releaseIfNoNewStreams();
    if (this.connection != null) {
      // We had an already-allocated connection and it's good.
      //將StreamAllocation持有的連接賦值給result哺徊,表示就用這個持有的連接
      result = this.connection;
      //不釋放當前連接
      releasedConnection = null;
    }

StreamAllocation持有的連接this.connection一開始肯定是為null,但是當從連接池中找到了可用的連接后乾闰,或者從連接池沒找到落追,新建一個連接后,StreamAllocation持有的連接this.connection就不為null. 不為null的時候就把它賦值給 result涯肩,表示就用這個持有的連接轿钠。

但我們知道在RetryAndFollowUpInterceptor的攔截方法里面巢钓,StreamAllocation是新建的。每調用一次RetryAndFollowUpInterceptor的攔截方法就會新建一個StreamAllocation疗垛,也就是說每一次請求都會新建一個StreamAllocation. 那么也就意味著每一次請求使用的連接根本不可能用到StreamAllocation持有的連接this.connection症汹,因為StreamAllocation 是新建的,this.connection一直是null. 那么什么時候this.connection才會不為null呢贷腕?其實只有在第一次請求背镇,服務端返回一個比如狀態(tài)碼為307這樣的需要重定向的響應的時候,并且重定向的Request的host泽裳、port瞒斩、scheme與之前一致時出現(xiàn)。在RetryAndFollowUpInterceptor中涮总,如果響應為需要重定向胸囱,那么會再發(fā)起一次請求,第二次請求時瀑梗,使用的StreamAllocation就是第一次創(chuàng)建的烹笔,這個時候就會用到這個StreamAllocation持有的連接(不太明白可以去看下Okhttp源碼學習三(重試和重定向,橋接抛丽,緩存攔截器的內部原理))

2. 如果不是重定向請求谤职,就遍歷連接池中的所有連接,看是否有可復用的連接
  if (result == null) {    //result為null铺纽,意味著不是重定向請求
    // Attempt to get a connection from the pool.
    Internal.instance.get(connectionPool, address, this, null);
    if (connection != null) {
      foundPooledConnection = true;
      result = connection;
    } else {
      selectedRoute = route;
    }
  }

調用了Internal.instance.get(connectionPool, address, this, null)從連接池中去找

public abstract class Internal {

  public static void initializeInstanceForTests() {
   // Needed in tests to ensure that the instance is actually pointing to something.
    new OkHttpClient();
  }

  public static Internal instance;

  public abstract void addLenient(Headers.Builder builder, String line);
  .......
}

Internal是一個抽象類柬帕,它的唯一實現(xiàn)是在 OkHttpClient中:

public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
  static final List<Protocol> DEFAULT_PROTOCOLS = Util.immutableList(Protocol.HTTP_2, Protocol.HTTP_1_1);

  static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);

  static {
    Internal.instance = new Internal() {
     ........
    @Override public RealConnection get(ConnectionPool pool, Address address,StreamAllocation streamAllocation, Route route) {
      //調用的是連接池ConnectionPool的get方法
      return pool.get(address, streamAllocation, route);
    }
    .........
  };
 ......
}

看一下ConnectionPool的get()方法:

 @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    //變量連接池中的所有連接哟忍,connections是Deque類型
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
   return null;
 }

調用了 connection.isEligible(address, route)來判斷是否可以復用

//RealConnection.isEligible()
public boolean isEligible(Address address, @Nullable Route route) {
  //如果當前連接上的并發(fā)流數(shù)量超過最大值1狡门,或當前連接不能創(chuàng)建新的流,返回false
  if (allocations.size() >= allocationLimit || noNewStreams) return false;
  //如果兩個address除了host以外的所有域不相同锅很,返回false
  if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
  //如果host也相同其馏,那么當前連接可以復用,直接返回
  if (address.url().host().equals(this.route().address().url().host())) {
    return true; // This connection is a perfect match.
  }
  //http2連接為空爆安,直接返回false
  if (http2Connection == null) return false;

  if (route == null) return false;
  //路由用到了代理叛复,返回false
  if (route.proxy().type() != Proxy.Type.DIRECT) return false;
  if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
  //socket地址相同不同,返回false
  if (!this.route.socketAddress().equals(route.socketAddress())) return false;

  // 3. This connection's server certificate's must cover the new host.
  if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
  if (!supportsUrl(address.url())) return false;

  // 4. Certificate pinning must match the host.
  try {
    address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
  } catch (SSLPeerUnverifiedException e) {
    return false;
  }

  return true; // The caller's address can be carried by this connection.
}

判斷是否可以復用的條件大致就是(后面對HTTP2的復用條件判斷暫時沒看明白):

當前連接的流的數(shù)量要少于1個扔仓,請求地址的host相同

如果連接池中有符合上面的條件的連接褐奥,就調用streamAllocation.acquire(connection, true);

//StreamAllocation.acquire()
public void acquire(RealConnection connection, boolean reportedAcquired) {
  assert (Thread.holdsLock(connectionPool));
  if (this.connection != null) throw new IllegalStateException();
  //給StreamAllocation持有的連接賦值
  this.connection = connection;
  this.reportedAcquired = reportedAcquired;
  //將連接connection與StreamAllocation綁定
  connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
}

connection.allocations是一個列表:

public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();

每一個連接對象RealConnection都有一個列表,列表的元素類型是StreamAllocation的弱引用翘簇,它用來記錄當前連接上建立的流撬码。因為每一次請求都會創(chuàng)建一個新的StreamAllocation

回到StreamAllocation.findConnection()中:

 if (result == null) {
    // Attempt to get a connection from the pool.
    Internal.instance.get(connectionPool, address, this, null);
    //如果連接池中有復用的連接,connection就不為null
    if (connection != null) {
      foundPooledConnection = true;
      //使用復用的連接
      result = connection;
    } else {
      selectedRoute = route;
    }
  }
}
closeQuietly(toClose);

if (releasedConnection != null) {
  eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
  eventListener.connectionAcquired(call, result);
}
if (result != null) {
  // If we found an already-allocated or pooled connection, we're done.
  return result;
}

如果從連接池找到了可以復用的連接版保,直接返回這個復用的連接

3. 如果在連接池沒有找到可復用的連接呜笑,就切換路由夫否,再從連接池中找一次
 if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
  newRouteSelection = true;
  //切換路由
  routeSelection = routeSelector.next();
}

synchronized (connectionPool) {
  if (canceled) throw new IOException("Canceled");

  if (newRouteSelection) {
    // Now that we have a set of IP addresses, make another attempt at getting a connection from
    // the pool. This could match due to connection coalescing.
    List<Route> routes = routeSelection.getAll();
    for (int i = 0, size = routes.size(); i < size; i++) {
      Route route = routes.get(i);
      Internal.instance.get(connectionPool, address, this, route);
      if (connection != null) {
        foundPooledConnection = true;
        result = connection;
        this.route = route;
        break;
      }
    }
  }

關于RouteSelector以及路由的選擇,切換叫胁,下篇再分析

4. 切換路由后凰慈,連接池中還是沒有找到可以復用的連接,就新建一個連接驼鹅,并將新建的connection和當前的StreamAllocation綁定
 if (!foundPooledConnection) {
    if (selectedRoute == null) {
      selectedRoute = routeSelection.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.
    route = selectedRoute;
    refusedStreamCount = 0;
    //新建一個連接
    result = new RealConnection(connectionPool, selectedRoute);
    //綁定connection和StreamAllocation
    acquire(result, false);
  }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末微谓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子输钩,更是在濱河造成了極大的恐慌堰酿,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件张足,死亡現(xiàn)場離奇詭異触创,居然都是意外死亡,警方通過查閱死者的電腦和手機为牍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門哼绑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碉咆,你說我怎么就攤上這事抖韩。” “怎么了疫铜?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵茂浮,是天一觀的道長。 經常有香客問我壳咕,道長席揽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任谓厘,我火速辦了婚禮幌羞,結果婚禮上,老公的妹妹穿的比我還像新娘竟稳。我一直安慰自己属桦,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布他爸。 她就那樣靜靜地躺著聂宾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诊笤。 梳的紋絲不亂的頭發(fā)上系谐,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音盏混,去河邊找鬼蔚鸥。 笑死惜论,一個胖子當著我的面吹牛,可吹牛的內容都是我干的止喷。 我是一名探鬼主播馆类,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弹谁!你這毒婦竟也來了乾巧?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤预愤,失蹤者是張志新(化名)和其女友劉穎沟于,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體植康,經...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡旷太,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了销睁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片供璧。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖冻记,靈堂內的尸體忽然破棺而出睡毒,到底是詐尸還是另有隱情,我是刑警寧澤冗栗,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布演顾,位于F島的核電站,受9級特大地震影響隅居,放射性物質發(fā)生泄漏钠至。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一军浆、第九天 我趴在偏房一處隱蔽的房頂上張望棕洋。 院中可真熱鬧,春花似錦乒融、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至奢驯,卻和暖如春申钩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘪阁。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工撒遣, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留邮偎,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓义黎,卻偏偏與公主長得像禾进,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子廉涕,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內容