一文讓你徹底理解 Java NIO 核心組件

同步滩褥、異步病蛉、阻塞、非阻塞

首先瑰煎,這幾個概念非常容易搞混淆铺然,但NIO中又有涉及,所以總結(jié)一下[1]酒甸。

  • 同步:API調(diào)用返回時調(diào)用者就知道操作的結(jié)果如何了(實際讀取/寫入了多少字節(jié))魄健。
  • 異步:相對于同步,API調(diào)用返回時調(diào)用者不知道操作的結(jié)果烘挫,后面才會回調(diào)通知結(jié)果诀艰。
  • 阻塞:當無數(shù)據(jù)可讀柬甥,或者不能寫入所有數(shù)據(jù)時饮六,掛起當前線程等待。
  • 非阻塞:讀取時苛蒲,可以讀多少數(shù)據(jù)就讀多少然后返回卤橄,寫入時,可以寫入多少數(shù)據(jù)就寫入多少然后返回臂外。

對于I/O操作窟扑,根據(jù)Oracle官網(wǎng)的文檔,同步異步的劃分標準是“調(diào)用者是否需要等待I/O操作完成”漏健,這個“等待I/O操作完成”的意思不是指一定要讀取到數(shù)據(jù)或者說寫入所有數(shù)據(jù)嚎货,而是指真正進行I/O操作時,比如數(shù)據(jù)在TCP/IP協(xié)議棧緩沖區(qū)和JVM緩沖區(qū)之間傳輸?shù)倪@段時間蔫浆,調(diào)用者是否要等待殖属。

所以,我們常用的 read() 和 write() 方法都是同步I/O瓦盛,同步I/O又分為阻塞和非阻塞兩種模式洗显,如果是非阻塞模式外潜,檢測到無數(shù)據(jù)可讀時,直接就返回了挠唆,并沒有真正執(zhí)行I/O操作处窥。

總結(jié)就是,Java中實際上只有 同步阻塞I/O玄组、同步非阻塞I/O 與 異步I/O 三種機制滔驾,我們下文所說的是前兩種,JDK 1.7才開始引入異步 I/O俄讹,那稱之為NIO.2嵌灰。

傳統(tǒng)IO

我們知道,一個新技術的出現(xiàn)總是伴隨著改進和提升颅悉,Java NIO的出現(xiàn)亦如此沽瞭。

傳統(tǒng) I/O 是阻塞式I/O,主要問題是系統(tǒng)資源的浪費剩瓶。比如我們?yōu)榱俗x取一個TCP連接的數(shù)據(jù)驹溃,調(diào)用 InputStream 的 read() 方法,這會使當前線程被掛起延曙,直到有數(shù)據(jù)到達才被喚醒豌鹤,那該線程在數(shù)據(jù)到達這段時間內(nèi),占用著內(nèi)存資源(存儲線程棧)卻無所作為枝缔,也就是俗話說的占著茅坑不拉屎布疙,為了讀取其他連接的數(shù)據(jù),我們不得不啟動另外的線程愿卸。在并發(fā)連接數(shù)量不多的時候灵临,這可能沒什么問題,然而當連接數(shù)量達到一定規(guī)模趴荸,內(nèi)存資源會被大量線程消耗殆盡儒溉。另一方面,線程切換需要更改處理器的狀態(tài)发钝,比如程序計數(shù)器顿涣、寄存器的值,因此非常頻繁的在大量線程之間切換酝豪,同樣是一種資源浪費涛碑。

隨著技術的發(fā)展,現(xiàn)代操作系統(tǒng)提供了新的I/O機制孵淘,可以避免這種資源浪費蒲障。基于此,誕生了Java NIO晌涕,NIO的代表性特征就是非阻塞I/O滋捶。緊接著我們發(fā)現(xiàn),簡單的使用非阻塞I/O并不能解決問題余黎,因為在非阻塞模式下重窟,read()方法在沒有讀取到數(shù)據(jù)時就會立即返回,不知道數(shù)據(jù)何時到達的我們惧财,只能不停的調(diào)用read()方法進行重試巡扇,這顯然太浪費CPU資源了,從下文可以知道垮衷,Selector組件正是為解決此問題而生厅翔。

Java NIO 核心組件

1.Channel

概念

Java NIO中的所有I/O操作都基于Channel對象,就像流操作都要基于Stream對象一樣搀突,因此很有必要先了解Channel是什么刀闷。以下內(nèi)容摘自JDK 1.8的文檔

A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.

從上述內(nèi)容可知,一個Channel(通道)代表和某一實體的連接仰迁,這個實體可以是文件甸昏、網(wǎng)絡套接字等。也就是說徐许,通道是Java NIO提供的一座橋梁施蜜,用于我們的程序和操作系統(tǒng)底層I/O服務進行交互。

通道是一種很基本很抽象的描述雌隅,和不同的I/O服務交互翻默,執(zhí)行不同的I/O操作,實現(xiàn)不一樣恰起,因此具體的有FileChannel修械、SocketChannel等。

通道使用起來跟Stream比較像村缸,可以讀取數(shù)據(jù)到Buffer中祠肥,也可以把Buffer中的數(shù)據(jù)寫入通道。


1.jpg

當然梯皿,也有區(qū)別,主要體現(xiàn)在如下兩點:

  • 一個通道县恕,既可以讀又可以寫东羹,而一個Stream是單向的(所以分 InputStream 和 OutputStream)
  • 通道有非阻塞I/O模式

實現(xiàn)

Java NIO中最常用的通道實現(xiàn)是如下幾個,可以看出跟傳統(tǒng)的 I/O 操作類是一一對應的忠烛。

  • FileChannel:讀寫文件
  • DatagramChannel: UDP協(xié)議網(wǎng)絡通信
  • SocketChannel:TCP協(xié)議網(wǎng)絡通信
  • ServerSocketChannel:監(jiān)聽TCP連接

2.Buffer

NIO中所使用的緩沖區(qū)不是一個簡單的byte數(shù)組属提,而是封裝過的Buffer類,通過它提供的API,我們可以靈活的操縱數(shù)據(jù)冤议,下面細細道來斟薇。

與Java基本類型相對應,NIO提供了多種 Buffer 類型恕酸,如ByteBuffer堪滨、CharBuffer、IntBuffer等蕊温,區(qū)別就是讀寫緩沖區(qū)時的單位長度不一樣(以對應類型的變量為單位進行讀寫)袱箱。

Buffer中有3個很重要的變量,它們是理解Buffer工作機制的關鍵义矛,分別是

  • capacity (總?cè)萘浚?/li>
  • position (指針當前位置)
  • limit (讀/寫邊界位置)

Buffer的工作方式跟C語言里的字符數(shù)組非常的像发笔,類比一下,capacity就是數(shù)組的總長度凉翻,position就是我們讀/寫字符的下標變量了讨,limit就是結(jié)束符的位置。Buffer初始時3個變量的情況如下圖

2.jpg

在對Buffer進行讀/寫的過程中制轰,position會往后移動量蕊,而 limit 就是 position 移動的邊界。由此不難想象艇挨,在對Buffer進行寫入操作時残炮,limit應當設置為capacity的大小,而對Buffer進行讀取操作時缩滨,limit應當設置為數(shù)據(jù)的實際結(jié)束位置势就。(注意:將Buffer數(shù)據(jù) 寫入 通道是Buffer 讀取 操作,從通道 讀取 數(shù)據(jù)到Buffer是Buffer 寫入 操作)

在對Buffer進行讀/寫操作前脉漏,我們可以調(diào)用Buffer類提供的一些輔助方法來正確設置 position 和 limit 的值苞冯,主要有如下幾個

  • flip(): 設置 limit 為 position 的值,然后 position 置為0侧巨。對Buffer進行讀取操作前調(diào)用舅锄。
  • rewind(): 僅僅將 position 置0。一般是在重新讀取Buffer數(shù)據(jù)前調(diào)用司忱,比如要讀取同一個Buffer的數(shù)據(jù)寫入多個通道時會用到皇忿。
  • clear(): 回到初始狀態(tài),即 limit 等于 capacity坦仍,position 置0鳍烁。重新對Buffer進行寫入操作前調(diào)用。
  • compact(): 將未讀取完的數(shù)據(jù)(position 與 limit 之間的數(shù)據(jù))移動到緩沖區(qū)開頭繁扎,并將 position 設置為這段數(shù)據(jù)末尾的下一個位置幔荒。其實就等價于重新向緩沖區(qū)中寫入了這么一段數(shù)據(jù)糊闽。

然后,看一個實例爹梁,使用 FileChannel 讀寫文本文件右犹,通過這個例子驗證通道可讀可寫的特性以及Buffer的基本用法(注意 FileChannel 不能設置為非阻塞模式)。

FileChannel channel = new RandomAccessFile("test.txt", "rw").getChannel();
    channel.position(channel.size());  // 移動文件指針到末尾(追加寫入)

    ByteBuffer byteBuffer = ByteBuffer.allocate(20);

    // 數(shù)據(jù)寫入Buffer
    byteBuffer.put("你好姚垃,世界念链!\n".getBytes(StandardCharsets.UTF_8));

    // Buffer -> Channel
    byteBuffer.flip();
    while (byteBuffer.hasRemaining()) {
        channel.write(byteBuffer);
    }

    channel.position(0); // 移動文件指針到開頭(從頭讀取)
    CharBuffer charBuffer = CharBuffer.allocate(10);
    CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();

    // 讀出所有數(shù)據(jù)
    byteBuffer.clear();
    while (channel.read(byteBuffer) != -1 || byteBuffer.position() > 0) {
        byteBuffer.flip();

        // 使用UTF-8解碼器解碼
        charBuffer.clear();
        decoder.decode(byteBuffer, charBuffer, false);
        System.out.print(charBuffer.flip().toString());

        byteBuffer.compact(); // 數(shù)據(jù)可能有剩余
    }

    channel.close();復制代碼

這個例子中使用了兩個Buffer莉炉,其中 byteBuffer 作為通道讀寫的數(shù)據(jù)緩沖區(qū)钓账,charBuffer 用于存儲解碼后的字符。clear() 和 flip() 的用法正如上文所述絮宁,需要注意的是最后那個 compact() 方法梆暮,即使 charBuffer 的大小完全足以容納 byteBuffer 解碼后的數(shù)據(jù),這個 compact() 也必不可少绍昂,這是因為常用中文字符的UTF-8編碼占3個字節(jié)啦粹,因此有很大概率出現(xiàn)在中間截斷的情況,請看下圖:


3.jpg

當 Decoder 讀取到緩沖區(qū)末尾的 0xe4 時窘游,無法將其映射到一個 Unicode唠椭,decode()方法第三個參數(shù) false 的作用就是讓 Decoder 把無法映射的字節(jié)及其后面的數(shù)據(jù)都視作附加數(shù)據(jù),因此 decode() 方法會在此處停止忍饰,并且 position 會回退到 0xe4 的位置贪嫂。如此一來, 緩沖區(qū)中就遺留了“中”字編碼的第一個字節(jié)艾蓝,必須將其 compact 到前面力崇,以正確的和后序數(shù)據(jù)拼接起來。

BTW赢织,例子中的 CharsetDecoder 也是 Java NIO 的一個新特性亮靴,所以大家應該發(fā)現(xiàn)了一點哈,NIO的操作是面向緩沖區(qū)的(傳統(tǒng)I/O是面向流的)于置。

至此茧吊,我們了解了 Channel 與 Buffer 的基本用法。接下來要說的是讓一個線程管理多個Channel的重要組件八毯。

3.Selector

Selector 是什么

Selector(選擇器)是一個特殊的組件搓侄,用于采集各個通道的狀態(tài)(或者說事件)。我們先將通道注冊到選擇器宪彩,并設置好關心的事件休讳,然后就可以通過調(diào)用select()方法,靜靜地等待事件發(fā)生尿孔。

通道有如下4個事件可供我們監(jiān)聽:

  • Accept:有可以接受的連接
  • Connect:連接成功
  • Read:有數(shù)據(jù)可讀
  • Write:可以寫入數(shù)據(jù)了

為什么要用Selector

前文說了,如果用阻塞I/O,需要多線程(浪費內(nèi)存)活合,如果用非阻塞I/O雏婶,需要不斷重試(耗費CPU)。Selector的出現(xiàn)解決了這尷尬的問題白指,非阻塞模式下留晚,通過Selector,我們的線程只為已就緒的通道工作告嘲,不用盲目的重試了错维。比如,當所有通道都沒有數(shù)據(jù)到達時橄唬,也就沒有Read事件發(fā)生赋焕,我們的線程會在select()方法處被掛起,從而讓出了CPU資源仰楚。

使用方法

如下所示隆判,創(chuàng)建一個Selector,并注冊一個Channel僧界。

注意:要將 Channel 注冊到 Selector侨嘀,首先需要將 Channel 設置為非阻塞模式,否則會拋異常捂襟。

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);復制代碼

register()方法的第二個參數(shù)名叫“interest set”咬腕,也就是你所關心的事件集合。如果你關心多個事件葬荷,用一個“按位或運算符”分隔涨共,比如

SelectionKey.OP_READ | SelectionKey.OP_WRITE復制代碼

這種寫法一點都不陌生,支持位運算的編程語言里都這么玩闯狱,用一個整型變量可以標識多種狀態(tài)煞赢,它是怎么做到的呢,其實很簡單哄孤,舉個例子照筑,首先預定義一些常量,它們的值(二進制)如下


4.jpg

可以發(fā)現(xiàn)瘦陈,它們值為1的位都是錯開的凝危,因此對它們進行按位或運算之后得出的值就沒有二義性,可以反推出是由哪些變量運算而來晨逝。怎么判斷呢蛾默,沒錯,就是“按位與”運算捉貌。比如支鸡,現(xiàn)在有一個狀態(tài)集合變量值為 0011冬念,我們只需要判斷 “0011 & OP_READ” 的值是 1 還是 0 就能確定集合是否包含 OP_READ 狀態(tài)。

然后牧挣,注意 register() 方法返回了一個SelectionKey的對象急前,這個對象包含了本次注冊的信息,我們也可以通過它修改注冊信息瀑构。從下面完整的例子中可以看到裆针,select()之后,我們也是通過獲取一個 SelectionKey 的集合來獲取到那些狀態(tài)就緒了的通道寺晌。

一個完整實例

概念和理論的東西闡述完了(其實寫到這里世吨,我發(fā)現(xiàn)沒寫出多少東西,好尷尬(⊙?⊙))呻征,看一個完整的例子吧耘婚。

這個例子使用Java NIO實現(xiàn)了一個單線程的服務端,功能很簡單怕犁,監(jiān)聽客戶端連接边篮,當連接建立后,讀取客戶端的消息奏甫,并向客戶端響應一條消息戈轿。

需要注意的是,我用字符 ‘\0′(一個值為0的字節(jié)) 來標識消息結(jié)束阵子。

單線程Server

public class NioServer {

    public static void main(String[] args) throws IOException {
        // 創(chuàng)建一個selector
        Selector selector = Selector.open();

        // 初始化TCP連接監(jiān)聽通道
        ServerSocketChannel listenChannel = ServerSocketChannel.open();
        listenChannel.bind(new InetSocketAddress(9999));
        listenChannel.configureBlocking(false);
        // 注冊到selector(監(jiān)聽其ACCEPT事件)
        listenChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 創(chuàng)建一個緩沖區(qū)
        ByteBuffer buffer = ByteBuffer.allocate(100);

        while (true) {
            selector.select(); //阻塞思杯,直到有監(jiān)聽的事件發(fā)生
            Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();

            // 通過迭代器依次訪問select出來的Channel事件
            while (keyIter.hasNext()) {
                SelectionKey key = keyIter.next();

                if (key.isAcceptable()) { // 有連接可以接受
                    SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
                    channel.configureBlocking(false);
                    channel.register(selector, SelectionKey.OP_READ);

                    System.out.println("與【" + channel.getRemoteAddress() + "】建立了連接!");

                } else if (key.isReadable()) { // 有數(shù)據(jù)可以讀取
                    buffer.clear();

                    // 讀取到流末尾說明TCP連接已斷開挠进,
                    // 因此需要關閉通道或者取消監(jiān)聽READ事件
                    // 否則會無限循環(huán)
                    if (((SocketChannel) key.channel()).read(buffer) == -1) {
                        key.channel().close();
                        continue;
                    } 

                    // 按字節(jié)遍歷數(shù)據(jù)
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        byte b = buffer.get();

                        if (b == 0) { // 客戶端消息末尾的\0
                            System.out.println();

                            // 響應客戶端
                            buffer.clear();
                            buffer.put("Hello, Client!\0".getBytes());
                            buffer.flip();
                            while (buffer.hasRemaining()) {
                                ((SocketChannel) key.channel()).write(buffer);
                            }
                        } else {
                            System.out.print((char) b);
                        }
                    }
                }

                // 已經(jīng)處理的事件一定要手動移除
                keyIter.remove();
            }
        }
    }
}復制代碼

Client

這個客戶端純粹測試用色乾,為了看起來不那么費勁,就用傳統(tǒng)的寫法了领突,代碼很簡短暖璧。

要嚴謹一點測試的話,應該并發(fā)運行大量Client君旦,統(tǒng)計服務端的響應時間澎办,而且連接建立后不要立刻發(fā)送數(shù)據(jù),這樣才能發(fā)揮出服務端非阻塞I/O的優(yōu)勢金砍。

public class Client {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("localhost", 9999);
        InputStream is = socket.getInputStream();
        OutputStream os = socket.getOutputStream();

        // 先向服務端發(fā)送數(shù)據(jù)
        os.write("Hello, Server!\0".getBytes());

        // 讀取服務端發(fā)來的數(shù)據(jù)
        int b;
        while ((b = is.read()) != 0) {
            System.out.print((char) b);
        }
        System.out.println();

        socket.close();
    }
}復制代碼

NIO vs IO

學習了NIO之后我們都會有這樣一個疑問:到底什么時候該用NIO局蚀,什么時候該用傳統(tǒng)的I/O呢?

其實了解他們的特性后恕稠,答案還是比較明確的琅绅,NIO擅長1個線程管理多條連接,節(jié)約系統(tǒng)資源鹅巍,但是如果每條連接要傳輸?shù)臄?shù)據(jù)量很大的話千扶,因為是同步I/O料祠,會導致整體的響應速度很慢;而傳統(tǒng)I/O為每一條連接創(chuàng)建一個線程县貌,能充分利用處理器并行處理的能力术陶,但是如果連接數(shù)量太多凑懂,內(nèi)存資源會很緊張煤痕。

總結(jié)就是:連接數(shù)多數(shù)據(jù)量小用NIO,連接數(shù)少用I/O(寫起來也簡單- -)接谨。

Next

經(jīng)過NIO核心組件的學習摆碉,了解了非阻塞服務端實現(xiàn)的基本方法。然而脓豪,細心的你們肯定也發(fā)現(xiàn)了巷帝,上面那個完整的例子奋隶,實際上就隱藏了很多問題汪榔。比如,例子中只是簡單的將讀取到的每個字節(jié)輸出庶诡,實際環(huán)境中肯定是要讀取到完整的消息后才能進行下一步處理笤闯,由于NIO的非阻塞特性堕阔,一次可能只讀取到消息的一部分,這已經(jīng)很糟糕了颗味,如果同一條連接會連續(xù)發(fā)來多條消息超陆,那不僅要對消息進行拼接,還需要切割浦马,同理时呀,例子中給客戶端響應的時候,用了個while()循環(huán)晶默,保證數(shù)據(jù)全部write完成再做其它工作谨娜,實際應用中為了性能,肯定不會這么寫磺陡。另外趴梢,為了充分利用現(xiàn)代處理器多核心并行處理的能力,應該用一個線程組來管理這些連接的事件仅政。

要解決這些問題垢油,需要一個嚴謹而繁瑣的設計,不過幸運的是圆丹,我們有開源的框架可用滩愁,那就是優(yōu)雅而強大的Netty,Netty基于Java NIO辫封,提供異步調(diào)用接口硝枉,開發(fā)高性能服務器的一個很好的選擇廉丽,之前在項目中使用過,但沒有深入學習妻味,打算下一步好好學學它正压,到時候再寫一篇筆記。

Java NIO設計的目標是為程序員提供API以享受現(xiàn)代操作系統(tǒng)最新的I/O機制责球,所以覆蓋面較廣焦履,除了文中所涉及的組件與特性,還有很多其它的雏逾,比如 Pipe(管道)嘉裤、Path(路徑)、Files(文件) 等栖博,有的是用于提升I/O性能的新組件屑宠,有的是簡化I/O操作的工具,具體用法可以參看最后 References 里的鏈接仇让。

對Java架構(gòu)技術感興趣的同學典奉,歡迎加QQ群619881427,一起學習丧叽,相互討論卫玖。

對Java架構(gòu)技術感興趣的同學,歡迎加QQ群619881427蠢正,一起學習骇笔,相互討論。

群內(nèi)已經(jīng)有小伙伴將知識體系整理好(源碼嚣崭,筆記笨触,PPT,學習視頻)雹舀,歡迎加群免費領取芦劣。

分享給喜歡Java的,喜歡編程说榆,有夢想成為架構(gòu)師的程序員們虚吟,希望能夠幫助到你們。

不是Java的程序員也沒關系签财,幫忙轉(zhuǎn)發(fā)給更多朋友串慰!謝謝。

分享一個小技巧點擊閱讀原文也唱蒸“铞辏可以輕松獲取學習資料哦!

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市庆捺,隨后出現(xiàn)的幾起案子古今,更是在濱河造成了極大的恐慌,老刑警劉巖滔以,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捉腥,死亡現(xiàn)場離奇詭異,居然都是意外死亡你画,警方通過查閱死者的電腦和手機抵碟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撬即,“玉大人立磁,你說我怎么就攤上這事“保” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵宪摧,是天一觀的道長粒竖。 經(jīng)常有香客問我,道長几于,這世上最難降的妖魔是什么蕊苗? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮沿彭,結(jié)果婚禮上朽砰,老公的妹妹穿的比我還像新娘。我一直安慰自己喉刘,他們只是感情好瞧柔,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著睦裳,像睡著了一般造锅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上廉邑,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天哥蔚,我揣著相機與錄音,去河邊找鬼蛛蒙。 笑死糙箍,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的牵祟。 我是一名探鬼主播深夯,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼课舍!你這毒婦竟也來了塌西?” 一聲冷哼從身側(cè)響起他挎,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎捡需,沒想到半個月后办桨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡站辉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年呢撞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饰剥。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡殊霞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出汰蓉,到底是詐尸還是另有隱情绷蹲,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布顾孽,位于F島的核電站祝钢,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏若厚。R本人自食惡果不足惜拦英,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望测秸。 院中可真熱鬧疤估,春花似錦、人聲如沸霎冯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肃晚。三九已至锚贱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間关串,已是汗流浹背拧廊。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留晋修,地道東北人吧碾。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像墓卦,于是被迫代替她去往敵國和親倦春。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

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