NodeJS服務(wù)器篇之簡(jiǎn)單靜態(tài)文件合并

NodeJS是一個(gè)基于Chrome V8引擎的JavaScript運(yùn)行環(huán)境,其使用了事件驅(qū)動(dòng)灿椅、異步I/O機(jī)制,具有運(yùn)行速度快钞支,性能優(yōu)異等特點(diǎn)茫蛹,非常適合在分布式設(shè)備上運(yùn)行數(shù)據(jù)密集型的實(shí)時(shí)應(yīng)用。

本文主要介紹一下通過搭建簡(jiǎn)單的NodeJS服務(wù)器烁挟,實(shí)現(xiàn)靜態(tài)文件的合并婴洼,并通過瀏覽器訪問輸出的功能;同時(shí)撼嗓,還會(huì)進(jìn)行功能的完善柬采,通過不斷的迭代開發(fā),從易用性且警、性能粉捻、安全性等等方面,較為全面的介紹一下NodeJS服務(wù)器的開發(fā)過程斑芜,為以后的進(jìn)一步學(xué)習(xí)做準(zhǔn)備肩刃。

在下面的內(nèi)容開始之前,假定您對(duì)JavaScript已經(jīng)有了一定的了解,如果您之前沒有了解過盈包,請(qǐng)先熟悉一下七天學(xué)會(huì)NodeJS沸呐,本文主要參考上述資料的最后一部分,為作者的開源奉獻(xiàn)精神表示感謝续语。下面正式開始介紹服務(wù)器的具體實(shí)現(xiàn):

需求

實(shí)現(xiàn)一個(gè)靜態(tài)文件合并的服務(wù)器垂谢,通過請(qǐng)求的鏈接(URL)指定需要合并的文件,之后把文件內(nèi)容返回給客戶端疮茄。參考鏈接如下:

http://127.0.0.1:8300/??a.js,b.js

分析

鏈接中的??是一個(gè)分隔符滥朱,前面是需要合并的文件路徑,后面是需要合并的文件名力试,多個(gè)文件名之間用,分隔徙邻,因此服務(wù)器處理這個(gè)URL后返回的是各個(gè)文件的路徑;之后畸裳,通過遞歸讀取文件內(nèi)容缰犁,再進(jìn)行拼接合并;最后怖糊,通過響應(yīng)數(shù)據(jù)輸出給客戶端帅容。這是整個(gè)服務(wù)器的全部分析過程。

由于涉及到文件操作伍伤,所以需要fs模塊并徘、path模塊;加上服務(wù)器模塊http扰魂,一共需要三個(gè)模塊:fs麦乞、path、http劝评。

第一版

源碼如下:

var fs = require('fs'),
    path = require('path'),
    http = require('http');

var MIME = {
    '.css': 'text/css',
    '.js': 'application/javascript'
};

// 合并文件內(nèi)容
function combineFiles(pathnames, callback) {
    var output = [];

    (function next(i, len) {
        if (i < len) {
            fs.readFile(pathnames[i], function (err, data) {
                if (err) {
                    callback(err);
                } else {
                    output.push(data);
                    next(i + 1, len);
                }
            });
        } else {
            const data = Buffer.concat(output);
            console.log(data);
            
            callback(null, data);
        }
    }(0, pathnames.length));
}

function main(argv) {
    // 從文件讀取配置參數(shù)
    // var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
    //     root = config.root || '.',
    //     port = config.port || 80;

    // 直接給定配置參數(shù)
    var root = __dirname;
    var port = 8300;

    http.createServer(function (request, response) {
         var urlInfo = parseURL(root, request.url);

         console.log(urlInfo);

         combineFiles(urlInfo.pathnames, function (err, data) {
             if (err) {
                 response.writeHead(404);
                 response.end(err.message);
             } else {
                 response.writeHead(200, {
                     'Content-Type': urlInfo.mime
                 });

                 response.end(data);
             }
         });
    }).listen(port);
}

// 解析文件路徑
function parseURL (root, url) {
    var base, pathnames, parts;

    if (url.indexOf('??') === -1) {
        url = url.replace('/', '/??');
    }

    parts = url.split('??');
    base = parts[0];
    pathnames = parts[1].split(',').map(function(value) {
        var filePath = path.join(root, base, value);
        return filePath;
    });

    return {
        mime: MIME[path.extname(pathnames[0])] || 'text/plain',
        pathnames: pathnames
    };
}

main(process.argv.slice(2));

/*
測(cè)試URL: 127.0.0.1:8300/??a.js,b.js
輸出:
    hello
    kelvin
    world
 */

以上代碼完整實(shí)現(xiàn)了服務(wù)器的功能姐直,可以用測(cè)試URL請(qǐng)求,就會(huì)輸出其后的內(nèi)容蒋畜。其中声畏,有幾點(diǎn)需要注意:

  • 命令行參數(shù)可以通過讀取JSON配置文件,或者直接在main函數(shù)內(nèi)設(shè)定(缺點(diǎn)是修改不方便姻成,配置不靈活)
  • 入口main函數(shù)開啟了http服務(wù)器插龄;combineFiles函數(shù)負(fù)責(zé)異步讀取文件內(nèi)容,并合并文件內(nèi)容佣渴;parseULR函數(shù)負(fù)責(zé)解析URL,并返回文件的MIME類型(在返回?cái)?shù)據(jù)給客戶端時(shí)初斑,指定數(shù)據(jù)的類型)和文件名數(shù)組辛润,。

服務(wù)器的工作流程如下:

發(fā)送請(qǐng)求       等待服務(wù)端響應(yīng)         接收響應(yīng)
---------+----------------------+------------->
         --                                        解析請(qǐng)求
           ------                                  讀取a.js
                 ------                            讀取b.js
                       ------                      讀取c.js
                             --                    合并數(shù)據(jù)
                               --                  輸出響應(yīng)

第二版

由于第一版中,代碼是把文件內(nèi)容全部讀取到內(nèi)存后砂竖,再進(jìn)行數(shù)據(jù)合并的真椿,這會(huì)導(dǎo)致如下問題:

  • 當(dāng)請(qǐng)求的文件較多,需要合并的數(shù)據(jù)量又比較大時(shí)乎澄,串行讀取文件會(huì)比較耗時(shí)突硝,拖慢服務(wù)的相應(yīng)時(shí)間
  • 每次都完整的把數(shù)據(jù)讀到內(nèi)存緩存起來,當(dāng)服務(wù)器并發(fā)數(shù)較大時(shí)置济,就會(huì)有較大的內(nèi)存開銷

針對(duì)上面的第一個(gè)問題解恰,如果改為并行讀取方式,對(duì)于機(jī)械磁盤來說浙于,需要不停的切換磁頭护盈,反而會(huì)降低I/O效率。而對(duì)于固態(tài)硬盤羞酗,是存在多個(gè)并行的I/O的腐宋,對(duì)單個(gè)請(qǐng)求采用并行也不會(huì)提高效率。因此檀轨,采用流式讀取方式:一遍讀取胸竞,一遍輸出,把相應(yīng)的輸出時(shí)機(jī)提前至讀取第一個(gè)文件的時(shí)刻参萄,這樣就能解決上述的問題卫枝。

修改后的服務(wù)器工作流程如下:

發(fā)送請(qǐng)求 等待服務(wù)端響應(yīng) 接收響應(yīng)
---------+----+------------------------------->
         --                                        解析請(qǐng)求
           --                                      檢查文件是否存在
             --                                    輸出響應(yīng)頭
               ------                              讀取和輸出a.js
                     ------                        讀取和輸出b.js
                           ------                  讀取和輸出c.js

可以看到,調(diào)整后的代碼是邊讀取邊輸出拧揽,即快速響應(yīng)請(qǐng)求剃盾,有減少了內(nèi)存的壓力。

源碼如下:

var fs = require('fs'),
    path = require('path'),
    http = require('http');

var MIME = {
    '.css': 'text/css',
    '.js': 'application/javascript'
};

function main(argv) {
    var root = __dirname;
    var port = 8300;

    http.createServer((request, response) => {
        var urlInfo = parseURL(root, request.url);
        
        validateFiles(urlInfo.pathnames, (err, pathnames) => {
            if (err) {
                response.writeHead(404);
                response.end(err.message);
            } else {
                response.writeHead(200, {
                    'Content-Type': urlInfo.mime
                });

                outputFiles(pathnames, response);
            }
        })
    }).listen(port);
}

function outputFiles(pathnames, writer) {
    (function next(i, len) {
        if (i <len) {
            var reader = fs.createReadStream(pathnames[i]);

            reader.pipe(writer, {end: false});
            reader.on('end', function() {
                next(i + 1, len);
            })
        } else {
            writer.end();
        }
    }(0, pathnames.length));
}

function validateFiles(pathnames, callback) {
    (function next(i, len) {
        if (i < len) {
            fs.stat(pathnames[i], (err, stats) => {
                if (err) {
                    callback(err);
                } else if (!stats.isFile()){
                    callback(new Error());
                } else {
                    next(i + 1, len);
                }
            });
        } else {
            callback(null, pathnames);
        }
    }(0, pathnames.length));
}

function parseURL (root, url) {
    var base, pathnames, parts;

    if (url.indexOf('??') === -1) {
        url = url.replace('/', '/??');
    }

    parts = url.split('??');
    base = parts[0];
    pathnames = parts[1].split(',').map(function(value) {
        var filePath = path.join(root, base, value);
        return filePath;
    });

    return {
        mime: MIME[path.extname(pathnames[0])] || 'text/plain',
        pathnames: pathnames
    };
}

main();

第三版

服務(wù)器的功能和性能已經(jīng)得到初步滿足淤袜,接下來我們要考慮穩(wěn)定性痒谴。由于沒有系統(tǒng)是絕對(duì)的穩(wěn)定,都存在一定的宕機(jī)風(fēng)險(xiǎn)铡羡,而這一問題不可避免积蔚,所以我們要盡量減少宕機(jī)的時(shí)間,比如增加一個(gè)守護(hù)進(jìn)程烦周,在服務(wù)器掛掉后立即重啟尽爆。并且NodeJS官方也建議在出現(xiàn)異常時(shí)重啟,因?yàn)檫@時(shí)系統(tǒng)處于一種不穩(wěn)定的狀態(tài)读慎。

所以漱贱,我們利用NodeJS的進(jìn)程管理機(jī)制,將守護(hù)進(jìn)程作為父進(jìn)程夭委,將服務(wù)器進(jìn)程作為子進(jìn)程幅狮,讓父進(jìn)程監(jiān)控子進(jìn)程的運(yùn)行狀態(tài),在其異常時(shí)立即退出重啟子進(jìn)程。

守護(hù)進(jìn)程代碼如下:

var cp = require('child_process');

var worker;

function spawn(server, config) {
    worker = cp.spawn('node', [server, config]);
    worker.on('exit', (code) => {
        console.log("code: " + code)
        if (code != 0) {
            console.log('自動(dòng)重啟');
            spawn(server, config);
        }
    });
}

function main(argv) {
    spawn('server2.js', argv[0]);

    process.on('SIGTERM', () => {
        worker.kill();
        process.exit(0);
    });
}

main(process.argv.slice(2));

服務(wù)器代碼也要在main函數(shù)里做如下調(diào)整:

function main(argv) {
    ...

    server = http.createServer((request, response) => {
        var urlInfo = parseURL(root, request.url);
        
        validateFiles(urlInfo.pathnames, (err, pathnames) => {
    ...
        })
    }).listen(port);

    process.on('SIGTERM', () => {
        server.close(() => {
            process.exit(0);
        });
    });
}

這樣調(diào)整后崇摄,守護(hù)進(jìn)程會(huì)進(jìn)一步啟動(dòng)和監(jiān)控服務(wù)器進(jìn)程擎值。此外,為了能夠正常終止服務(wù)逐抑,我們讓守護(hù)進(jìn)程在接收到SIGTERM信號(hào)時(shí)終止服務(wù)器進(jìn)程鸠儿。而在服務(wù)器進(jìn)程這一端,同樣在收到SIGTERM信號(hào)時(shí)先停掉HTTP服務(wù)再正常退出厕氨。至此进每,我們的服務(wù)器程序就靠譜很多了。

至此腐巢,NodeJS合并文件的服務(wù)器開發(fā)完成品追,當(dāng)然還有許多不足之處,比如:提供日志通知訪問量冯丙、充分利用多核CPU等等肉瓦。如有興趣,可以在此基礎(chǔ)之上胃惜,做進(jìn)一步的開發(fā)泞莉。


源碼地址

https://github.com/BirdandLion/NodeJSCombineFiles

參考資料

七天學(xué)會(huì)NodeJS

Node.js官網(wǎng)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市船殉,隨后出現(xiàn)的幾起案子鲫趁,更是在濱河造成了極大的恐慌,老刑警劉巖利虫,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挨厚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡糠惫,警方通過查閱死者的電腦和手機(jī)疫剃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硼讽,“玉大人巢价,你說我怎么就攤上這事」谈螅” “怎么了壤躲?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)备燃。 經(jīng)常有香客問我碉克,道長(zhǎng),這世上最難降的妖魔是什么并齐? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任漏麦,我火速辦了婚禮法瑟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘唁奢。我一直安慰自己,他們只是感情好窝剖,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布麻掸。 她就那樣靜靜地躺著,像睡著了一般赐纱。 火紅的嫁衣襯著肌膚如雪脊奋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天疙描,我揣著相機(jī)與錄音诚隙,去河邊找鬼。 笑死起胰,一個(gè)胖子當(dāng)著我的面吹牛久又,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播效五,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼地消,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了畏妖?” 一聲冷哼從身側(cè)響起脉执,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎戒劫,沒想到半個(gè)月后半夷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡迅细,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年巫橄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疯攒。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嗦随,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出敬尺,到底是詐尸還是另有隱情枚尼,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布砂吞,位于F島的核電站署恍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蜻直。R本人自食惡果不足惜盯质,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一袁串、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呼巷,春花似錦囱修、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至压储,卻和暖如春鲜漩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背集惋。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工孕似, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刮刑。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓喉祭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親雷绢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子臂拓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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