一篇讓你受用的okhttp分析

本文希望你在讀完之后能夠清楚的事情:一次http請求的經(jīng)歷蹲姐,這期間會遇到什么問題,okhttp怎么解決的,在它的責(zé)任鏈中的那一部分解決的這個(gè)問題腐晾,怎樣監(jiān)控我們自己的網(wǎng)絡(luò)請求,怎樣監(jiān)控網(wǎng)絡(luò)狀況丐一。

一次http網(wǎng)絡(luò)請求的歷程

網(wǎng)絡(luò)請求要依次經(jīng)歷 DNS解析藻糖、創(chuàng)建連接、收發(fā)數(shù)據(jù)库车、關(guān)閉連接幾個(gè)過程巨柒。下圖是其他教程里的一張圖,畫的非常清晰:
網(wǎng)絡(luò)請求的過程.png

這期間需要應(yīng)對的問題有:

1柠衍、DNS劫持洋满。即使我們現(xiàn)在幾萬用戶的小體量app,每月也能碰到幾起DNS劫持珍坊。除了DNS劫持牺勾,這部分還需要考慮IP選取策略、DNS緩存阵漏、容災(zāi)等問題禽最,如果必要的話腺怯,可以對其進(jìn)行優(yōu)化,參考百度的DNS優(yōu)化,以及美圖案例(考慮了非okhttp的情況)川无。

2呛占、連接復(fù)用。http基于TCP懦趋,所以連接要經(jīng)歷三次握手晾虑,關(guān)閉連接要經(jīng)歷4次握手,TCP連接在發(fā)送數(shù)據(jù)的時(shí)候仅叫,在起初會限制連接速度帜篇,隨著傳輸?shù)某晒蜁r(shí)間的推移逐漸提高速度(防擁塞),再加上TLS密匙協(xié)商诫咱,如果每次網(wǎng)絡(luò)請求都要經(jīng)歷創(chuàng)建連接的過程笙隙,帶來的開銷是非常大的。

3坎缭、I/O問題竟痰。客戶端會等待服務(wù)器的返回?cái)?shù)據(jù)掏呼,數(shù)據(jù)收到后還要把數(shù)據(jù)從內(nèi)核copy到用戶空間坏快,期間根據(jù)網(wǎng)絡(luò)的阻塞模型(基本有五種,常見的有阻塞I/O憎夷、非阻塞I/O莽鸿、多路復(fù)用I/O),會遇到不同程度的阻塞拾给。

4祥得、數(shù)據(jù)壓縮和加密。減少數(shù)據(jù)體積蒋得,對數(shù)據(jù)進(jìn)行加密级及。

對于上述問題的方案:

1、okhttp提供了自定義DNS解析的接口窄锅。

2、持久連接缰雇。http1.1支持事務(wù)結(jié)束之后將TCP保持在打開狀態(tài)(http1.1默認(rèn)將Keep-Alive首部開啟入偷,用于客戶端和服務(wù)器通信連接的保存時(shí)間,TCP中有Keep-Alive報(bào)文械哟,來定時(shí)探測通信雙方是否存活疏之,但是這一部分內(nèi)容用于長連接時(shí)會存在問題),對http1.1進(jìn)行連接復(fù)用暇咆,將連接放入連接池锋爪。支持http2.0丙曙,http2.0使用多路復(fù)用,支持一條連接同時(shí)處理多個(gè)請求其骄,請求可以并發(fā)進(jìn)行亏镰,一個(gè)域名會保留一條連接(一條連接即一個(gè)TCP連接,收發(fā)只有一根管道拯爽,并不是真正意義上的并發(fā)索抓,而是利用TCP把數(shù)據(jù)拆分裝包加標(biāo)簽的特性實(shí)現(xiàn)的復(fù)用),能有效降低延時(shí)(也有特殊情況毯炮,比如一個(gè)域名的數(shù)據(jù)請求特別多逼肯,或者服務(wù)端對單個(gè)連接有速度限制,如視頻流)桃煎。長連接(推薦讀下這篇文章篮幢,如果沒有從事過長連接開發(fā)的話)也是一種方案,在某些場景下非常有效为迈,但是okhttp不支持三椿。

3、OKhttp使用非阻塞I/O模型OKio(算是nio吧曲尸,和我們理解的nio不太一致赋续,理解的nio定時(shí)去檢查是否有數(shù)據(jù)到來,有的話就讀另患,沒有就返回纽乱,但是okio的實(shí)現(xiàn)是定時(shí)去檢查是否已經(jīng)讀寫完成,沒完成就認(rèn)為超時(shí)昆箕,close掉該socket)鸦列,該I/O框架的內(nèi)存表現(xiàn)也很好(mars使用epoll)。

4鹏倘、http2.0協(xié)議本身對頭部有壓縮薯嗤。對于body的壓縮okhttp提供了Gzip壓縮的支持。

OKhttp實(shí)現(xiàn)分析

網(wǎng)上找到的一個(gè)okhttp整體調(diào)用圖纤泵,由于okhttp的分析已經(jīng)很多骆姐,盡量以少代碼多總結(jié)的方式來闡述這部分內(nèi)容。主要講解okhttp應(yīng)對上述問題的具體實(shí)現(xiàn)捏题。


okhttp整體調(diào)用圖.png

設(shè)計(jì)結(jié)構(gòu)十分清晰玻褪,通過責(zé)任鏈將請求發(fā)送任務(wù)進(jìn)行拆解。

1公荧、okhttp中自定義DNS解析

    OkHttpClient client = new OkHttpClient.Builder()
        .dns(new Dns(){

          @Override
          public List<InetAddress> lookup(String hostname) throws UnknownHostException {
            return Arrays.asList(InetAddress.getAllByName(DNSHelper.getIpByHost(hostname)));
          }
        })
        .build();

DNSHelper可以通過ip直連的方式訪問自己設(shè)置的DNS服務(wù)器带射。也可以通過這種方式接入一些第三方對外提供的DNS服務(wù)。

在RetryAndFollowUpInterceptor這個(gè)攔截器中循狰,將會創(chuàng)建Address對象窟社,該對象收集網(wǎng)絡(luò)請求需要的配置信息券勺,包括DNS、host灿里、port关炼、proxy等。在ConnectIntercepter這一層創(chuàng)建了RouteSelecter钠四,用于路由選擇盗扒,持有Address對象,調(diào)用其next函數(shù)選擇路由時(shí)會調(diào)用DNS對象的lookup函數(shù)返回host的ip缀去。

2侣灶、連接復(fù)用
這部分需要操心的事情一個(gè)是連接的管理,一個(gè)是對網(wǎng)絡(luò)請求的流的管理缕碎。連接的管理交由ConnectionPool褥影,內(nèi)部含有一個(gè)保存了RealConnection對象的隊(duì)列。http2.0一個(gè)連接對應(yīng)多個(gè)流咏雌,在RealConnection內(nèi)保存了一個(gè)代表流的StreamAllocation對象的list凡怎。

在ConnectIntercepter這一層調(diào)用StreamAllocation的newStrem,嘗試在連接池里找到一個(gè)RealConnection赊抖,沒找到則創(chuàng)建一個(gè)统倒,并調(diào)用acquire添加一個(gè)自身的弱引用到RealConnection的流引用List中。newStrem最終返回一個(gè)httpcodec接口的實(shí)現(xiàn)氛雪,代表了具體的http協(xié)議內(nèi)容創(chuàng)建規(guī)則房匆,有兩種實(shí)現(xiàn),對應(yīng)了okhttp適配的兩個(gè)http版本报亩,然后傳遞給下一級浴鸿。當(dāng)然流在讀寫完成后也是需要被清理的,清理函數(shù)deallocate弦追,一個(gè)連接的流都被清理掉之后岳链,通知ConnectionPool判斷連接的kepp-alive時(shí)間,以及空閑連接數(shù)量劲件,移除超時(shí)或者超出數(shù)量限制后空閑時(shí)間最久的連接掸哑。

如下是調(diào)用流和連接的綁定過程(省略了路由選擇過程),新創(chuàng)建的連接會執(zhí)行socket的connect零远,connect的時(shí)候會判斷http協(xié)議是哪個(gè)版本苗分,然后新創(chuàng)建的RealConnection會添加到連接池里。

  // Attempt to use an already-allocated connection.
  RealConnection allocatedConnection = this.connection;
  if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
    return allocatedConnection;
  }

  // Attempt to get a connection from the pool.
  Internal.instance.get(connectionPool, address, this, null);
  if (connection != null) {
    return connection;
  }


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

  // 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.
  result = new RealConnection(connectionPool, selectedRoute);
  acquire(result);
}

// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
routeDatabase().connected(result.route());

Socket socket = null;
synchronized (connectionPool) {
  // Pool the connection.
  Internal.instance.put(connectionPool, result);

  // If another multiplexed connection to the same address was created concurrently, then
  // release this connection and acquire that one.
  if (result.isMultiplexed()) {
    socket = Internal.instance.deduplicate(connectionPool, address, this);
    result = connection;
  }
}
closeQuietly(socket);

return result;

如下可以看出連接池對空閑時(shí)間和空閑連接數(shù)量的限制(順帶一提遍烦,okhttp的線程池也是有數(shù)量限制的俭嘁,大約在60個(gè)左右躺枕,如果項(xiàng)目網(wǎng)絡(luò)庫比較亂服猪,使用線程也不太注意供填,線程過多,超過500個(gè)罢猪,在一些華為手機(jī)上會因?yàn)樯暾埐坏骄€程而崩潰)近她。

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

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

如下是具體的清理邏輯:

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

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          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);
      } else if (idleConnectionCount > 0) {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

3、I/O優(yōu)化

a膳帕、okio的使用:

Okio.buffer(Okio.sink(socket)) .writeUtf8("write string by utf-8.\n") .writeInt(1234).close();

b粘捎、okio沒有使用java提供的select(多路復(fù)用),而是自定義了nio實(shí)現(xiàn)危彩。個(gè)人猜測這樣實(shí)現(xiàn)的原因是多路復(fù)用實(shí)際在網(wǎng)絡(luò)連接非常多的時(shí)候表現(xiàn)更好攒磨,對于客戶端來講不一定適用,反倒會增加大量的select/epoll系統(tǒng)調(diào)用汤徽,更多用于服務(wù)器娩缰。

設(shè)置一個(gè)watchdog,將一次事件(讀谒府、寫)封裝到AsyncTimeout中拼坎,AsyncTimeout持有一個(gè)static鏈表,Watchdog定期檢測鏈表完疫。

  private static final class Watchdog extends Thread {
    Watchdog() {
      super("Okio Watchdog");
      setDaemon(true);
    }

    public void run() {
      while (true) {
        try {
          AsyncTimeout timedOut;
          synchronized (AsyncTimeout.class) {
            timedOut = awaitTimeout();

            // Didn't find a node to interrupt. Try again.
            if (timedOut == null) continue;

            // The queue is completely empty. Let this thread exit and let another watchdog thread
            // get created on the next call to scheduleTimeout().
            if (timedOut == head) {
              head = null;
              return;
            }
          }

          // Close the timed out node.
          timedOut.timedOut();
        } catch (InterruptedException ignored) {
        }
      }
    }
  }

awaitTimeout()函數(shù)讀取鏈表泰鸡,設(shè)置等待事件。到時(shí)間后壳鹤,返回鏈表中的一個(gè)AsyncTimeout 對象盛龄,并調(diào)用該對象的timedOut()函數(shù)。

  static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException {
    // Get the next eligible node.
    AsyncTimeout node = head.next;

    // The queue is empty. Wait until either something is enqueued or the idle timeout elapses.
    if (node == null) {
      long startNanos = System.nanoTime();
      AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
      return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
          ? head  // The idle timeout elapsed.
          : null; // The situation has changed.
    }

    long waitNanos = node.remainingNanos(System.nanoTime());

    // The head of the queue hasn't timed out yet. Await that.
    if (waitNanos > 0) {
      // Waiting is made complicated by the fact that we work in nanoseconds,
      // but the API wants (millis, nanos) in two arguments.
      long waitMillis = waitNanos / 1000000L;
      waitNanos -= (waitMillis * 1000000L);
      AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
      return null;
    }

    // The head of the queue has timed out. Remove it.
    head.next = node.next;
    node.next = null;
    return node;
  }

而若這個(gè)事件已經(jīng)完成則會調(diào)用exit()函數(shù),將該事件在隊(duì)列中移除器虾。

  /** Returns true if the timeout occurred. */
  public final boolean exit() {
    if (!inQueue) return false;
    inQueue = false;
    return cancelScheduledTimeout(this);
  }

  /** Returns true if the timeout occurred. */
  private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
    // Remove the node from the linked list.
    for (AsyncTimeout prev = head; prev != null; prev = prev.next) {
      if (prev.next == node) {
        prev.next = node.next;
        node.next = null;
        return false;
      }
    }

    // The node wasn't found in the linked list: it must have timed out!
    return true;
  }

超時(shí)后認(rèn)為連接不可用讯嫂,調(diào)用Sockect對象的close函數(shù)關(guān)閉該連接。

c兆沙、緩存
是okio對java I/O 做的最重要的優(yōu)化欧芽。主要思想是buffer復(fù)用,而不是創(chuàng)建大量的朝生夕死的buffer對象葛圃,防止頻繁GC千扔。這部分內(nèi)容可以對比BufferedInputStream的實(shí)現(xiàn)(BufferedInputStream內(nèi)部結(jié)構(gòu)和Segment類似,當(dāng)其設(shè)置的初始緩存byte數(shù)組大小不夠時(shí)库正,新申請一個(gè)更大容量的數(shù)組曲楚,并將原緩存數(shù)組的內(nèi)容copy過來,舍棄原數(shù)組)褥符。
代碼思路:Segment對象為byte數(shù)組的封裝龙誊,是數(shù)據(jù)的容器,是一個(gè)雙向鏈表中的節(jié)點(diǎn)喷楣,可以有插入趟大、刪除鹤树、拆分、合并逊朽、復(fù)制幾個(gè)操作罕伯。SegmentPool緩存了不用的segment,是一個(gè)靜態(tài)的單鏈表叽讳,需要時(shí)調(diào)用take獲取Segment追他,不需要時(shí)調(diào)用recycle回收岛蚤。Buffer對象封裝了這兩者的使用邑狸,例如使用okio調(diào)用writeString函數(shù)時(shí)的實(shí)現(xiàn)如下:

  @Override
  public Buffer writeString(String string, int beginIndex, int endIndex, Charset charset) {
    if (string == null) throw new IllegalArgumentException("string == null");
    if (beginIndex < 0) throw new IllegalAccessError("beginIndex < 0: " + beginIndex);
    if (endIndex < beginIndex) {
      throw new IllegalArgumentException("endIndex < beginIndex: " + endIndex + " < " + beginIndex);
    }
    if (endIndex > string.length()) {
      throw new IllegalArgumentException(
          "endIndex > string.length: " + endIndex + " > " + string.length());
    }
    if (charset == null) throw new IllegalArgumentException("charset == null");
    if (charset.equals(Util.UTF_8)) return writeUtf8(string, beginIndex, endIndex);
    byte[] data = string.substring(beginIndex, endIndex).getBytes(charset);
    return write(data, 0, data.length);
  }
  @Override public Buffer write(byte[] source, int offset, int byteCount) {
    if (source == null) throw new IllegalArgumentException("source == null");
    checkOffsetAndCount(source.length, offset, byteCount);

    int limit = offset + byteCount;
    while (offset < limit) {
      Segment tail = writableSegment(1);

      int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
      System.arraycopy(source, offset, tail.data, tail.limit, toCopy);

      offset += toCopy;
      tail.limit += toCopy;
    }

    size += byteCount;
    return this;
  }
  Segment writableSegment(int minimumCapacity) {
    if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();

    if (head == null) {
      head = SegmentPool.take(); // Acquire a first segment.
      return head.next = head.prev = head;
    }

    Segment tail = head.prev;
    if (tail.limit + minimumCapacity > Segment.SIZE || !tail.owner) {
      tail = tail.push(SegmentPool.take()); // Append a new empty segment to fill up.
    }
    return tail;
  }

核心邏輯是緩存大小的管理推溃,然后調(diào)用System.arraycopy將數(shù)據(jù)復(fù)制到容器中硬萍。

4畜普、數(shù)據(jù)的壓縮加密

對這一部分的處理主要在BridgeInterceptor中街立。會在頭部自動添加Accept-Encoding: gzip,并自動對response的進(jìn)行解壓縮,若手動添加了狠鸳,則不處理response的數(shù)據(jù)揣苏。
對于發(fā)送數(shù)據(jù)的body,官方推薦自定義攔截器實(shí)現(xiàn)件舵。攔截器內(nèi)選用Gzip或者其他的壓縮算法對數(shù)據(jù)進(jìn)行壓縮卸察。
除了單純的壓縮,使用protobuffer代替json也是一種選擇铅祸,除了壓縮率和速度坑质,protobuffer對數(shù)據(jù)是一種天然的混淆,更安全一些临梗,但是使用起來比json要麻煩涡扼。
同樣的手段,也可以插入一個(gè)自定義的攔截器來對數(shù)據(jù)進(jìn)行加密盟庞。

網(wǎng)絡(luò)請求的監(jiān)控

okhttp在3.11版本開始提供了一個(gè)網(wǎng)絡(luò)時(shí)間監(jiān)控的回調(diào)接口HttpEventListener吃沪,能進(jìn)行一些耗時(shí)和事件統(tǒng)計(jì)。
360的方案加入攔截器統(tǒng)計(jì)響應(yīng)時(shí)間和上下行流量什猖。

網(wǎng)絡(luò)質(zhì)量的監(jiān)控

okhttp沒有這部分內(nèi)容巷波,但是有一些工具可以用,可以執(zhí)行l(wèi)inux的ping命令卸伞,在socket連接前后加入計(jì)時(shí)抹镊,使用tracerout(利用ICMP協(xié)議來查看到目標(biāo)機(jī)器鏈路中的節(jié)點(diǎn)的可達(dá)性,其報(bào)文內(nèi)會含有目標(biāo)網(wǎng)絡(luò)荤傲、主機(jī)垮耳、端口可達(dá)性等一系列信息,再加上ip協(xié)議的TTL來遍歷當(dāng)前節(jié)點(diǎn)到目標(biāo)節(jié)點(diǎn)的鏈路信息),程序?qū)崿F(xiàn)參考《traceroute程序-c語言實(shí)現(xiàn)》

弱網(wǎng)優(yōu)化和失敗處理

這部分就留坑吧终佛。okhttp對網(wǎng)絡(luò)失敗做了處理俊嗽,但是說到針對弱網(wǎng)的優(yōu)化,還是要去翻看mars铃彰。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绍豁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子牙捉,更是在濱河造成了極大的恐慌竹揍,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邪铲,死亡現(xiàn)場離奇詭異芬位,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)带到,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門昧碉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人揽惹,你說我怎么就攤上這事被饿。” “怎么了搪搏?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵锹漱,是天一觀的道長。 經(jīng)常有香客問我慕嚷,道長哥牍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任喝检,我火速辦了婚禮嗅辣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘挠说。我一直安慰自己澡谭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布损俭。 她就那樣靜靜地躺著蛙奖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杆兵。 梳的紋絲不亂的頭發(fā)上雁仲,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音琐脏,去河邊找鬼攒砖。 笑死缸兔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吹艇。 我是一名探鬼主播惰蜜,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼受神!你這毒婦竟也來了抛猖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鼻听,失蹤者是張志新(化名)和其女友劉穎财著,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體精算,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年碎连,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了灰羽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鱼辙,死狀恐怖廉嚼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情倒戏,我是刑警寧澤怠噪,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站杜跷,受9級特大地震影響傍念,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜葛闷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一憋槐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧淑趾,春花似錦阳仔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至延蟹,卻和暖如春评矩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阱飘。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工稚照, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓果录,卻偏偏與公主長得像上枕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子弱恒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

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