NodeJS
是一個基於Chrome V8
引擎的JavaScript
運行環境,其使用了事件驅動、異步I/O
機制,具有運行速度快,性能優異等特點,非常適合在分佈式設備上運行數據密集型的實時應用。
本文主要介紹一下通過搭建簡單的NodeJS
服務器,實現靜態文件的合併,並通過瀏覽器訪問輸出的功能;同時,還會進行功能的完善,通過不斷的迭代開發,從易用性、性能、安全性等等方面,較爲全面的介紹一下NodeJS
服務器的開發過程,爲以後的進一步學習做準備。
在下面的內容開始之前,假定您對JavaScript
已經有了一定的瞭解,如果您之前沒有了解過,請先熟悉一下七天學會NodeJS,本文主要參考上述資料的最後一部分,爲作者的開源奉獻精神表示感謝。下面正式開始介紹服務器的具體實現:
需求
實現一個靜態文件合併的服務器,通過請求的鏈接(URL
)指定需要合併的文件,之後把文件內容返回給客戶端。參考鏈接如下:
http://127.0.0.1:8300/??a.js,b.js
分析
鏈接中的??
是一個分隔符,前面是需要合併的文件路徑,後面是需要合併的文件名,多個文件名之間用,
分隔,因此服務器處理這個URL後返回的是各個文件的路徑;之後,通過遞歸讀取文件內容,再進行拼接合並;最後,通過響應數據輸出給客戶端。這是整個服務器的全部分析過程。
由於涉及到文件操作,所以需要fs
模塊、path
模塊;加上服務器模塊http
,一共需要三個模塊:fs、path、http
。
第一版
源碼如下:
var fs = require('fs'),
path = require('path'),
http = require('http');
var MIME = {
'.css': 'text/css',
'.js': 'application/javascript'
};
// 合併文件內容
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) {
// 從文件讀取配置參數
// var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
// root = config.root || '.',
// port = config.port || 80;
// 直接給定配置參數
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));
/*
測試URL: 127.0.0.1:8300/??a.js,b.js
輸出:
hello
kelvin
world
*/
以上代碼完整實現了服務器的功能,可以用測試URL
請求,就會輸出其後的內容。其中,有幾點需要注意:
- 命令行參數可以通過讀取
JSON
配置文件,或者直接在main
函數內設定(缺點是修改不方便,配置不靈活) - 入口
main
函數開啓了http
服務器;combineFiles
函數負責異步讀取文件內容,併合並文件內容;parseULR
函數負責解析URL
,並返回文件的MIME
類型(在返回數據給客戶端時,指定數據的類型)和文件名數組,。
服務器的工作流程如下:
發送請求 等待服務端響應 接收響應
---------+----------------------+------------->
-- 解析請求
------ 讀取a.js
------ 讀取b.js
------ 讀取c.js
-- 合併數據
-- 輸出響應
第二版
由於第一版中,代碼是把文件內容全部讀取到內存後,再進行數據合併的,這會導致如下問題:
- 當請求的文件較多,需要合併的數據量又比較大時,串行讀取文件會比較耗時,拖慢服務的相應時間
- 每次都完整的把數據讀到內存緩存起來,當服務器併發數較大時,就會有較大的內存開銷
針對上面的第一個問題,如果改爲並行讀取方式,對於機械磁盤來說,需要不停的切換磁頭,反而會降低I/O
效率。而對於固態硬盤,是存在多個並行的I/O
的,對單個請求採用並行也不會提高效率。因此,採用流式讀取方式:一遍讀取,一遍輸出,把相應的輸出時機提前至讀取第一個文件的時刻,這樣就能解決上述的問題。
修改後的服務器工作流程如下:
發送請求 等待服務端響應 接收響應
---------+----+------------------------------->
-- 解析請求
-- 檢查文件是否存在
-- 輸出響應頭
------ 讀取和輸出a.js
------ 讀取和輸出b.js
------ 讀取和輸出c.js
可以看到,調整後的代碼是邊讀取邊輸出,即快速響應請求,有減少了內存的壓力。
源碼如下:
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();
第三版
服務器的功能和性能已經得到初步滿足,接下來我們要考慮穩定性。由於沒有系統是絕對的穩定,都存在一定的宕機風險,而這一問題不可避免,所以我們要儘量減少宕機的時間,比如增加一個守護進程,在服務器掛掉後立即重啓。並且NodeJS
官方也建議在出現異常時重啓,因爲這時系統處於一種不穩定的狀態。
所以,我們利用NodeJS
的進程管理機制,將守護進程作爲父進程,將服務器進程作爲子進程,讓父進程監控子進程的運行狀態,在其異常時立即退出重啓子進程。
守護進程代碼如下:
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('自動重啓');
spawn(server, config);
}
});
}
function main(argv) {
spawn('server2.js', argv[0]);
process.on('SIGTERM', () => {
worker.kill();
process.exit(0);
});
}
main(process.argv.slice(2));
服務器代碼也要在main
函數裏做如下調整:
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);
});
});
}
這樣調整後,守護進程會進一步啓動和監控服務器進程。此外,爲了能夠正常終止服務,我們讓守護進程在接收到SIGTERM信號時終止服務器進程。而在服務器進程這一端,同樣在收到SIGTERM信號時先停掉HTTP服務再正常退出。至此,我們的服務器程序就靠譜很多了。
至此,NodeJS
合併文件的服務器開發完成,當然還有許多不足之處,比如:提供日誌通知訪問量、充分利用多核CPU
等等。如有興趣,可以在此基礎之上,做進一步的開發。
源碼地址
https://github.com/BirdandLion/NodeJSCombineFiles
參考資料