OkHttp的ConnectInterceptor連接攔截器剖析

OkHttp的CacheInterceptor緩存攔截器剖析傳送門:http://www.reibang.com/p/3c07a12dc8ed
在RetryAndFollowUpInterceptor里初始化了一個StreamAllocation對象,這個StreamAllocation對象里初始化了一個Socket對象用來做連接,但是并沒有真正的連接浴韭,等到處理完hader和緩存信息之后,才調(diào)用ConnectInterceptor來進(jìn)行真正的連接屡立。
這個類源碼很少,如下:

/** Opens a connection to the target server and proceeds to the next interceptor.
 *  OkHttp當(dāng)中的真正的網(wǎng)絡(luò)請求都是通過網(wǎng)絡(luò)連接器來實現(xiàn)的
 * */
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();
        // 建立Http網(wǎng)絡(luò)請求所有需要的網(wǎng)絡(luò)組件 ,在RetryAndFollowUpInterceptor創(chuàng)建了StreamAllocation万哪,在這里使用
        StreamAllocation streamAllocation = realChain.streamAllocation();

        // We need the network to satisfy this request. Possibly for validating a conditional GET.
        // 我們需要網(wǎng)絡(luò)來滿足這個要求侠驯。 可能用于驗證條件GET
        boolean doExtensiveHealthChecks = !request.method().equals("GET");
        // HttpCodec用來編碼Request,解碼Response
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        // RealConnection用來進(jìn)行實際的網(wǎng)絡(luò)io傳輸?shù)募唇⑦B接
        RealConnection connection = streamAllocation.connection(); // 很關(guān)鍵的

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

在這里分析一下我們在RetryAndFollowUpInterceptor提到的StreamAllocation對象;
StreamAllocation相當(dāng)于是個管理類,維護(hù)了Connections奕巍、Streams和Calls之間的管理,該類初始化一個Socket連接對象儒士,獲取輸入/輸出流對象的止。
順著上面連接攔截器的方法點進(jìn)去看看下面這個方法:

streamAllocation.newStream

// 創(chuàng)建HttpCodec
    public HttpCodec newStream(OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {

        int connectTimeout = chain.connectTimeoutMillis(); // 設(shè)置的連接超時時間
        int readTimeout = chain.readTimeoutMillis();  // 讀取超時
        int writeTimeout = chain.writeTimeoutMillis(); // 寫入超時
        int pingIntervalMillis = client.pingIntervalMillis(); // Web socket ping 間隔 (毫秒) 定時通知服務(wù)器,為心跳連接做準(zhǔn)備着撩,如果pingIntervalMillis 設(shè)置為0的時候 心跳executor是不會執(zhí)行的
        boolean connectionRetryEnabled = client.retryOnConnectionFailure();  // 連接失敗是否重試

        try {
            // 生成實際的網(wǎng)絡(luò)連接類 诅福,RealConnection利用Socket建立連接
            RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
                    writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
            // 通過網(wǎng)絡(luò)連接的實際類生成網(wǎng)絡(luò)請求和網(wǎng)絡(luò)響應(yīng)的編碼類
            HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

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

這是一個創(chuàng)建HttpCodec的方法,這是一個接口拖叙,有兩個實現(xiàn)類氓润,Http1Codec和Http2Codec,是根據(jù)協(xié)議版本去分別創(chuàng)建的薯鳍。Http1.x和Http2.x協(xié)議咖气。

方法findHealthyConnection():

 /**
     * 找到一個連接,如果它是健康的挖滤,則返回它.
     * 如果不正常(健康)崩溪,則重復(fù)該過程,直到找到正常連接為止
     */
    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) {
                if (candidate.successCount == 0) { // 等于0的時候表示整個網(wǎng)絡(luò)請求已經(jīng)結(jié)束了
                    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.
            // 不健康斩松,網(wǎng)絡(luò)鏈接沒及時關(guān)閉伶唯,輸入輸出流沒有及時關(guān)閉,這時候就認(rèn)為不健康
            if (!candidate.isHealthy(doExtensiveHealthChecks)) { // 當(dāng)這個網(wǎng)絡(luò)連接類不健康
                noNewStreams(); // 回收網(wǎng)絡(luò)請求資源
                continue; // 跳出這次循環(huán)惧盹,接著下一次循環(huán)
            }

            return candidate;
        }
    }

方法findConnection():

/**
     * Returns a connection to host a new stream. This prefers the existing connection if it exists,
     * then the pool, finally building a new connection.
     * 返回一個連接來托管一個新的流乳幸。 可以復(fù)用現(xiàn)有的連接(如果存在的話),然后是池钧椰,最后建立一個新的連接
      * 調(diào)用該方法的RealConnection.connect()方法建立連接,connect-->connectSocket()進(jìn)行socket連接-->Platform.get().connectSocket()-->socket.connect(address, connectTimeout);(此時進(jìn)行了三次握手),握手完成后調(diào)用establishProtocol()粹断。
     */
    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.
            // 翻譯上面的注釋:嘗試使用已分配的連接。 我們在這里需要小心演侯,因為我們已經(jīng)分配的連接可能已經(jīng)被限制在創(chuàng)建新的流中
            releasedConnection = this.connection; // 直接復(fù)用
            toClose = releaseIfNoNewStreams();
            // 查看是否有完好的連接
            if (this.connection != null) {
                // We had an already-allocated connection and it's good.
                result = this.connection;
                releasedConnection = null;
            }
            if (!reportedAcquired) {
                // If the connection was never reported acquired, don't report it as released!
                releasedConnection = null;
            }
            // 連接池中是否用可用的連接姿染,有則使用
            if (result == null) {
                // Attempt to get a connection from the pool. 從連接池中返回一個RealConnection
                Internal.instance.get(connectionPool, address, this, 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.
            // 如果我們找到了已經(jīng)分配或者連接的連接,我們就完成了,直接返回
            return result;
        }

        // If we need a route selection, make one. This is a blocking operation.
        // 如果我們需要路線選擇悬赏,請選擇一個狡汉。 這是一項阻止操作。
        // 線程的選擇闽颇,多IP操作
        boolean newRouteSelection = false;
        if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
            newRouteSelection = true;
            routeSelection = routeSelector.next();
        }

        // 如果沒有可用連接盾戴,則自己創(chuàng)建一個
        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.
                // 現(xiàn)在我們有一組IP地址,再次嘗試從池中獲取連接兵多。 這可能由于連接合并而匹配
                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.
                // 創(chuàng)建一個連接并立即將其分配給該分配尖啡。 這使得異步cancel()可以中斷我們即將進(jìn)行的握手
                route = selectedRoute;
                refusedStreamCount = 0;
                result = new RealConnection(connectionPool, selectedRoute); // 創(chuàng)建連接
                acquire(result, false);
            }
        }

        // If we found a pooled connection on the 2nd time around, we're done.
        // 如果我們第二次發(fā)現(xiàn)一個連接池,我們就完成了
        if (foundPooledConnection) {
            eventListener.connectionAcquired(call, result);
            return result;
        }

        // Do TCP + TLS handshakes. This is a blocking operation. 進(jìn)行實際的網(wǎng)絡(luò)連接
        // TODO 連接具體方法 開始TCP以及TLS握手操作,這是阻塞操作
        result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                connectionRetryEnabled, call, eventListener);
        routeDatabase().connected(result.route());

        // 將新創(chuàng)建的連接剩膘,放在連接池中
        Socket socket = null;
        synchronized (connectionPool) {
            reportedAcquired = true;

            // Pool the connection. 緊接著把這個RealConnection放入連接池中
            Internal.instance.put(connectionPool, result);

            // If another multiplexed connection to the same address was created concurrently, then
            // release this connection and acquire that one.
            // 如果同時創(chuàng)建了到同一地址的另一個多路復(fù)用連接衅斩,則釋放此連接并獲取該連接
            if (result.isMultiplexed()) {
                socket = Internal.instance.deduplicate(connectionPool, address, this);
                result = connection;
            }
        }
        closeQuietly(socket);

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

上面方法中標(biāo)記:TODO 連接具體方法 result.connect()點擊進(jìn)去。
RealConnection類中的方法connect()怠褐,這里注意RealConnection繼承Http2Connection.Listener畏梆,說明走的是http2.x協(xié)議,效率更高奈懒。
下面這幾個方法都是出自RealConnection類奠涌,介紹怎么創(chuàng)建隧道建立連接。

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
                        int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
                        EventListener eventListener) {
        if (protocol != null) throw new IllegalStateException("already connected");
        //線路選擇
        RouteException routeException = null;
        List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
        ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

        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"));
            }
        }
        //開始連接
        while (true) {
            try {
                //建立隧道連接
                if (route.requiresTunnel()) {
                    connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
                    if (rawSocket == null) {
                        // We were unable to connect the tunnel but properly closed down our resources.
                        //我們無法連接隧道磷杏,但正確關(guān)閉了我們的資源溜畅。
                        break;
                    }
                } else {
                    //建立普通連接
                    connectSocket(connectTimeout, readTimeout, call, eventListener);
                }
                // 建立協(xié)議
                //不管是建立隧道連接,還是建立普通連接极祸,都少不了 建立協(xié)議 這一步慈格。
                // 這一步是在建立好了TCP連接之后,而在該TCP能被拿來收發(fā)數(shù)據(jù)之前執(zhí)行的贿肩。
                // 它主要為數(shù)據(jù)的加密傳輸做一些初始化峦椰,比如TLS握手,HTTP/2的協(xié)議協(xié)商等
                establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
                eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
                break;
                //完成連接
            } catch (IOException e) {
                closeQuietly(socket);
                closeQuietly(rawSocket);
                socket = null;
                rawSocket = null;
                source = null;
                sink = null;
                handshake = null;
                protocol = null;
                http2Connection = null;

                eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);

                if (routeException == null) {
                    routeException = new RouteException(e);
                } else {
                    routeException.addConnectException(e);
                }

                if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
                    throw routeException;
                }
            }
        }

        if (route.requiresTunnel() && rawSocket == null) {
            ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
                    + MAX_TUNNEL_ATTEMPTS);
            throw new RouteException(exception);
        }

        if (http2Connection != null) {
            synchronized (connectionPool) {
                allocationLimit = http2Connection.maxConcurrentStreams();
            }
        }
    }

方法connectSocket()

/**
     * Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket.
     * 完成在原始套接字上構(gòu)建完整的HTTP或HTTPS連接所需的所有工作汰规。
     */
    private void connectSocket(int connectTimeout, int readTimeout, Call call,
                               EventListener eventListener) 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);

        eventListener.connectStart(call, route.socketAddress(), proxy);
        rawSocket.setSoTimeout(readTimeout);
        try {
            //建立Socket連接
            Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
        } catch (ConnectException e) {
            ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
            ce.initCause(e);
            throw ce;
        }

        // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
    // More details:
    // https://github.com/square/okhttp/issues/3245
    // https://android-review.googlesource.com/#/c/271775/
        try {
            //獲取輸入/輸出流 使用的Okio庫
            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);
            }
        }
    }

// Platform.get().connectSocket
public class Platform {
  public void connectSocket(Socket socket, InetSocketAddress address,
      int connectTimeout) throws IOException {
    //最終調(diào)用java的connect
    socket.connect(address, connectTimeout);
  }
}

connectTunnel()隧道鏈接

/**
     * Does all the work to build an HTTPS connection over a proxy tunnel. The catch here is that a
     * proxy server can issue an auth challenge and then close the connection.
     * 是否通過代理隧道建立HTTPS連接的所有工作汤功。 這里的問題是代理服務(wù)器可以發(fā)出一個驗證質(zhì)詢,然后關(guān)閉連接溜哮。
     */
    private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
                               EventListener eventListener) throws IOException {
        // 構(gòu)造一個 建立隧道連接 請求滔金。
        Request tunnelRequest = createTunnelRequest();
        HttpUrl url = tunnelRequest.url();
        for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
            // 與HTTP代理服務(wù)器建立TCP連接。
            connectSocket(connectTimeout, readTimeout, call, eventListener);
            // 創(chuàng)建隧道茂嗓。這主要是將 建立隧道連接 請求發(fā)送給HTTP代理服務(wù)器餐茵,并處理它的響應(yī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);
            // 重復(fù)上面的代碼,直到建立好了隧道連接,當(dāng)然最多21次述吸,      
            // MAX_TUNNEL_ATTEMPTS=21,在類中寫死的常量忿族。
        }
    }

隧道創(chuàng)建方法createTunnel()锣笨,返回一個Request對象:

/**
     * To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create
     * the proxy connection. This may need to be retried if the proxy requires authorization.
     * 要通過HTTP代理建立HTTPS連接,請發(fā)送未加密的CONNECT請求以創(chuàng)建代理連接道批。 如果代理需要授權(quán)错英,則可能需要重試。
     */
    private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
                                 HttpUrl url) throws IOException {
        // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
        // 在每個SSL +代理連接的第一個消息對上創(chuàng)建一個SSL隧道隆豹。
        String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
        while (true) {
            Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
            source.timeout().timeout(readTimeout, MILLISECONDS);
            sink.timeout().timeout(writeTimeout, MILLISECONDS);
            tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
            //sink.flush();
            tunnelConnection.finishRequest();
            Response response = tunnelConnection.readResponseHeaders(false)
                    .request(tunnelRequest)
                    .build();
            // The response body from a CONNECT should be empty, but if it is not then we should consume
            // it before proceeding.
            long contentLength = HttpHeaders.contentLength(response);
            if (contentLength == -1L) {
                contentLength = 0L;
            }
            Source body = tunnelConnection.newFixedLengthSource(contentLength);
            Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
            body.close();

            switch (response.code()) {
                case HTTP_OK:
                    // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
                    // that happens, then we will have buffered bytes that are needed by the SSLSocket!
                    // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
                    // that it will almost certainly fail because the proxy has sent unexpected data.
                    if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
                        throw new IOException("TLS tunnel buffered too many bytes!");
                    }
                    return null;

                case HTTP_PROXY_AUTH:
                    tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
                    if (tunnelRequest == null)
                        throw new IOException("Failed to authenticate with proxy");

                    if ("close".equalsIgnoreCase(response.header("Connection"))) {
                        return tunnelRequest;
                    }
                    break;

                default:
                    throw new IOException(
                            "Unexpected response code for CONNECT: " + response.code());
            }
        }
    }

到這里把連接攔截器的具體過程剖析完了椭岩,當(dāng)然只是把核心的代碼過了一下,詳細(xì)的代碼太多了璃赡,有興趣的可以自己去官網(wǎng)下載下來看:https://github.com/square/okhttp/blob/master/okhttp/src/main/java/okhttp3/internal/connection/ConnectInterceptor.java
下一篇把ConnectionPool連接池也說一下判哥,這個類本身不大,但在連接這里起到很大的作用碉考,具體我們下篇再講塌计。

感謝閱讀,歡迎糾錯

連接攔截器中用到的復(fù)用連接池ConnectionPool傳送門:http://www.reibang.com/p/522b3c7bf333

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侯谁,一起剝皮案震驚了整個濱河市夺荒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌良蒸,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伍玖,死亡現(xiàn)場離奇詭異嫩痰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)窍箍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門串纺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人椰棘,你說我怎么就攤上這事纺棺。” “怎么了邪狞?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵祷蝌,是天一觀的道長。 經(jīng)常有香客問我帆卓,道長巨朦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任剑令,我火速辦了婚禮糊啡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吁津。我一直安慰自己棚蓄,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著梭依,像睡著了一般稍算。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上睛挚,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天邪蛔,我揣著相機(jī)與錄音,去河邊找鬼扎狱。 笑死侧到,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的淤击。 我是一名探鬼主播匠抗,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼污抬!你這毒婦竟也來了汞贸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤印机,失蹤者是張志新(化名)和其女友劉穎矢腻,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體射赛,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡多柑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了楣责。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竣灌。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖秆麸,靈堂內(nèi)的尸體忽然破棺而出初嘹,到底是詐尸還是另有隱情,我是刑警寧澤沮趣,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布屯烦,位于F島的核電站,受9級特大地震影響兔毒,放射性物質(zhì)發(fā)生泄漏漫贞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一育叁、第九天 我趴在偏房一處隱蔽的房頂上張望迅脐。 院中可真熱鬧,春花似錦豪嗽、人聲如沸谴蔑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隐锭。三九已至窃躲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钦睡,已是汗流浹背蒂窒。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留荞怒,地道東北人洒琢。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像褐桌,于是被迫代替她去往敵國和親衰抑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

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