三捷凄、深入理解OkHttp:連接處理-ConnectIntercepter

一垄懂、前言

【1.1】OkHttp系列其他篇章:

  1. 同步請求的實現(xiàn)流程瓶颠。
  2. 異步請求的實現(xiàn)流程
  3. 重要攔截器:CacheInterceptor 的解析拟赊。
  4. 重要攔截器:ConnectInterceptor 的解析
  5. 重要攔截器:CallServerInterceptor 的解析粹淋。

【1.2】陳述

終于來到OkHttp的網(wǎng)絡(luò)連接模塊吸祟,這塊內(nèi)容是OkHttp的核心內(nèi)容。我們知道Http的連接需要進(jìn)行3此握手桃移,斷開需要4次揮手屋匕。而連接的每一次握手,都需要進(jìn)行Socket連接借杰、釋放过吻,這是一個非常麻煩而且耗時耗力的過程。那么連接的服用就顯得尤為重要了蔗衡,同個地址的連接纤虽,如果在用完后不斷開,保持連接粘都,在下次的請求中便能重復(fù)使用這個連接廓推,節(jié)省了連接的時間。這對于大部分時間需要重復(fù)頻繁訪問同一個服務(wù)器地址的移動端網(wǎng)絡(luò)來說更加不可或缺翩隧。

在本篇文章中樊展,我們將以ConnectIntercepter為起點呻纹,跟隨網(wǎng)絡(luò)連接獲取的過程,深入探究其中涉及到的:連接查找专缠、連接復(fù)用雷酪,網(wǎng)絡(luò)連接的建立(三次握手、Http2協(xié)議等的處理)涝婉。面對這復(fù)雜的過程哥力,我們先總體的走一遍連接獲取過程,然后在后續(xù)介紹 RealConnection.javaConnectionPool.java 來更深入的理解連接的建立和緩存查找等邏輯墩弯。除此之外吩跋,我們還需要先看一下另一個類:Transmitter.java ,它將在connect的過程中起到重要的地位渔工。

二锌钮、Transmmiter:應(yīng)用和Http的橋梁

【2.1】來歷和作用

RealCall.java
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.transmitter = new Transmitter(client, call);
    return call;
  }
 
Transmitter.java
  public final class Transmitter {
  
  private final OkHttpClient client;
  //重點:連接池
  private final RealConnectionPool connectionPool;
  //此次請求
  private final Call call;
  private Request request;
  //重點:連接查找器,它將承當(dāng)主要的連接查找工作引矩。
  private ExchangeFinder exchangeFinder;
  //Connecttion的實現(xiàn)類梁丘,代表著和服務(wù)器的連接。
  public RealConnection connection;
  //重點:負(fù)責(zé)請求的發(fā)送和響應(yīng)接收
  private @Nullable Exchange exchange;
  //請求是否已取消
  private boolean canceled;
  ...

  public Transmitter(OkHttpClient client, Call call) {
    this.client = client;
    this.connectionPool = Internal.instance.realConnectionPool(client.connectionPool());
    this.call = call;
    this.eventListener = client.eventListenerFactory().create(call);
    this.timeout.timeout(client.callTimeoutMillis(), MILLISECONDS);
  }
  

總結(jié):Transmitter是在創(chuàng)建RealCall的時候被創(chuàng)建的旺韭,其中需要了OkHttpClient和當(dāng)前請求Call作為參數(shù)氛谜。所以我們知道了,一個請求對應(yīng)著一個Transmitter区端。而且值漫,它的成員變量里有ExchangeFinder等類,負(fù)責(zé)為這個請求查找到一個合適的請求织盼。

【2.2】 releaseConnectionNoEvents()

這個方法是釋放一個連接惭嚣,該方法在后面的查找連接中會涉及到,我們在這里先對其進(jìn)行講述悔政。

Transmitter.java
@Nullable Socket releaseConnectionNoEvents() {
    ...
    int index = -1;
    //一個連接,可以有多個transmitter延旧,也就是用于多個請求谋国。所以在這里需要
    //找到自己的那一個。
    for (int i = 0, size = this.connection.transmitters.size(); i < size; i++) {
      Reference<Transmitter> reference = this.connection.transmitters.get(i);
      if (reference.get() == this) {
        index = i;
        break;
      }
    }

    if (index == -1) throw new IllegalStateException();

    //將自己從連接中剔除掉迁沫。
    RealConnection released = this.connection;
    released.transmitters.remove(index);
    this.connection = null;

    //如果這個請求釋放了這個連接后芦瘾,這個連接沒有被用于其他請求
    //調(diào)用連接池,使這個連接變?yōu)橐粋€空閑連接集畅。
    if (released.transmitters.isEmpty()) {
      released.idleAtNanos = System.nanoTime();
      //詳見【5.4】
      if (connectionPool.connectionBecameIdle(released)) {
        //沒人在用了近弟,把Socket返回回去。
        return released.socket();
      }
    }
    
    //還有其他請求在用挺智,就不返回socket回去祷愉。
    return null;
  }

總結(jié):這是一個請求關(guān)閉一個連接的過程。

  1. 先找到連接中對直接的索引,斷開二鳄。
  2. 判斷連接是否還有請求在用赴涵,不用使其變?yōu)榭臻e連接。

【2.1】prepareToConnect():連接的準(zhǔn)備工作

【2.2.1】
RetryAndFollowUpInterceptor.java
@Override public Response intercept(Chain chain) throws IOException {
    ...
    Transmitter transmitter = realChain.transmitter();
    ...
    while (true) {
      transmitter.prepareToConnect(request);

      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      ...
    }
  }

從上面可以看到订讼,在執(zhí)行第一個默認(rèn)攔截器的邏輯的時候髓窜,調(diào)用transmitter.prepareToConnect()方法。我們接下去看一下這個方法做了上面準(zhǔn)備工作欺殿。

【2.2.2】prepareToConnect()
Transmitter.java
 public void prepareToConnect(Request request) {
 
    if (this.request != null) {
    //如果這個Transmitter已經(jīng)有了一個請求了
    //并且他們的url所指向的地址都是同一個寄纵,那么這個連接可以復(fù)用,直接返回脖苏。
      if (sameConnection(this.request.url(), request.url())) return; 
      //如果上個請求的信息交換器不為空程拭,代表這次request還沒有結(jié)束
      //那么拋出錯誤,該Transmitter不能給新的request用帆阳。
      if (exchange != null) throw new IllegalStateException();
        
      //釋放上次的連接哺壶。
      if (exchangeFinder != null) {
        maybeReleaseConnection(null, true);
        exchangeFinder = null;
      }
    }
    
    //第一次進(jìn)來時,直接來到這里蜒谤。
    this.request = request;
    //給自己創(chuàng)建一個連接查找器山宾,注意這里的CreateAddress(),它將返回一個Adrees對象鳍徽,代表著遠(yuǎn)方服務(wù)器的一個地址资锰。
    this.exchangeFinder = new ExchangeFinder(this, connectionPool, createAddress(request.url()),
        call, eventListener);
  }

總結(jié):其實這個方法,重點就是為連接作準(zhǔn)備阶祭。但是主要目的還是找到可以復(fù)用的連接绷杜。它的邏輯如下:

  1. 如果這個Transmiiter之前已經(jīng)有過請求,而且和新請求所指向的地址是一樣的濒募,那么這個連接可以復(fù)用鞭盟,直接返回。
  2. 如果不是同個地址瑰剃,而且上個請求還沒用完(exchange != null)齿诉,那么這個Tranmitter不能復(fù)用。直接報錯晌姚。如果上個請求已經(jīng)完了粤剧, 釋放上個請求的連接。
  3. 如果這個Tranmitter是新的挥唠,那么給這個Transmiiter新創(chuàng)建一個ExcahgeFinder抵恋,請注意這個類,它很重要宝磨,將負(fù)責(zé)最主要的連接查找工作弧关。

【2.2】acquireConnectionNoEvents

Transmitter.java
 void acquireConnectionNoEvents(RealConnection connection) {
    ...
    this.connection = connection;
    connection.transmitters.add(new TransmitterReference(this, callStackTrace));
  }

總結(jié): 這個方法是代表Transmitter獲得了一個可用的連接了盅安。那么它做的工作是將這個連接保存起來。然后將自己登記到RealConnection梯醒。這個方法后面會有用到宽堆,這里先講解一下。

三茸习、查找連接

有了章節(jié)二的預(yù)備知識后畜隶,我們可以來看ConnectIntercepter了。不過他只是觸發(fā)打開連接的按鈕号胚,真正連接的查找和連接邏輯在exchangeFinder.java和Exchage.java籽慢。不管怎么樣,我們先來看一下開始的地方猫胁。

【3.1】ConnectIntercepter

ConnectIntercepter.java
@Override public Response intercept(Chain chain) throws IOException {
    ...
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //詳見【3.2】
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    return realChain.proceed(request, transmitter, exchange);
  }

調(diào)用transmitter的newExcahge()方法箱亿,得到一個可以與遠(yuǎn)程地址進(jìn)行通行的Exchage,然后就丟給下一個攔截器了弃秆。順帶說一下届惋,在第一篇《》我們知道,緊跟著ConnectIntercepter的下一個攔截器是ServerIntercepter菠赚,那我們可以很容易的推理出脑豹,它拿到了ConnectIntercepter的excahge后,就進(jìn)行了數(shù)據(jù)傳輸和數(shù)據(jù)接收衡查。

【3.2】newExchange()

Transmitter.java
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    synchronized (connectionPool) {
    ...
    //詳見3.3:find()
    //詳見四:ExchangeCodec.java
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

    synchronized (connectionPool) {
      this.exchange = result;
      this.exchangeRequestDone = false;
      this.exchangeResponseDone = false;
      return result;
    }
  }

調(diào)用exchangeFinder.find()找到一個連接瘩欺,返回ExchangeCodec。ExchangeCodec是一個接口拌牲,它代表著Http請求的加密俱饿,和響應(yīng)的解密。它有2個具體實現(xiàn):Http1ExchangeCodec和Http2ExchangeCodec塌忽,它的詳細(xì)內(nèi)容詳見【4】拍埠。我們繼續(xù)看連接的查找。

【3.4】find()

ExcahgeFinder.java
 public ExchangeCodec find(
      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 {
      //詳見【3.5】
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      //詳見【3.7】      
      return resultConnection.newCodec(client, chain);
    } catch (RouteException e) {
      trackFailure();
      throw e;
    } catch (IOException e) {
      trackFailure();
      throw new RouteException(e);
    }
  }

【3.5】findHealthyConnection()

ExcahgeFinder.java
  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      //詳見:【3.6】找到連接候選人
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // 如果這個連接是全新的土居,那么可以直接用
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // 在這里需要檢查一下這個連接是否健康的
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
       //如果不加看械拍,調(diào)用RealConnection.noNewExcahge()方法,將此連接丟棄并繼續(xù)找装盯。
        candidate.noNewExchanges();
        continue;
      }

      return candidate;
    }
  }

總結(jié): 該方法顧名思義,就是通過一個while(true)不斷的找一個連接候選人甲馋,然后檢查是否健康可用的埂奈,如果不能用就進(jìn)行標(biāo)記,丟棄定躏。詳細(xì)的如下:

  1. 調(diào)用findConnection()找到一個連接账磺。
  2. 如果全新的直接用芹敌。
  3. 如果不健康的調(diào)用RealConnection.noNewExcahge(),它內(nèi)部主要做的是noNewExchanges = true; 這個標(biāo)志為后續(xù)將會用到垮抗,用來丟棄連接氏捞。
  4. 不見看的判斷:isHealthy() 就不展開了,就是判斷connection的socket等是否被關(guān)閉了冒版,進(jìn)而判斷連接是否健康液茎。

【3.6】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;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
      //1.如果這個請求已經(jīng)被取消過捆等,那么再次請求拋出錯誤。
      if (transmitter.isCanceled()) throw new IOException("Canceled");
      ...
      
      //2. 先找到這個連接之前的路由結(jié)果
      Route previousRoute = retryCurrentRoute()
          ? transmitter.connection.route()
          : null;

      //3. 在這里嘗試使用一個已經(jīng)分配好的連接续室,但是如上文【3.5】看到的
      //他會檢查它的noNewExchange標(biāo)志為栋烤,如果是true 的話,那么這個連接不但不能用挺狰,而且還要復(fù)制給toClose明郭,關(guān)閉掉。
      //詳見【2.3】:releaseConnectionNoEvents()
      releasedConnection = transmitter.connection;
      toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
          ? transmitter.releaseConnectionNoEvents()
          : null;

      //4.如果transmitter的connection歷經(jīng)了上面的的邏輯丰泊,沒有被置空薯定,說明這個連接可用,賦值給result趁耗。
      if (transmitter.connection != null) {
        result = transmitter.connection;
        releasedConnection = null;  //這個連接可以用沉唠,不能把他釋放掉,重新置為空苛败。
      }

      //5. 如果此時的result還是為空满葛,說明上面嘗試獲取一個已經(jīng)分配好的連接失敗
      //那么這次嘗試重連接池中獲取。
      if (result == null) {
        //詳見【5.3】:嘗試獲取一個連接
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
         //獲取成功
          foundPooledConnection = true;
          result = transmitter.connection;
        } else {
          //連接池都獲取失敗的話罢屈,需要進(jìn)行路由
          selectedRoute = previousRoute;
        }
      }
    }
    //將剛剛要關(guān)閉的連接關(guān)閉嘀韧。
    closeQuietly(toClose);

    ...
    //result不空,找到一個可用的連接缠捌,直接返回锄贷。
    if (result != null) {
      return result;
    }

    // 6.進(jìn)行路由選擇
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    List<Route> routes = null;
    synchronized (connectionPool) {
      if (transmitter.isCanceled()) throw new IOException("Canceled");

      if (newRouteSelection) {
        //7. 由于有新的路由,用路由選擇的新的IP集合曼月,再次此時到連接池中找可以復(fù)用的連接谊却。
        routes = routeSelection.getAll();
        if (connectionPool.transmitterAcquirePooledConnection(
            address, transmitter, routes, false)) {
          foundPooledConnection = true;
          result = transmitter.connection;
        }
      }
       
      //8. 既然路由都沒有找到可以用的,那么就創(chuàng)建一個新的RealConnection哑芹,
      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }
        result = new RealConnection(connectionPool, selectedRoute);
        connectingConnection = result;
      }
    }

    // 9. 如果剛剛第二次在連接池找到了炎辨,那么返回這個連接。
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result);
      return result;
    }

    // 10. 詳見【4.2】說明要用新連接聪姿,那么進(jìn)行TCP+TSL連接
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    connectionPool.routeDatabase.connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      connectingConnection = null;
      //11. 當(dāng)多個連接連接到同一個主機(jī)時碴萧,在這里會進(jìn)行連接合并乙嘀。這是最后一次嘗試
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
        //說明連接池中已經(jīng)有一個可用的連接了,不需要剛剛創(chuàng)建的連接破喻。
        pooled connection.
        result.noNewExchanges = true;
        socket = result.socket();
        result = transmitter.connection;
      } else {
        //12.詳見【5.5】新創(chuàng)建的連接正常使用虎谢,將它放入池子中
        connectionPool.put(result);
        transmitter.acquireConnectionNoEvents(result);
      }
    }
    //如果有需要,丟掉剛剛新創(chuàng)建的連接
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    //終于可以返回
    return result;
  }

總結(jié):這是一個查找連接的過程曹质,在查找的時候婴噩,綜合考慮了自身的連接,路由的結(jié)果咆繁,連接池的復(fù)用讳推,和新建幾種方案。具體的如下:

  1. 嘗試獲取這個請求被已經(jīng)被分配的連接玩般,如果存在银觅,且可用,那么直接使用這個坏为,返回究驴。否則,這個連接的socket將會被關(guān)閉匀伏。
  2. 嘗試從連接池中獲取洒忧,如果可用,返回結(jié)果够颠。
  3. 嘗試使用路由器進(jìn)行路由選擇出路由結(jié)果集合熙侍,再次到連接池中進(jìn)行查找可用連接。
  4. 如果路由都找不到履磨,新建一個連接蛉抓,進(jìn)行TCP+TSL 連接。
  5. 再次到連接池中剃诅,看有沒有可用的連接巷送,避免多次連接造成重復(fù)創(chuàng)建。如果找到矛辕,關(guān)閉新建的連接笑跛。結(jié)果替換為新找到的連接。如果沒有聊品,將新連接放入連接池中飞蹂。
  6. 返回結(jié)果。

【3.7】 newCodec(): 獲得數(shù)據(jù)加解密器

ExchangeCodec newCodec(OkHttpClient client, Interceptor.Chain chain) throws SocketException {
    if (http2Connection != null) {
      return new Http2ExchangeCodec(client, this, chain, http2Connection);
    } else {
      socket.setSoTimeout(chain.readTimeoutMillis());
      source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
      return new Http1ExchangeCodec(client, this, source, sink);
    }
  }

總結(jié): 根據(jù)連接性質(zhì)不一樣翻屈,生成不同的數(shù)據(jù)加解密器陈哑。

章節(jié)小結(jié):本節(jié)從ConnectIntercepter開始,追尋了一個連接如何被獲得的過程,它涉及到了新建連接芥颈、路由選擇,連接池復(fù)用等邏輯赚抡,最終的產(chǎn)物是Exchange爬坑,由它去到下一個攔截器:ServerIntercepter進(jìn)行網(wǎng)絡(luò)傳輸工作。其中Exchange涂臣、RealConnectionPool起到了很重要角色盾计,我們將在下一小節(jié)中解析


四、RealConnection

RealConnection赁遗,描述的是一次與遠(yuǎn)程服務(wù)器的連接署辉,所以它需要具備與遠(yuǎn)程地址進(jìn)行建立連接,通行的能力岩四。這些能里我們可以在后續(xù)它的成員變量和方法中看出來哭尝。照例,我們來看一下的構(gòu)造函數(shù)和成員變量剖煌。

【4.1】成員變量和構(gòu)造

public final class RealConnection extends Http2Connection.Listener implements Connection {
  ...
  private static final int MAX_TUNNEL_ATTEMPTS = 21;

  //連接池
  public final RealConnectionPool connectionPool;
  //路由器
  private final Route route;

  //這個socket將在connect()方法中被賦值材鹦,并且不會再重新賦值。它用于底層的Socket通信耕姊。
  private Socket rawSocket;

  //代表著應(yīng)用層的Socket
  private Socket socket;
  //描述一次完整握手過程的對象桶唐。
  private Handshake handshake;
  //協(xié)議枚舉類,包括“http/1.0”茉兰、“http/3.1”等尤泽。
  private Protocol protocol;
  //代表了一個Http2的Socket連接
  private Http2Connection http2Connection;
  //與服務(wù)器進(jìn)行數(shù)據(jù)交互的流操作對象。
  private BufferedSource source;
  private BufferedSink sink;

  //表示connection的一個標(biāo)志位规脸,被connectionPool管理著坯约,并且一旦為true,將一直為true燃辖。代表著這個連接不需要新的Exchage了鬼店。
  boolean noNewExchanges;

  ...

  /** 這個連接所負(fù)載的請求 */
  final List<Reference<Transmitter>> transmitters = new ArrayList<>();
  ...

  //構(gòu)造函數(shù)需要連接池和路由器。
  public RealConnection(RealConnectionPool connectionPool, Route route) {
    this.connectionPool = connectionPool;
    this.route = route;
  }

總結(jié): 一些主要的成員變量已經(jīng)如上列出注釋黔龟。接下來從它最重要的方法connect()入手來理解它的作用妇智。

【4.2】 connect():與遠(yuǎn)程服務(wù)器建立連接

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    //當(dāng)portacol不等空時,代表連接已經(jīng)建立氏身,拋出錯誤巍棱。
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    //注意這里的ConnectSpec對象,它代表了Http的Socket通信的配置蛋欣,比如它會指定TLS協(xié)議版本航徙。
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    //1.對協(xié)議配置的一些檢查,如果配置不合法將會拋出錯誤
    //HTTP的話陷虎,判斷是否配置了不允許明文傳輸或者Android平臺規(guī)定了不允許明文傳輸到踏。不滿足的拋出錯誤杠袱。
    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    } else {
    //如果是Https連接,判斷是否配置h2_prior_knowledge窝稿。
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
      }
    }

    //從這里開始連接楣富。
    while (true) {
      try {
        //2.檢查是否需要隧道模式,如果需要就建立隧道連接伴榔。
        //如果目標(biāo)地址是Https協(xié)議纹蝴,但是又通過Http協(xié)議代理的話,將會滿足判定踪少。
        if (route.requiresTunnel()) {
          //詳見【4.3】
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          //rawSocket為空塘安,代表不能建立隧道連接,退出援奢。
          if (rawSocket == null) {
            break;
          }
        } else {
        //3.詳見【4.4】建立普通的Socket連接
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        //4. 詳見【4.5】建立協(xié)議兼犯。
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        ...
      }
    }

    //5. 對隧道連接建立失敗的處理
    if (route.requiresTunnel() && rawSocket == null) {
      ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
          + MAX_TUNNEL_ATTEMPTS);
      throw new RouteException(exception);
    }
    
    //如果是Http2協(xié)議,獲取最大并發(fā)流限制
    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

總結(jié):該方法是Connection處理連接邏輯的地方萝究,主要包括一下幾點:

  1. 通過protocol來判斷這個連接是否已經(jīng)建立好了免都,如果不為空,就代表已經(jīng)建立好帆竹,此時會拋出錯誤绕娘。
  2. 根據(jù)不同的Http連接協(xié)議,進(jìn)行配置的檢查栽连。
  3. 判斷是否建立隧道連接险领,是的話進(jìn)入隧道連接流程。它的判定條件是秒紧,HTTP代理的Http2或者Https绢陌。建立隧道連接的Http代理將不再解析數(shù)據(jù),而是直接轉(zhuǎn)發(fā)數(shù)據(jù)熔恢。
  4. 建立普通的Socket連接脐湾。
  5. 建立協(xié)議:TCL握手,HTTP/2的協(xié)商等叙淌。
  6. 對連接建立后或者失敗的一些處理

【4.3】connectTunnel():建立隧道連接

private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
      EventListener eventListener) throws IOException {
    //1. 創(chuàng)建用于隧道連接用的請求秤掌。
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
      //2. 詳見【4.4】和普通連接一樣,也需要進(jìn)行Socket連接
      connectSocket(connectTimeout, readTimeout, call, eventListener);
      //3. 創(chuàng)建隧道
      tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

      if (tunnelRequest == null) break; // Tunnel successfully created.

      // The proxy decided to close the connection after an auth challenge. We need to create a new
      // connection, but this time with the auth credentials.
      closeQuietly(rawSocket);
      rawSocket = null;
      sink = null;
      source = null;
      eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
    }
  }

總結(jié): 創(chuàng)建隧道連接鹰霍,就是在Http代理的代理上建立Https連接闻鉴。主要的做了如下事情:

  1. 創(chuàng)建一個tunnelRequest:一個通過代理建里TLS隧道的請求。由于是未加密的茂洒,它的頭部信息是只包涵了最小的頭集孟岛,這也是為了避免傳遞一些敏感的數(shù)據(jù)給到Http,比如cookie等。
  2. 進(jìn)行Socket連接渠羞。
  3. 進(jìn)行隧道連接:傳入剛剛創(chuàng)建的tunnelRequset斤贰,構(gòu)建出一個Http1ExchangeCodec對象,用于Http1協(xié)議的流操作實現(xiàn)類次询,對Http代理進(jìn)行連接請求腋舌。

【4.4】connectSocket()

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

    //1. 根據(jù)不同的代理類型來選擇不同的Socket生成策略。
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    eventListener.connectStart(call, route.socketAddress(), proxy);
    //設(shè)置超時
    rawSocket.setSoTimeout(readTimeout);
    try {
    //2. 采用平臺上的連接socket方式
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ...
    }

   //得到Socket的輸出輸入流
    try {
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    } catch (NullPointerException npe) {
      if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
        throw new IOException(npe);
      }
    }
  }

總結(jié): 該方法是與遠(yuǎn)程服務(wù)器地址建立起Socket連接渗蟹,并獲得輸入輸出流。具體的如下:

  1. 如果是直連或者Http類型時赞辩,直接通過SocketFactory新建一個Socket雌芽。否則是代理Socket,將代理傳入Socket新建一個代理Socket辨嗽。
  2. 建立Socket連接世落。Platform.get()在這里得到的是Android平臺,而它的內(nèi)部做的是也就是
socket.connect(address, connectTimeout);

在這一步connect過后糟需,socket完成了3次握手建立TCP連接屉佳。

  1. 獲得Socket的輸出輸入流。

【4.5】establishProtocol()

private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
      //1. 判斷是否為http請求
    if (route.address().sslSocketFactory() == null) {
      //2.如果http請求里包涵了“h2_prior_knowledge”協(xié)議洲押,代表是一個支持明文的http2請求武花,所以仍然開啟的是http2的連接
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        socket = rawSocket;
        protocol = Protocol.H2_PRIOR_KNOWLEDGE;
        //3. 建立http2連接
        startHttp2(pingIntervalMillis);
        return;
      }
      //4. 不屬于以上情況,正常建立http連接
      socket = rawSocket;
      protocol = Protocol.HTTP_1_1;
      return;
    }

    eventListener.secureConnectStart(call);
    //5. 詳見【4.6】建立Tls協(xié)議
    connectTls(connectionSpecSelector);
    eventListener.secureConnectEnd(call, handshake);
    
    //建立http2連接
    if (protocol == Protocol.HTTP_2) {
      startHttp2(pingIntervalMillis);
    }
  }

總結(jié): 該方法根據(jù)請求協(xié)議杈帐,來確定建立的連接是否需要進(jìn)一步協(xié)議處理体箕。具體的如下:

  1. 如果是http請求但是包涵“h2_prior_knowledge”或者是http2協(xié)議,都進(jìn)一步構(gòu)建htpp2連接挑童。
  2. 其他的為正常的http請求累铅,直接將代表底層的rawSocket賦值給應(yīng)用層的socket。

【4.6】connectTls()

private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // 1. 將剛剛得到的socket通過sslSocketFactory進(jìn)行包裝
      //得到SSLSocket對象站叼。
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      // 2. 詳見【4.7】對sslSocket進(jìn)行配置協(xié)議娃兽。
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      //3. 看情況是否進(jìn)行Tls擴(kuò)展配置
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      //4. 開始進(jìn)行三次握手
      sslSocket.startHandshake();
      SSLSession sslSocketSession = sslSocket.getSession();
      Handshake unverifiedHandshake = Handshake.get(sslSocketSession);

      //5. 對sslSocket的地址與主機(jī)地址進(jìn)行校驗,確保一致可用尽楔。
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
        List<Certificate> peerCertificates = unverifiedHandshake.peerCertificates();
        if (!peerCertificates.isEmpty()) {
          X509Certificate cert = (X509Certificate) peerCertificates.get(0);
          throw new SSLPeerUnverifiedException(
              "Hostname " + address.url().host() + " not verified:"
                  + "\n    certificate: " + CertificatePinner.pin(cert)
                  + "\n    DN: " + cert.getSubjectDN().getName()
                  + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
        } else {
          throw new SSLPeerUnverifiedException(
              "Hostname " + address.url().host() + " not verified (no certificates)");
        }
      }

      //6. 證書校驗
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      //7. 在3中如果配置了進(jìn)行擴(kuò)展投储,那么在這里將會取到協(xié)議協(xié)商的結(jié)果。
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      
      //8. 將剛才完成握手和協(xié)議校驗的sslSocket保存起來
      //并且獲得用于IO傳輸?shù)膕ource翔试、sink
      socket = sslSocket;
      source = Okio.buffer(Okio.source(socket));
      sink = Okio.buffer(Okio.sink(socket));
      handshake = unverifiedHandshake;
      protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
      success = true;
    } catch (AssertionError e) {
      ...
    } finally {
      ...
    }
  }

總結(jié): 在這個方法里轻要,連接將進(jìn)行SSL配置,三次握手垦缅,證書校驗等工作冲泥。具體的如下:

  1. 將socket包裝成SLLSocket。
  2. 對SLLSocket進(jìn)行協(xié)議配置。
  3. 如果有需要凡恍,對SLL協(xié)議進(jìn)行擴(kuò)展配置志秃。
  4. 開始三次握手。
  5. 對主機(jī)地址一致性進(jìn)行校驗嚼酝,防止握手過程中丟失浮还。
  6. 對服務(wù)器回傳回來的證書進(jìn)行合法行校驗。
  7. 如果需要闽巩,取得握手過程中钧舌,協(xié)議協(xié)商選擇出的協(xié)議。
  8. 將完成握手和協(xié)議校驗的SSLSocket保存起來涎跨,并獲得用于IO傳輸?shù)膕ource洼冻、sink。

【4.7】configureSecureSocket() 對SSLScoket進(jìn)行協(xié)議配置

ConnectionSpecSelector.java
 ConnectionSpec configureSecureSocket(SSLSocket sslSocket) throws IOException {
    ConnectionSpec tlsConfiguration = null;
    for (int i = nextModeIndex, size = connectionSpecs.size(); i < size; i++) {
      ConnectionSpec connectionSpec = connectionSpecs.get(i);
      if (connectionSpec.isCompatible(sslSocket)) {
        tlsConfiguration = connectionSpec;
        nextModeIndex = i + 1;
        break;
      }
    }
    ...
    Internal.instance.apply(tlsConfiguration, sslSocket, isFallback);
    return tlsConfiguration;
  }

總結(jié): 可以看到隅很,對SSLScoket配置撞牢,就是遍歷connectionSpecs集合,然后挑出適合于這個sslScoket的配置叔营,然后進(jìn)行要用屋彪。具體的如下:

  1. connectionSpecs集合:在OkHttpClient創(chuàng)建的時候有默認(rèn)值:
OkHttpClient.java
 static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
      ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
  1. 協(xié)議的應(yīng)用最終會調(diào)用到ConnectionSpec.apply() 方法,對SSLScoket進(jìn)行tsl版本绒尊,設(shè)置密碼套件畜挥。

【4.8】ConnectionSpec.apply(): 協(xié)議應(yīng)用

ConnectionSpec.java
 void apply(SSLSocket sslSocket, boolean isFallback) {
    ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);

    if (specToApply.tlsVersions != null) {
      sslSocket.setEnabledProtocols(specToApply.tlsVersions);
    }
    if (specToApply.cipherSuites != null) {
      sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
    }
  }

總結(jié): 對這個socket設(shè)置tls版本和密碼套件

【4.9】isEligible():判斷連接是否可復(fù)用的邏輯

RealConnection.java
boolean isEligible(Address address, @Nullable List<Route> routes) {
    // 如果這個連接所承載的請求達(dá)到最大,則不能重用
    if (transmitters.size() >= allocationLimit || noNewExchanges) return false;

    // 如果不是Host域婴谱,看他們地址是否完全一樣砰嘁。
    if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

    // Host域相同,返回可以復(fù)用的結(jié)果勘究。
    if (address.url().host().equals(this.route().address().url().host())) {
      return true; 
    }

    //下面是Http2連接復(fù)用相關(guān)矮湘。
    ....
    
    return true; 
  }

總結(jié): 這個方法在后續(xù)的解析中會涉及到,所以先放在這里講了口糕。主要是用來判斷這個連接可不可以復(fù)用的祭衩。判斷條件如注釋演熟。


五钾唬、ConnectiongPool:連接池

在3.6的findConnetion過程中仓犬,我們看到了很多次連接池的身影,它對連接的復(fù)用也起著絕對重要的位置超棺,如果不仔細(xì)的理解它的話向族,查找連接這塊的邏輯就會少一大快。照例棠绘,從它的出生件相、成員變量和構(gòu)造函數(shù)來初步認(rèn)識它再扭。

【5.1】RealConnection 的出生

OkHttpClient.Builder.java
public Builder() {
      ...
      connectionPool = new ConnectionPool();
    }

在Builder()里創(chuàng)建默認(rèn)的連接池。

public final class ConnectionPool {
  final RealConnectionPool delegate;
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.delegate = new RealConnectionPool(maxIdleConnections, keepAliveDuration, timeUnit);
  }

總結(jié): 可以看出到夜矗,ConectionPool才用代理模式泛范,實際邏輯交給RealConnection()。5個最大空閑連接紊撕,每個連接可卑盏矗活5分鐘。

【5.2】成員變量和構(gòu)造函數(shù)

public final class RealConnectionPool{
 //
 private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));

  /** 每個地址可保持的最大空閑連接 */
  private final int maxIdleConnections;
  //連接的倍苑觯活時間
  private final long keepAliveDurationNs;
  //連接清理任務(wù)
  private final Runnable cleanupRunnable = () -> {
    while (true) {
    //詳見【5.6】
      long waitNanos = cleanup(System.nanoTime());
      if (waitNanos == -1) return;
      if (waitNanos > 0) {
        long waitMillis = waitNanos / 1000000L;
        waitNanos -= (waitMillis * 1000000L);
        synchronized (RealConnectionPool.this) {
          try {
            //等待喚醒執(zhí)行清理任務(wù)区赵。
            RealConnectionPool.this.wait(waitMillis, (int) waitNanos);
          } catch (InterruptedException ignored) {
          }
        }
      }
    }
  };

  //連接集合,采用雙向鏈標(biāo)數(shù)據(jù)結(jié)構(gòu)
  private final Deque<RealConnection> connections = new ArrayDeque<>();
  
  //路由數(shù)據(jù)庫
  final RouteDatabase routeDatabase = new RouteDatabase();
  
  //清除任務(wù)執(zhí)行標(biāo)志
  boolean cleanupRunning;

  /**
  *構(gòu)造函數(shù)
  */
  public RealConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
    ...
  }
}

總結(jié): 可以看出浪南,這個連接池是用來管理同個地址的連接的惧笛。它提供根據(jù)地址查找可用連接、清除連接等功能逞泄。接下來介紹一下它的幾個重要方法。

【5.3】transmitterAcquirePooledConnection():獲取連接

boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
      @Nullable List<Route> routes, boolean requireMultiplexed) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (requireMultiplexed && !connection.isMultiplexed()) continue;
      【詳見4.9】
      if (!connection.isEligible(address, routes)) continue;
      【詳見2.2】
      transmitter.acquireConnectionNoEvents(connection);
      return true;
    }
    return false;
  }

總結(jié): 遍歷保存的連接拜效,調(diào)用RealConnection.isEligible() 來判斷這個連接是否符合條件喷众。將這個請求的Transmitter登記到RealConnection。

【5.4】connectionBecameIdle()

boolean connectionBecameIdle(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (connection.noNewExchanges || maxIdleConnections == 0) {
      connections.remove(connection);
      return true;
    } else {
      //通知清理任務(wù)執(zhí)行紧憾。
      notifyAll(); 
      connection limit.
      return false;
    }
  }

總結(jié): 將一個連接變?yōu)榭臻e連接到千。如果此時這個連接不可用的話,將連接從連接集合中移除赴穗,并返回true憔四。如果還可以,通知清理任務(wù)執(zhí)行般眉,并返回false了赵。

【5.5】put()

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

總結(jié): 該方法是將一個連接放入連接池中,然后執(zhí)行清理任務(wù)甸赃,不過它會被堵塞住柿汛,直到【5.4】方法觸發(fā)。

【5.6】cleanup()

long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    synchronized (this) {
    //1. 遍歷連接池
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //2. 如果連接還在用埠对,繼續(xù)遍歷
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        //3. 找出最長空閑時間和對于的連接
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

        //4. 清除空閑最長的連接络断,而且需要滿足如下條件:
        //a. 空閑時間大于最大保活時間项玛。
        //b. 空閑連接數(shù)大于最大空閑連接數(shù)
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // 清理不了貌笨,返回下次清理需要的時間
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 沒有空閑連接,返回keepAliveDuration時間襟沮,代表keepAliveDuration后再執(zhí)行锥惋。
        return keepAliveDurationNs;
      } else {
        // 沒有空閑或者在用的連接昌腰,清理結(jié)束。
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // 已經(jīng)清理了一個净刮,會立即再執(zhí)行清理任務(wù)剥哑。
    return 0;
  }

總結(jié): 這是一個清理連接的方法,它做的使其如下:

  1. 遍歷連接淹父,如果連接還在用跳過株婴。
  2. 找出最長空閑時間和其連接。
  3. 如果滿足條件下暑认,將連接移除連接池困介。然后觸發(fā)再執(zhí)行一次清理任務(wù)。
  4. 如果沒有找到蘸际,返回下次需要清理的時間或者-1代表結(jié)束清理座哩。

小篇結(jié):本篇是介紹OkHttp的網(wǎng)絡(luò)連接建立。開篇先介紹了Trasnmitter這一重要的類粮彤,隨后從ConnectIntercepter入手根穷,深入研究了連接Connection的獲取邏輯。在獲取的過程中导坟,我們將到了連接緩存的處理屿良。當(dāng)獲取不到緩存的時候,便會新建一個全新的網(wǎng)絡(luò)連接惫周,在這個過程中會進(jìn)行Http的3次握手等過程尘惧。在最后2小節(jié)中,分別介紹了在整個過程中的中心類递递,被查找對象:RealConnection喷橙。和管理緩存Connection的ConnectionPool。最后以一張圖來總結(jié)這一過程

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末登舞,一起剝皮案震驚了整個濱河市贰逾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌菠秒,老刑警劉巖似踱,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異稽煤,居然都是意外死亡核芽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門酵熙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來轧简,“玉大人,你說我怎么就攤上這事匾二∠溃” “怎么了拳芙?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長皮璧。 經(jīng)常有香客問我舟扎,道長,這世上最難降的妖魔是什么悴务? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任睹限,我火速辦了婚禮,結(jié)果婚禮上讯檐,老公的妹妹穿的比我還像新娘羡疗。我一直安慰自己,他們只是感情好别洪,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布叨恨。 她就那樣靜靜地躺著,像睡著了一般挖垛。 火紅的嫁衣襯著肌膚如雪痒钝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天痢毒,我揣著相機(jī)與錄音送矩,去河邊找鬼。 笑死闸准,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的梢灭。 我是一名探鬼主播夷家,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼敏释!你這毒婦竟也來了库快?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤钥顽,失蹤者是張志新(化名)和其女友劉穎义屏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜂大,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡闽铐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奶浦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兄墅。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖澳叉,靈堂內(nèi)的尸體忽然破棺而出隙咸,到底是詐尸還是另有隱情沐悦,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布五督,位于F島的核電站藏否,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏充包。R本人自食惡果不足惜副签,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望误证。 院中可真熱鬧继薛,春花似錦、人聲如沸愈捅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蓝谨。三九已至灌具,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間譬巫,已是汗流浹背咖楣。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留芦昔,地道東北人诱贿。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像咕缎,于是被迫代替她去往敵國和親珠十。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345