在第一次迭代之后,我們已經有了一個可工作的版本,滿足了功能需求。接下來我們需要從性能的角度出發,看看代碼還有哪些改進余地。
## 設計
把`map`方法換成`for`循環或許會更快一些,但第一版代碼最大的性能問題存在于從讀取文件到輸出響應的過程當中。我們以處理`/??a.js,b.js,c.js`這個請求為例,看看整個處理過程中耗時在哪兒。
~~~
發送請求 等待服務端響應 接收響應
---------+----------------------+------------->
-- 解析請求
------ 讀取a.js
------ 讀取b.js
------ 讀取c.js
-- 合并數據
-- 輸出響應
~~~
可以看到,第一版代碼依次把請求的文件讀取到內存中之后,再合并數據和輸出響應。這會導致以下兩個問題:
1. 當請求的文件比較多比較大時,串行讀取文件會比較耗時,從而拉長了服務端響應等待時間。
2. 由于每次響應輸出的數據都需要先完整地緩存在內存里,當服務器請求并發數較大時,會有較大的內存開銷。
對于第一個問題,很容易想到把讀取文件的方式從串行改為并行。但是別這樣做,因為對于機械磁盤而言,因為只有一個磁頭,嘗試并行讀取文件只會造成磁頭頻繁抖動,反而降低IO效率。而對于固態硬盤,雖然的確存在多個并行IO通道,但是對于服務器并行處理的多個請求而言,硬盤已經在做并行IO了,對單個請求采用并行IO無異于拆東墻補西墻。因此,正確的做法不是改用并行IO,而是一邊讀取文件一邊輸出響應,把響應輸出時機提前至讀取第一個文件的時刻。這樣調整后,整個請求處理過程變成下邊這樣。
~~~
發送請求 等待服務端響應 接收響應
---------+----+------------------------------->
-- 解析請求
-- 檢查文件是否存在
-- 輸出響應頭
------ 讀取和輸出a.js
------ 讀取和輸出b.js
------ 讀取和輸出c.js
~~~
按上述方式解決第一個問題后,因為服務器不需要完整地緩存每個請求的輸出數據了,第二個問題也迎刃而解。
## 實現
根據以上設計,第二版代碼按以下方式調整了部分函數。
~~~
function main(argv) {
var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
root = config.root || '.',
port = config.port || 80;
http.createServer(function (request, response) {
var urlInfo = parseURL(root, request.url);
validateFiles(urlInfo.pathnames, function (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], function (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));
}
~~~
可以看到,第二版代碼在檢查了請求的所有文件是否有效之后,立即就輸出了響應頭,并接著一邊按順序讀取文件一邊輸出響應內容。并且,在讀取文件時,第二版代碼直接使用了只讀數據流來簡化代碼。