你應(yīng)該知道的Node.js子進(jìn)程

文章翻譯自Node.js Child Processes: Everything you need to know

如何使用spawn函數(shù)、exec函數(shù)钥星、execFile函數(shù)和for函數(shù)

child-process.png

Node.js中的非阻塞單線程的特性對單進(jìn)程任務(wù)是非常有用沾瓦。但是事實上,面對日益復(fù)雜的業(yè)務(wù)邏輯谦炒,單個cpu中的單進(jìn)程所能提供的計算力顯然是不足的贯莺。因為無論服務(wù)器如何強大,單線程只可以利用有限的資源宁改。

事實上缕探,Node.js運行在單線程上,并不意味著開發(fā)者不能利用多進(jìn)程透且,當(dāng)然還有多臺服務(wù)器撕蔼。

使用多進(jìn)程是擴展Node.js程序最佳的方式。Node.js就是為在多個節(jié)點秽誊,創(chuàng)建分布式應(yīng)用而設(shè)計的鲸沮。這也是取名Node的原因」郏可伸縮性已經(jīng)滲透到平臺中讼溺,因此開發(fā)不能等到應(yīng)用程序運行到生命周期后期,在開始思考這個問題最易。

請注意怒坯,在閱讀本篇文章前你應(yīng)該理解Node.js事件和Node.js流的相關(guān)知識。如果你還沒準(zhǔn)備好藻懒,我推薦你閱讀下面兩篇文章:

Node.js事件驅(qū)動
你應(yīng)該知道的Node.js流

子進(jìn)程模塊

開發(fā)者通過Node的child_process模塊剔猿,可以很容易衍生出子進(jìn)程。這些子系統(tǒng)可以通過消息系統(tǒng)實現(xiàn)相互通信嬉荆。

開發(fā)者可以通過child_process模塊的內(nèi)部命令归敬,來訪問操作系統(tǒng)。

開發(fā)者可以控制子進(jìn)程的輸入流,監(jiān)聽其輸出流汪茧。開發(fā)者同樣可以控制輸入底層操作系統(tǒng)命令的參數(shù)椅亚、并且對命令的輸出做任何所需要的改動。由于命令的輸入與輸出數(shù)據(jù)都可以被Node.js流處理舱污,因此開發(fā)者可以將一個命令的輸出(就像linux命令那樣)作為另一個命令源數(shù)據(jù)呀舔。

注意本文中所有的例子都是基于linux系統(tǒng),如果你使用的系統(tǒng)時windows系統(tǒng)扩灯,你需要將對應(yīng)的linux命令換成windows命令媚赖。

在Node.js中有四種函數(shù)創(chuàng)建子進(jìn)程:spawn()、fork()驴剔、exec()和execFile()省古。

接下來粥庄,我們將會討論這四種函數(shù)間的不同函數(shù)的應(yīng)用場景丧失。

Spawn(衍生)子進(jìn)程

Spawan函數(shù)可以衍生出新的子進(jìn)程,并通過Spwan函數(shù)向子進(jìn)程傳遞命令惜互。例如布讹,通過衍生的子進(jìn)程,執(zhí)行"pwd"命令:

const { spawn } = require('child_process');
const child = spawn('pwd');

Node.js程序從child_process模塊析構(gòu)出spawn函數(shù)训堆,向函數(shù)傳遞OS命令描验,并在子進(jìn)程中執(zhí)行OS命令。

執(zhí)行spawn函數(shù)的結(jié)果是繼承事件接口的子進(jìn)程實例對象坑鱼,開發(fā)者可以對它直接注冊事件處理函數(shù)膘流。例如開發(fā)者對子進(jìn)程執(zhí)行結(jié)果和子進(jìn)程退出行為注冊事件:

child.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});
child.stderr.on('data', (data) => {
  console.log(`stderr: ${data}`);
});

child.on('exit', function (code, signal) {
    console.log('child process exited with ' +
                `code ${code} and signal ${signal}`);
  });

開發(fā)者對子進(jìn)程還可以注冊的處理事件有:disconnect、error和message鲁沥。

  • disconnect事件:當(dāng)父進(jìn)程調(diào)用child.disconnect函數(shù)時觸發(fā)
  • error事件:當(dāng)進(jìn)程不能衍生或者進(jìn)程被殺死時觸發(fā)
  • close事件:當(dāng)子進(jìn)程的stdio流關(guān)閉時觸發(fā)
  • message事件:當(dāng)子進(jìn)程使用process.send()函數(shù)時觸發(fā)呼股,這個函數(shù)主要用于父子進(jìn)程間的通信。

每個子進(jìn)程都具有標(biāo)準(zhǔn)的stdio流画恰,開發(fā)者可以通過child.stdin彭谁、child.stdout和child.stderr操作stdio流。

在子進(jìn)程中的stdio流關(guān)閉時允扇,子進(jìn)程會觸發(fā)close事件缠局。close事件并不完全等同于exist事件,主要在于子進(jìn)程可以共享相同的stdio流考润,當(dāng)一個子進(jìn)程并不會導(dǎo)致流關(guān)閉狭园。

由于流是事件的觸發(fā)者,開發(fā)者可以監(jiān)聽子進(jìn)程stdio流中的事件糊治。
與普通進(jìn)程不同唱矛,在子進(jìn)程中,stdout/stderr是可讀流、stdin是可寫流揖赴。從根本上講馆匿,這些流在子進(jìn)程與主進(jìn)程的屬性是相反的。最為重要的燥滑,通過監(jiān)聽data事件渐北,程序可以獲得命令的輸出或執(zhí)行命令時產(chǎn)生的異常信息。

child.stdout.on('data', (data) => {
  console.log(`child stdout:\n${data}`);
});

child.stderr.on('data', (data) => {
  console.error(`child stderr:\n${data}`);
});

當(dāng)程序執(zhí)行上面spawn函數(shù)铭拧,"pwd"命令的輸出將會打印出來赃蛛。子進(jìn)程將會退出,并返回0搀菩,這說明沒有異常發(fā)生呕臂。

除了可以向spawn函數(shù)衍生出的子進(jìn)程傳遞命令,開發(fā)者還可以向它傳遞命令的參數(shù)肪跋,這個參數(shù)的格式要求是數(shù)組歧蒋。例如下面的find命令:

const child = spawn('find', ['.', '-type', 'f']);

如果命令執(zhí)行的過程中出現(xiàn)異常,child.stderr的data事件被觸發(fā)該事件獲得程序退出code是1(意味著程序出現(xiàn)異常)州既,異常的信息通常是根據(jù)異常的類型和OS系統(tǒng)有所不同谜洽。

由于子進(jìn)程的stdin是可寫流,開發(fā)者可以通過它向子進(jìn)程寫入數(shù)據(jù)吴叶。就像其它的可寫流一樣震肮,pipe方法是使用可寫流最簡單的方式成榜,程序可以將可讀流寫入到可寫流中拂盯。由于主進(jìn)程的stdin是可讀流幽钢,因此可以實現(xiàn)主進(jìn)程向子進(jìn)程穿數(shù)據(jù)。例如:

const { spawn } = require('child_process');

const child = spawn('wc');

process.stdin.pipe(child.stdin)

child.stdout.on('data', (data) => {
  console.log(`child stdout:\n${data}`);
});

在上面的例子中逊彭,子進(jìn)程啟動wc命令來計算輸入數(shù)據(jù)的行數(shù)咸灿、字符數(shù)。然后將主進(jìn)程的stdin(可讀流)傳輸給子進(jìn)程的stdin(可寫流)中诫龙。執(zhí)行上面的程序后析显,命令行工具將會開啟輸入模式。當(dāng)輸入組合鍵Ctrl+D后签赃,終止輸入谷异。已經(jīng)輸入的數(shù)據(jù)將會作為wc命令的輸入數(shù)據(jù)源。

stream-pipe.gif

開發(fā)者將進(jìn)程的輸出作為另一個進(jìn)程的輸入數(shù)據(jù)源锦聊,實現(xiàn)像linux命令那樣的管道命令歹嘹。例如開發(fā)者將find命令的stdout流,做為wc命令的輸入數(shù)據(jù)源孔庭,實現(xiàn)計量文件夾中的文件數(shù)量:

const { spawn } = require('child_process');

const find = spawn('find', ['.', '-type', 'f']);
const wc = spawn('wc', ['-l']);

find.stdout.pipe(wc.stdin);

wc.stdout.on('data', (data) => {
  console.log(`Number of files ${data}`);
});

在wc命令后添加參數(shù)-l尺上,實現(xiàn)計算文件的行數(shù)材蛛。上面的程序?qū)Ξ?dāng)前項下所有目錄中所有文件進(jìn)行計數(shù)。

Shell語法和exec函數(shù)

默認(rèn)情況下怎抛,spawn函數(shù)并不會衍生新的shell卑吭,執(zhí)行通過參數(shù)傳遞進(jìn)來的命令。由于不會創(chuàng)建新的shell马绝,這是spawn函數(shù)比exec函數(shù)高效的主要原因豆赏。exec函數(shù)與spawn函數(shù)還有一點主要的區(qū)別,spawn函數(shù)通過流操作命令執(zhí)行的結(jié)果富稻,而exec函數(shù)則將程序執(zhí)行的結(jié)果緩存起來掷邦,最后將緩存的結(jié)果傳給回調(diào)函數(shù)中。

下面通過exec函數(shù)實現(xiàn)find|wx命令的例子:

const { exec } = require('child_process');

exec('find . -type f | wc -l', (err, stdout, stderr) => {
  if (err) {
    console.error(`exec error: ${err}`);
    return;
  }

  console.log(`Number of files ${stdout}`);
});

因為exec函數(shù)使用shell執(zhí)行命令椭赋,因此開發(fā)者可以直接通過shell句法使用shell管道的特性抚岗。

值得注意,要確保向exec函數(shù)傳遞的OS命令是沒有安全隱患的哪怔。因為用戶只要輸入一些特定的命令就可以實現(xiàn)命令的注入攻擊宣蔚,如:rm -rf ~~

exec函數(shù)緩存命令的輸出蔓涧,并將輸出的結(jié)果作為回調(diào)函數(shù)的參數(shù)件已,傳遞給回調(diào)函數(shù)笋额。

如果你需要使用shell句法元暴,并且期望命令操作的文件比較小,使用shell句法是一項不錯的選擇兄猩。注意茉盏,exec函數(shù)先將所要返回的數(shù)據(jù)緩存在內(nèi)存中,然后返回枢冤。

如果執(zhí)行命令后得到的數(shù)據(jù)太大鸠姨,spawn函數(shù)將是很不錯的選擇,因為使用spawn函數(shù)會標(biāo)準(zhǔn)的IO對象轉(zhuǎn)換為流淹真。

程序可以通過spawn函數(shù)衍生出繼承父進(jìn)程標(biāo)準(zhǔn)I/O對象的子進(jìn)程讶迁,如果需要,可以在子進(jìn)程中使用shell句法核蘸。下面的代碼就是實現(xiàn)定制子進(jìn)程的代碼:

const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true
});

設(shè)置stdion: 'inherit'巍糯,當(dāng)執(zhí)行代碼時,子進(jìn)程將會繼承主進(jìn)程的stdin客扎、stdout和stderr祟峦。主進(jìn)程的process.stdout 流將會觸發(fā)子進(jìn)程的事件處理函數(shù),并在事件處理函數(shù)中立刻輸出結(jié)果徙鱼。

設(shè)置shell: true宅楞,就像exec函數(shù)一樣针姿,程序可以向衍生函數(shù)傳遞shell句法,作為衍生函數(shù)的參數(shù)厌衙。即便這樣距淫,依舊可以利用衍生函數(shù)中流的特性。不得不說這樣是非成粝#酷

除了在spawn衍生函數(shù)的option對象中設(shè)置shell和stdio溉愁,開發(fā)者還有設(shè)置其它的選項。通過cwd屬性設(shè)置程序工作的目錄饲趋。例如下面將程序的工作目錄設(shè)置為下載文件夾拐揭,實現(xiàn)計算對目的文件夾中所有文件計數(shù)的代碼:

const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true,
  cwd: '/Users/samer/Downloads'
});

使用option對象env屬性,可以設(shè)置對子進(jìn)程可見的環(huán)境變量奕塑。process.env是env屬性的默認(rèn)值,提供對當(dāng)前進(jìn)程環(huán)境的任何命令訪問權(quán)限堂污。開發(fā)者可以設(shè)置env屬性為空對象或子進(jìn)程可見的環(huán)境變量值,實現(xiàn)定制子進(jìn)程可見環(huán)境變量龄砰。

const child = spawn('echo $ANSWER', {
  stdio: 'inherit',
  shell: true,
  env: { ANSWER: 42 },
});

上面的echo命令并不能訪問父進(jìn)程的環(huán)境變量盟猖。由于設(shè)置env屬性值,進(jìn)程沒有訪問$HONE的權(quán)限但是可以訪問$ANSWER换棚。

通過設(shè)置spawn函數(shù)中option對象的detached屬性式镐,可以實現(xiàn)子進(jìn)程完全獨立于父進(jìn)程的調(diào)用。

假設(shè)我們有一個讓事件循環(huán)繁忙的timer.js測試程序:

setTimeout(() => {  
  // keep the event loop busy
}, 20000);

程序設(shè)置spawn函數(shù)中option對象的detached屬性固蚤,實現(xiàn)在后臺執(zhí)行timer.js程序:

const { spawn } = require('child_process');

const child = spawn('node', ['timer.js'], {
  detached: true,
  stdio: 'ignore'
});

child.unref();

獨立子進(jìn)程運行在不同的系統(tǒng)娘汞,有不同的行為。在Windows環(huán)境下夕玩,獨立的子進(jìn)程有獨立的控制臺窗口你弦。在Linux環(huán)境下,獨立的子進(jìn)程將會成為新的進(jìn)程組或會話的領(lǐng)導(dǎo)者燎孟。

在獨立的子進(jìn)程中調(diào)用unref函數(shù)禽作,父進(jìn)程可以可以獨立于子進(jìn)程終止運行。這一特性對于下面的場景很適用:子進(jìn)程需要在后臺運行很長時間揩页、子進(jìn)程的stdio流也要獨立于父進(jìn)程旷偿。

上面的示例代碼中,設(shè)置option對象的detached屬性為true 爆侣,獨立的子進(jìn)程在后臺執(zhí)行nodejs代碼(timer.js)萍程。設(shè)置option對象的option對象的stdio屬性為ignore,子進(jìn)程擁有獨立于主進(jìn)程的stdio流累提。這樣就可以實現(xiàn)在子進(jìn)程還是后臺執(zhí)行時尘喝,終止父進(jìn)程。

independent-stdio.gif

execFile函數(shù)

如果開發(fā)者不需要使用shell執(zhí)行文件斋陪,execFile函數(shù)是一個不錯的選擇朽褪。execFile函數(shù)與exec函數(shù)很像置吓,但是由于execFile并不會衍生新的shell,這是execFile函數(shù)比exec函數(shù)高效的主要原因缔赠。在Windows環(huán)境下衍锚,諸如.bat和.cmd文件并不能獨立執(zhí)行。但是可以通過exec函數(shù)或是設(shè)置spawn函數(shù)的shell特性執(zhí)行這些文件嗤堰。

*Sync函數(shù)

子進(jìn)程模塊中的spawn函數(shù)戴质,exec函數(shù)和execFile函數(shù)同樣有相應(yīng)同步、阻塞函數(shù)踢匣。它們將會等待子進(jìn)程執(zhí)行完畢后退出告匠。

const { 
  spawnSync, 
  execSync, 
  execFileSync,
} = require('child_process');

這些同步的函數(shù)對于簡化所要執(zhí)行的腳本或處理程序啟動的任務(wù)都非常有用,但是在其它方面要避免使用它們离唬。

fork函數(shù)

fork函數(shù)和spawn函數(shù)在衍生子進(jìn)程時并不相同后专。它們的區(qū)別主要在于:通過fork函數(shù)衍生的子進(jìn)程會建立通信管道,衍生的子進(jìn)程可以通過send函數(shù)向主進(jìn)程發(fā)送信息输莺,主進(jìn)程也可以通過send函數(shù)向子進(jìn)程發(fā)送信息戚哎。下面是示例代碼:

父進(jìn)程代碼:

const { fork } = require('child_process');

const forked = fork('child.js');

forked.on('message', (msg) => {
  console.log('Message from child', msg);
});

forked.send({ hello: 'world' });

子進(jìn)程代碼:

process.on('message', (msg) => {
  console.log('Message from parent:', msg);
});

let counter = 0;

setInterval(() => {
  process.send({ counter: counter++ });
}, 1000);

在父進(jìn)程的程序中,開發(fā)者可以fork文件(這個文件將會通過node命令執(zhí)行)嫂用,然后監(jiān)聽message事件型凳。當(dāng)子進(jìn)程調(diào)用process.send函數(shù)的時,父進(jìn)程的message事件將會被觸發(fā)嘱函。在上面的代碼中甘畅,子進(jìn)程每分鐘都會調(diào)用一次process.send函數(shù)。

當(dāng)從父進(jìn)程向子進(jìn)程傳遞數(shù)據(jù)時实夹,在父進(jìn)程中調(diào)用send函數(shù)后橄浓,子進(jìn)程的message監(jiān)聽事件將會被觸發(fā),從而獲取到父進(jìn)程傳遞的消息亮航。

當(dāng)執(zhí)行上面的父進(jìn)程后,父進(jìn)程將會向子進(jìn)程傳遞對象{hello: 'world'}匀们,然后子進(jìn)程將會把這些父進(jìn)程傳遞的消息打印出來缴淋。同時子進(jìn)程將每隔一分鐘向父進(jìn)程發(fā)送一個遞增的數(shù)字,這些數(shù)字將會在父進(jìn)程控制窗口打印出來泄朴。

fork.gif

讓我們看一個關(guān)于fork更實用的例子:

開發(fā)者在http服務(wù)上開啟兩個api重抖。其中之一是"/compute",在這個api上將會做大量的計算祖灰,計算過程將會占用很長時間钟沛。我們可以用一個for循環(huán)模擬上面的場景:

const http = require('http');
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const sum = longComputation();
    return res.end(`Sum is ${sum}`);
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

上面的程序存在一個問題:當(dāng)http服務(wù)"/compute"被請求時,由于for循環(huán)阻塞了http服務(wù)的進(jìn)程局扶,因此http服務(wù)將不能再處理其它api請求恨统。

由于請求的程序需要長期運行叁扫,因此我們可以設(shè)計出很多優(yōu)化代碼性能的方案。其中之一是通過fork函數(shù)衍生出新的子進(jìn)程畜埋,然后將計算的代碼放在子進(jìn)程中運行莫绣,運行結(jié)束后將結(jié)果傳輸給父進(jìn)程。

首先將longComputation函數(shù)封裝在一個獨立的js文件中悠鞍,通過父進(jìn)程的信息指令來執(zhí)行l(wèi)ongComputation函數(shù):

const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e9; i++) {
    sum += i;
  };
  return sum;
};

process.on('message', (msg) => {
  const sum = longComputation();
  process.send(sum);
});

不需要在主進(jìn)程中做longComputation函數(shù)中的運算对室,而是通過fork函數(shù)衍生出新的子進(jìn)程,然后在子進(jìn)程中計算咖祭,最后通過fork函數(shù)的信息傳遞管道將運算結(jié)果傳回父進(jìn)程中掩宜。

const http = require('http');
const { fork } = require('child_process');

const server = http.createServer();

server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const compute = fork('compute.js');
    compute.send('start');
    compute.on('message', sum => {
      res.end(`Sum is ${sum}`);
    });
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

當(dāng)請求'/compute'時,子進(jìn)程通過process.send函數(shù)將計算的結(jié)果傳回給父進(jìn)程么翰,這樣主進(jìn)程的事件循環(huán)將不再發(fā)生阻塞锭亏。

然而上面代碼的性能受限于程序可以通過fork函數(shù)衍生的進(jìn)程數(shù)量。但是當(dāng)通過http請求時硬鞍,主進(jìn)程并不會阻塞慧瘤。

如果服務(wù)是通過多個fork函數(shù)衍生的子進(jìn)程,Node.js的cluster模塊將會對來自外部的請求固该,做http請求的負(fù)載均衡處理锅减。這就會是我下個主題所要講述的內(nèi)容。

以上就是我關(guān)于這個主題所有的內(nèi)容伐坏,非常感謝你的閱讀怔匣,期待下次再見。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末桦沉,一起剝皮案震驚了整個濱河市每瞒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纯露,老刑警劉巖剿骨,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異埠褪,居然都是意外死亡浓利,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門钞速,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贷掖,“玉大人,你說我怎么就攤上這事渴语∑煌” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵驾凶,是天一觀的道長牙甫。 經(jīng)常有香客問我掷酗,道長,這世上最難降的妖魔是什么腹暖? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任汇在,我火速辦了婚禮,結(jié)果婚禮上脏答,老公的妹妹穿的比我還像新娘糕殉。我一直安慰自己,他們只是感情好殖告,可當(dāng)我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布阿蝶。 她就那樣靜靜地躺著,像睡著了一般黄绩。 火紅的嫁衣襯著肌膚如雪羡洁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天爽丹,我揣著相機與錄音筑煮,去河邊找鬼。 笑死粤蝎,一個胖子當(dāng)著我的面吹牛真仲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播初澎,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼秸应,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了碑宴?” 一聲冷哼從身側(cè)響起软啼,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎延柠,沒想到半個月后祸挪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡捕仔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年匕积,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榜跌。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖盅粪,靈堂內(nèi)的尸體忽然破棺而出钓葫,到底是詐尸還是另有隱情,我是刑警寧澤票顾,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布础浮,位于F島的核電站帆调,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏豆同。R本人自食惡果不足惜番刊,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望影锈。 院中可真熱鬧芹务,春花似錦、人聲如沸鸭廷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辆床。三九已至佳晶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間讼载,已是汗流浹背轿秧。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留咨堤,地道東北人菇篡。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像吱型,于是被迫代替她去往敵國和親逸贾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,843評論 2 354

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