關(guān)于Linux的零拷貝技術(shù)詳解

已同步更新到微信公眾號(hào)欺缘,手機(jī)閱讀更舒適~

零拷貝機(jī)制原理分析之前,我們先來看下傳統(tǒng)IO在數(shù)據(jù)拷貝的基本原理,從數(shù)據(jù)拷貝(I/O拷貝)的次數(shù)以及上下文切換的次數(shù)進(jìn)行對(duì)比分析。

傳統(tǒng)IO:

image.png

1氏仗、JVM進(jìn)程內(nèi)發(fā)起read()系統(tǒng)調(diào)用,操作系統(tǒng)由用戶態(tài)空間切換到內(nèi)核態(tài)空間(第一次上下文切換)
2夺鲜、通過DMA引擎建數(shù)據(jù)從磁盤拷貝到內(nèi)核態(tài)空間的輸入的socket緩沖區(qū)中(第一次拷貝)
3皆尔、將內(nèi)核態(tài)空間緩沖區(qū)的數(shù)據(jù)原封不動(dòng)的拷貝到用戶態(tài)空間的緩存區(qū)中(第二次拷貝),同時(shí)內(nèi)核態(tài)空間切換到用戶態(tài)空間(第二次上下文切換)币励,read()系統(tǒng)調(diào)用結(jié)束
4慷蠕、JVM進(jìn)程內(nèi)業(yè)務(wù)邏輯代碼執(zhí)行
5、JVM進(jìn)程內(nèi)發(fā)起write()系統(tǒng)調(diào)用
6食呻、操作系統(tǒng)由用戶態(tài)空間切換到內(nèi)核態(tài)空間(第三次上下文切換)流炕,將用戶態(tài)空間的緩存區(qū)數(shù)據(jù)原封不動(dòng)的拷貝到內(nèi)核態(tài)空間輸出的socket緩存區(qū)中(第三次拷貝)
7澎现、write()系統(tǒng)調(diào)用返回,操作系統(tǒng)由內(nèi)核態(tài)空間切換到用戶態(tài)空間(第四次上下文切換)每辟,通過DMA引擎將數(shù)據(jù)從內(nèi)核態(tài)空間的socket緩存區(qū)數(shù)據(jù)拷貝到協(xié)議引擎中(第四次拷貝)

傳統(tǒng)IO方式剑辫,一共在用戶態(tài)空間與內(nèi)核態(tài)空間之間發(fā)生了4次上下文的切換,4次數(shù)據(jù)的拷貝過程影兽,其中包括2次DMA拷貝和2次I/O拷貝(內(nèi)核態(tài)與用戶應(yīng)用程序之間發(fā)生的拷貝)揭斧。
內(nèi)核空間緩沖區(qū)的一大用處是為了減少磁盤I/O操作,因?yàn)樗鼤?huì)從磁盤中預(yù)讀更多的數(shù)據(jù)到緩沖區(qū)中峻堰。而使用BufferedInputStream的用處是減少“系統(tǒng)調(diào)用”讹开。

DMA:
DMA(Direct Memory Access) —直接內(nèi)存訪問 :DMA是允許外設(shè)組件將I/O數(shù)據(jù)直接傳送到主存儲(chǔ)器中并且傳輸不需要CPU的參與,以此將CPU解放出來去完成其他的事情捐名。

sendfile數(shù)據(jù)零拷貝:
顯然旦万,在傳統(tǒng)IO中,用戶態(tài)空間與內(nèi)核態(tài)空間之間的復(fù)制是完全不必要的镶蹋,因?yàn)橛脩魬B(tài)空間僅僅起到了一種數(shù)據(jù)轉(zhuǎn)存媒介的作用成艘,除此之外沒有做任何事情。
Linux 提供了sendfile()用來減少我們前面提到的數(shù)據(jù)拷貝和的上下文切換次數(shù)

image.png

1贺归、發(fā)起sendfile()系統(tǒng)調(diào)用淆两,操作系統(tǒng)由用戶態(tài)空間切換到內(nèi)核態(tài)空間(第一次上下文切換)
2、通過DMA引擎建數(shù)據(jù)從磁盤拷貝到內(nèi)核態(tài)空間的輸入的socket緩沖區(qū)中(第一次拷貝)
3拂酣、將數(shù)據(jù)從內(nèi)核空間拷貝到與之關(guān)聯(lián)的socket緩沖區(qū)(第二次拷貝)
4秋冰、將socket緩沖區(qū)的數(shù)據(jù)拷貝到協(xié)議引擎中(第三次拷貝)
5、sendfile()系統(tǒng)調(diào)用結(jié)束婶熬,操作系統(tǒng)由用戶態(tài)空間切換到內(nèi)核態(tài)空間(第二次上下文切換)

根據(jù)以上過程剑勾,一共有2次的上下文切換,3次的I/O拷貝赵颅。我們看到從用戶空間到內(nèi)核空間并沒有出現(xiàn)數(shù)據(jù)拷貝虽另,從操作系統(tǒng)角度來看,這個(gè)就是零拷貝饺谬。內(nèi)核空間出現(xiàn)了復(fù)制的原因: 通常的硬件在通過DMA訪問時(shí)期望的是連續(xù)的內(nèi)存空間捂刺。

支持scatter-gather特性的sendfile數(shù)據(jù)零拷貝

image.png

這次相比sendfile()數(shù)據(jù)零拷貝,減少了一次從內(nèi)核空間到與之相關(guān)的socket緩沖區(qū)的數(shù)據(jù)拷貝商蕴。
1叠萍、發(fā)起sendfile()系統(tǒng)調(diào)用,操作系統(tǒng)由用戶態(tài)空間切換到內(nèi)核態(tài)空間(第一次上下文切換)
2绪商、通過DMA引擎建數(shù)據(jù)從磁盤拷貝到內(nèi)核態(tài)空間的輸入的socket緩沖區(qū)中(第一次拷貝)
3苛谷、將描述符信息會(huì)拷貝到相應(yīng)的socket緩沖區(qū)當(dāng)中,該描述符包含了兩方面的信息:a)kernel buffer的內(nèi)存地址格郁;b)kernel buffer的偏移量腹殿。
4独悴、DMA gather copy根據(jù)socket緩沖區(qū)中描述符提供的位置和偏移量信息直接將內(nèi)核空間緩沖區(qū)中的數(shù)據(jù)拷貝到協(xié)議引擎上(第二次拷貝),這樣就避免了最后一次I/O數(shù)據(jù)拷貝锣尉。
5刻炒、sendfile()系統(tǒng)調(diào)用結(jié)束,操作系統(tǒng)由用戶態(tài)空間切換到內(nèi)核態(tài)空間(第二次上下文切換)
下面這個(gè)圖更進(jìn)一步理解:
image.png

Linux/Unix操作系統(tǒng)下可以通過下面命令查看是否支持scatter-gather特性自沧。

# ethtool -k eth0 | grep scatter-gather
scatter-gather: on
       tx-scatter-gather: on
       tx-scatter-gather-fraglist: on

許多的web server都已經(jīng)支持了零拷貝技術(shù)坟奥,比如Apache、Tomcat拇厢。

sendfile零拷貝消除了所有內(nèi)核空間緩沖區(qū)與用戶空間緩沖區(qū)之間的數(shù)據(jù)拷貝過程爱谁,因此sendfile零拷貝I/O的實(shí)現(xiàn)是完成在內(nèi)核空間中完成的,這對(duì)于應(yīng)用程序來說就無法對(duì)數(shù)據(jù)進(jìn)行操作了孝偎。
如果需要對(duì)數(shù)據(jù)做操作访敌,Linux提供了mmap零拷貝來實(shí)現(xiàn)。
mmap零拷貝:

image.png

通過上圖看到衣盾,一共發(fā)生了4次的上下文切換寺旺,3次的I/O拷貝,包括2次DMA拷貝和1次的I/O拷貝势决,相比于傳統(tǒng)IO減少了一次I/O拷貝阻塑。使用mmap()讀取文件時(shí),只會(huì)發(fā)生第一次從磁盤數(shù)據(jù)拷貝到OS文件系統(tǒng)緩沖區(qū)的操作果复。

1)在什么場(chǎng)景下使用mmap()去訪問文件會(huì)更高效叮姑?

對(duì)文件執(zhí)行隨機(jī)訪問時(shí),如果使用read()或write()据悔,則意味著較低的 cache 命中率。這種情況下使用mmap()通常將更高效耘沼。
多個(gè)進(jìn)程同時(shí)訪問同一個(gè)文件時(shí)(無論是順序訪問還是隨機(jī)訪問)极颓,如果使用mmap(),那么操作系統(tǒng)緩沖區(qū)的文件內(nèi)容可以在多個(gè)進(jìn)程之間共享群嗤,從操作系統(tǒng)角度來看菠隆,使用mmap()可以大大節(jié)省內(nèi)存。

2)什么場(chǎng)景下沒有使用mmap()的必要狂秘?

訪問小文件時(shí)骇径,直接使用read()或write()將更加高效。
單個(gè)進(jìn)程對(duì)文件執(zhí)行順序訪問時(shí)(sequential access)者春,使用mmap()幾乎不會(huì)帶來性能上的提升破衔。譬如說,使用read()順序讀取文件時(shí)钱烟,文件系統(tǒng)會(huì)使用 read-ahead 的方式提前將文件內(nèi)容緩存到文件系統(tǒng)的緩沖區(qū)晰筛,因此使用read()將很大程度上可以命中緩存嫡丙。

下面我們通過代碼示例來對(duì)比下傳統(tǒng)IO與使用了零拷貝技術(shù)的NIO之間的差異。
我們通過服務(wù)端開啟socket監(jiān)聽读第,然后客戶端連接的服務(wù)端進(jìn)行數(shù)據(jù)的傳輸曙博,數(shù)據(jù)傳輸文件大小為237M。
1怜瞒、構(gòu)建傳統(tǒng)IO的socket服務(wù)端父泳,監(jiān)聽8898端口。

public class OldIOServer {

    public static void main(String[] args) throws Exception {
        try (ServerSocket serverSocket = new ServerSocket(8898)) {
            while (true) {
                Socket socket = serverSocket.accept();
                DataInputStream inputStream = new DataInputStream(socket.getInputStream());

                byte[] bytes = new byte[4096];
                // 從socket中讀取字節(jié)數(shù)據(jù)
                while (true) {
                    // 讀取的字節(jié)數(shù)大小吴汪,-1則表示數(shù)據(jù)已被讀完
                    int readCount = inputStream.read(bytes, 0, bytes.length);

                    if (-1 == readCount) {
                        break;
                    }
                }

            }
        }
    }
}

2惠窄、構(gòu)建傳統(tǒng)IO的客戶端,連接服務(wù)端的8898端口浇坐,并從磁盤讀取237M的數(shù)據(jù)文件向服務(wù)端socket中發(fā)起寫請(qǐng)求睬捶。

public class OldIOClient {

    public static void main(String[] args) throws Exception {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("localhost", 8898)); // 連接服務(wù)端socket 8899端口

        // 設(shè)置一個(gè)大的文件, 237M
        try (FileInputStream fileInputStream = new FileInputStream(new File("/Users/david/Downloads/jdk-8u144-macosx-x64.dmg"));
                // 定義一個(gè)輸出流
                DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());) {

            // 讀取文件數(shù)據(jù)
            // 定義byte緩存
            byte[] buffer = new byte[4096];
            int readCount; // 每一次讀取的字節(jié)數(shù)
            int total = 0; // 讀取的總字節(jié)數(shù)

            long startTime = System.currentTimeMillis();

            while ((readCount = fileInputStream.read(buffer)) > 0) {
                total += readCount; //累加字節(jié)數(shù)
                dataOutputStream.write(buffer); // 寫入到輸出流中
            }

            System.out.println("發(fā)送的總字節(jié)數(shù):" + total + ", 耗時(shí):" + (System.currentTimeMillis() - startTime));
        }
    }
}

運(yùn)行結(jié)果:發(fā)送的總字節(jié)數(shù):237607747, 耗時(shí):450 (400~600毫秒之間)
接下來,我們通過使用JDK提供的NIO的方式實(shí)現(xiàn)數(shù)據(jù)傳輸與上述傳統(tǒng)IO做對(duì)比近刘。
1擒贸、構(gòu)建基于NIO的服務(wù)端,監(jiān)聽8899端口觉渴。

public class NewIOServer {

    public static void main(String[] args) throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8899));

        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false); // 這里設(shè)置為阻塞模式
            int readCount = socketChannel.read(byteBuffer);

            while (-1 != readCount) {
                readCount = socketChannel.read(byteBuffer);

                // 這里一定要調(diào)用下rewind方法介劫,將position重置為0開始位置
                byteBuffer.rewind();
            }
        }
    }
}

2、構(gòu)建基于NIO的客戶端案淋,連接NIO的服務(wù)端8899端口座韵,通過FileChannel.transferTo傳輸237M的數(shù)據(jù)文件虱疏。

public class NewIOClient {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8899));
        socketChannel.configureBlocking(true);

        String fileName = "/Users/david/Downloads/jdk-8u144-macosx-x64.dmg";

        FileInputStream fileInputStream = new FileInputStream(fileName);
        FileChannel fileChannel = fileInputStream.getChannel();

        long startTime = System.currentTimeMillis();
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel); // 目標(biāo)channel
        System.out.println("發(fā)送的總字節(jié)數(shù):" + transferCount + ",耗時(shí):" + (System.currentTimeMillis() - startTime));
        fileChannel.close();
    }
}

運(yùn)行結(jié)果:發(fā)送的總字節(jié)數(shù):237607747,耗時(shí):161(100到300毫秒之間)
結(jié)合運(yùn)行結(jié)果藐唠,基于NIO零拷貝技術(shù)要比傳統(tǒng)IO傳輸效率高3倍多。所以舌缤,后續(xù)當(dāng)設(shè)計(jì)大文件數(shù)據(jù)傳輸時(shí)可以優(yōu)先采用類似NIO的方式實(shí)現(xiàn)瓣距。
這里我們使用了FileChannel黔帕,其中調(diào)用的transferTo()方法將數(shù)據(jù)從FileChannel傳輸?shù)狡渌腸hannel中,如果操作系統(tǒng)底層支持的話transferTo蹈丸、transferFrom會(huì)使用相關(guān)的零拷貝技術(shù)來實(shí)現(xiàn)數(shù)據(jù)的傳輸成黄。所以,這里是否使用零拷貝必須依賴于底層的系統(tǒng)實(shí)現(xiàn)逻杖。

FileChannel.transferTo方法

public abstract long transferTo(long position,
                                long count,
                                WritableByteChannel target) throws IOException

將字節(jié)從此通道的文件傳輸?shù)浇o定的可寫入字節(jié)通道奋岁。
試圖讀取從此通道的文件中給定 position 處開始的 count 個(gè)字節(jié),并將其寫入目標(biāo)通道荸百。此方法的調(diào)用不一定傳輸所有請(qǐng)求的字節(jié)闻伶;是否傳輸取決于通道的性質(zhì)和狀態(tài)。如果此通道的文件從給定的 position 處開始所包含的字節(jié)數(shù)小于 count 個(gè)字節(jié)管搪,或者如果目標(biāo)通道是非阻塞的并且其輸出緩沖區(qū)中的自由空間少于 count 個(gè)字節(jié)虾攻,則所傳輸?shù)淖止?jié)數(shù)要小于請(qǐng)求的字節(jié)數(shù)铡买。
此方法不修改此通道的位置。如果給定的位置大于該文件的當(dāng)前大小霎箍,則不傳輸任何字節(jié)奇钞。如果目標(biāo)通道中有該位置,則從該位置開始寫入各字節(jié)漂坏,然后將該位置增加寫入的字節(jié)數(shù)景埃。
與從此通道讀取并將內(nèi)容寫入目標(biāo)通道的簡單循環(huán)語句相比,此方法可能高效得多顶别。很多操作系統(tǒng)可將字節(jié)直接從文件系統(tǒng)緩存?zhèn)鬏數(shù)侥繕?biāo)通道谷徙,而無需實(shí)際復(fù)制各字節(jié)。
參數(shù):
position - 文件中的位置驯绎,從此位置開始傳輸完慧;必須為非負(fù)數(shù)
count - 要傳輸?shù)淖畲笞止?jié)數(shù);必須為非負(fù)數(shù)
target - 目標(biāo)通道
返回:
實(shí)際已傳輸?shù)淖止?jié)數(shù)剩失,可能為零

FileChannel.transferFrom方法

public abstract long transferFrom(ReadableByteChannel src,
                                  long position,
                                  long count) throws IOException

將字節(jié)從給定的可讀取字節(jié)通道傳輸?shù)酱送ǖ赖奈募小?br> 試著從源通道中最多讀取 count 個(gè)字節(jié)屈尼,并將其寫入到此通道的文件中從給定 position 處開始的位置。此方法的調(diào)用不一定傳輸所有請(qǐng)求的字節(jié)拴孤;是否傳輸取決于通道的性質(zhì)和狀態(tài)脾歧。如果源通道的剩余空間小于 count 個(gè)字節(jié),或者如果源通道是非阻塞的并且其輸入緩沖區(qū)中直接可用的空間小于 count 個(gè)字節(jié)演熟,則所傳輸?shù)淖止?jié)數(shù)要小于請(qǐng)求的字節(jié)數(shù)鞭执。
此方法不修改此通道的位置。如果給定的位置大于該文件的當(dāng)前大小芒粹,則不傳輸任何字節(jié)兄纺。如果該位置在源通道中,則從該位置開始讀取各字節(jié)化漆,然后將該位置增加讀取的字節(jié)數(shù)囤热。
與從源通道讀取并將內(nèi)容寫入此通道的簡單循環(huán)語句相比,此方法可能高效得多获三。很多操作系統(tǒng)可將字節(jié)直接從源通道傳輸?shù)轿募到y(tǒng)緩存,而無需實(shí)際復(fù)制各字節(jié)锨苏。
參數(shù):
src - 源通道
position - 文件中的位置疙教,從此位置開始傳輸;必須為非負(fù)數(shù)
count - 要傳輸?shù)淖畲笞止?jié)數(shù)伞租;必須為非負(fù)數(shù)
返回:
實(shí)際已傳輸?shù)淖止?jié)數(shù)贞谓,可能為零

發(fā)生相應(yīng)的異常的情況:

異常拋出:
IllegalArgumentException - 如果關(guān)于參數(shù)的前提不成立
NonReadableChannelException - 如果不允許從此通道進(jìn)行讀取操作
NonWritableChannelException - 如果目標(biāo)通道不允許進(jìn)行寫入操作
ClosedChannelException - 如果此通道或目標(biāo)通道已關(guān)閉
AsynchronousCloseException - 如果正在進(jìn)行傳輸時(shí)另一個(gè)線程關(guān)閉了任一通道
ClosedByInterruptException - 如果正在進(jìn)行傳輸時(shí)另一個(gè)線程中斷了當(dāng)前線程,因此關(guān)閉了兩個(gè)通道并將當(dāng)前線程設(shè)置為中斷
IOException - 如果發(fā)生其他 I/O 錯(cuò)誤

參考資料:
http://xcorpion.tech/2016/09/10/It-s-all-about-buffers-zero-copy-mmap-and-Java-NIO/
http://www.reibang.com/p/e76e3580e356
http://www.linuxjournal.com/node/6345
http://senlinzhan.github.io/2017/03/25/%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B%E4%B8%AD%E7%9A%84zerocpoy%E6%8A%80%E6%9C%AF/
jdk官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末葵诈,一起剝皮案震驚了整個(gè)濱河市裸弦,隨后出現(xiàn)的幾起案子祟同,更是在濱河造成了極大的恐慌,老刑警劉巖理疙,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晕城,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡窖贤,警方通過查閱死者的電腦和手機(jī)砖顷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赃梧,“玉大人滤蝠,你說我怎么就攤上這事∈卩郑” “怎么了物咳?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蹄皱。 經(jīng)常有香客問我览闰,道長,這世上最難降的妖魔是什么夯接? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任焕济,我火速辦了婚禮,結(jié)果婚禮上盔几,老公的妹妹穿的比我還像新娘晴弃。我一直安慰自己,他們只是感情好逊拍,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布上鞠。 她就那樣靜靜地躺著,像睡著了一般芯丧。 火紅的嫁衣襯著肌膚如雪芍阎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天缨恒,我揣著相機(jī)與錄音谴咸,去河邊找鬼。 笑死骗露,一個(gè)胖子當(dāng)著我的面吹牛岭佳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播萧锉,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼珊随,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起叶洞,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤鲫凶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后衩辟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體螟炫,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年惭婿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了不恭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡财饥,死狀恐怖换吧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钥星,我是刑警寧澤沾瓦,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站谦炒,受9級(jí)特大地震影響贯莺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宁改,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一缕探、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧还蹲,春花似錦爹耗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至斗遏,卻和暖如春山卦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诵次。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國打工账蓉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逾一。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓剔猿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嬉荆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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