在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ HTML/ 迭代
文本編碼
小結(jié)
API 走馬觀花
API 走馬觀花
迭代
小結(jié)
運行
回調(diào)
需求
代碼設(shè)計模式
進程介紹
模塊
工程目錄
小結(jié)
小結(jié)
遍歷目錄
小結(jié)
小結(jié)
API 走馬觀花
用途
NPM
小結(jié)
安裝
網(wǎng)絡(luò)操作介紹
二進制模塊
什么是 NodeJS
命令行程序
靈機一點
域(Domain)
應(yīng)用場景
模塊路徑解析規(guī)則
文件拷貝

迭代

第一次迭代

快速迭代是一種不錯的開發(fā)方式,因此我們在第一次迭代時先實現(xiàn)服務(wù)器的基本功能。

設(shè)計

簡單分析了需求之后,我們大致會得到以下的設(shè)計方案。


           +---------+   +-----------+   +----------+
request -->|  parse  |-->|  combine  |-->|  output  |--> response
           +---------+   +-----------+   +----------+

也就是說,服務(wù)器會首先分析 URL,得到請求的文件的路徑和類型(MIME)。然后,服務(wù)器會讀取請求的文件,并按順序合并文件內(nèi)容。最后,服務(wù)器返回響應(yīng),完成對一次請求的處理。

另外,服務(wù)器在讀取文件時需要有個根目錄,并且服務(wù)器監(jiān)聽的HTTP端口最好也不要寫死在代碼里,因此服務(wù)器需要是可配置的。

實現(xiàn)

根據(jù)以上設(shè)計,我們寫出了第一版代碼如下。

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 {
            callback(null, Buffer.concat(output));
        }
    }(0, pathnames.length));
}

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);

        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) {
        return path.join(root, base, value);
    });

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

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

以上代碼完整實現(xiàn)了服務(wù)器所需的功能,并且有以下幾點值得注意:

  • 使用命令行參數(shù)傳遞 JSON 配置文件路徑,入口函數(shù)負責讀取配置并創(chuàng)建服務(wù)器。

  • 入口函數(shù)完整描述了程序的運行邏輯,其中解析 URL 和合并文件的具體實現(xiàn)封裝在其它兩個函數(shù)里。

  • 解析 URL 時先將普通 URL 轉(zhuǎn)換為了文件合并URL,使得兩種 URL 的處理方式可以一致。

  • 合并文件時使用異步 API 讀取文件,避免服務(wù)器因等待磁盤 IO 而發(fā)生阻塞。

我們可以把以上代碼保存為 server.js,之后就可以通過 node server.js config.json 命令啟動程序,于是我們的第一版靜態(tài)文件合并服務(wù)器就順利完工了。

另外,以上代碼存在一個不那么明顯的邏輯缺陷。例如,使用以下 URL 請求服務(wù)器時會有驚喜。

http://assets.example.com/foo/bar.js,foo/baz.js

經(jīng)過分析之后我們會發(fā)現(xiàn)問題出在/被自動替換/??這個行為上,而這個問題我們可以到第二次迭代時再解決。

第二次迭代

在第一次迭代之后,我們已經(jīng)有了一個可工作的版本,滿足了功能需求。接下來我們需要從性能的角度出發(fā),看看代碼還有哪些改進余地。

設(shè)計

把 map 方法換成 for 循環(huán)或許會更快一些,但第一版代碼最大的性能問題存在于從讀取文件到輸出響應(yīng)的過程當中。我們以處理/??a.js,b.js,c.js這個請求為例,看看整個處理過程中耗時在哪兒。


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

可以看到,第一版代碼依次把請求的文件讀取到內(nèi)存中之后,再合并數(shù)據(jù)和輸出響應(yīng)。這會導致以下兩個問題:

  • 當請求的文件比較多比較大時,串行讀取文件會比較耗時,從而拉長了服務(wù)端響應(yīng)等待時間。

  • 由于每次響應(yīng)輸出的數(shù)據(jù)都需要先完整地緩存在內(nèi)存里,當服務(wù)器請求并發(fā)數(shù)較大時,會有較大的內(nèi)存開銷。

對于第一個問題,很容易想到把讀取文件的方式從串行改為并行。但是別這樣做,因為對于機械磁盤而言,因為只有一個磁頭,嘗試并行讀取文件只會造成磁頭頻繁抖動,反而降低 IO 效率。而對于固態(tài)硬盤,雖然的確存在多個并行IO通道,但是對于服務(wù)器并行處理的多個請求而言,硬盤已經(jīng)在做并行 IO 了,對單個請求采用并行 IO 無異于拆東墻補西墻。因此,正確的做法不是改用并行 IO,而是一邊讀取文件一邊輸出響應(yīng),把響應(yīng)輸出時機提前至讀取第一個文件的時刻。這樣調(diào)整后,整個請求處理過程變成下邊這樣。

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

按上述方式解決第一個問題后,因為服務(wù)器不需要完整地緩存每個請求的輸出數(shù)據(jù)了,第二個問題也迎刃而解。

實現(xiàn)

根據(jù)以上設(shè)計,第二版代碼按以下方式調(diào)整了部分函數(shù)。

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));
}

可以看到,第二版代碼在檢查了請求的所有文件是否有效之后,立即就輸出了響應(yīng)頭,并接著一邊按順序讀取文件一邊輸出響應(yīng)內(nèi)容。并且,在讀取文件時,第二版代碼直接使用了只讀數(shù)據(jù)流來簡化代碼。

第三次迭代

第二次迭代之后,服務(wù)器本身的功能和性能已經(jīng)得到了初步滿足。接下來我們需要從穩(wěn)定性的角度重新審視一下代碼,看看還需要做些什么。

設(shè)計

從工程角度上講,沒有絕對可靠的系統(tǒng)。即使第二次迭代的代碼經(jīng)過反復檢查后能確保沒有 bug,也很難說是否會因為 NodeJS 本身,或者是操作系統(tǒng)本身,甚至是硬件本身導致我們的服務(wù)器程序在某一天掛掉。因此一般生產(chǎn)環(huán)境下的服務(wù)器程序都配有一個守護進程,在服務(wù)掛掉的時候立即重啟服務(wù)。一般守護進程的代碼會遠比服務(wù)進程的代碼簡單,從概率上可以保證守護進程更難掛掉。如果再做得嚴謹一些,甚至守護進程自身可以在自己掛掉時重啟自己,從而實現(xiàn)雙保險。

因此在本次迭代時,我們先利用 NodeJS 的進程管理機制,將守護進程作為父進程,將服務(wù)器程序作為子進程,并讓父進程監(jiān)控子進程的運行狀態(tài),在其異常退出時重啟子進程。

實現(xiàn)

根據(jù)以上設(shè)計,我們編寫了守護進程需要的代碼。

var cp = require('child_process');

var worker;

function spawn(server, config) {
    worker = cp.spawn('node', [ server, config ]);
    worker.on('exit', function (code) {
        if (code !== 0) {
            spawn(server, config);
        }
    });
}

function main(argv) {
    spawn('server.js', argv[0]);
    process.on('SIGTERM', function () {
        worker.kill();
        process.exit(0);
    });
}

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

此外,服務(wù)器代碼本身的入口函數(shù)也要做以下調(diào)整。

function main(argv) {
    var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
        root = config.root || '.',
        port = config.port || 80,
        server;

    server = http.createServer(function (request, response) {
        ...
    }).listen(port);

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

我們可以把守護進程的代碼保存為 daemon.js,之后我們可以通過 node daemon.js config.json 啟動服務(wù),而守護進程會進一步啟動和監(jiān)控服務(wù)器進程。此外,為了能夠正常終止服務(wù),我們讓守護進程在接收到 SIGTERM 信號時終止服務(wù)器進程。而在服務(wù)器進程這一端,同樣在收到 SIGTERM 信號時先停掉 HTTP 服務(wù)再正常退出。至此,我們的服務(wù)器程序就靠譜很多了。

第四次迭代

在我們解決了服務(wù)器本身的功能、性能和可靠性的問題后,接著我們需要考慮一下代碼部署的問題,以及服務(wù)器控制的問題。

設(shè)計

一般而言,程序在服務(wù)器上有一個固定的部署目錄,每次程序有更新后,都重新發(fā)布到部署目錄里。而一旦完成部署后,一般也可以通過固定的服務(wù)控制腳本啟動和停止服務(wù)。因此我們的服務(wù)器程序部署目錄可以做如下設(shè)計。

- deploy/
    - bin/
        startws.sh
        killws.sh
    + conf/
        config.json
    + lib/
        daemon.js
        server.js

在以上目錄結(jié)構(gòu)中,我們分類存放了服務(wù)控制腳本、配置文件和服務(wù)器代碼。

實現(xiàn)

按以上目錄結(jié)構(gòu)分別存放對應(yīng)的文件之后,接下來我們看看控制腳本怎么寫。首先是 start.sh。

#!/bin/sh
if [ ! -f "pid" ]
then
    node ../lib/daemon.js ../conf/config.json &
    echo $! > pid
fi
然后是killws.sh。

#!/bin/sh
if [ -f "pid" ]
then
    kill $(tr -d '\r\n' < pid)
    rm pid
fi

于是這樣我們就有了一個簡單的代碼部署目錄和服務(wù)控制腳本,我們的服務(wù)器程序就可以上線工作了。

后續(xù)迭代

我們的服務(wù)器程序正式上線工作后,我們接下來或許會發(fā)現(xiàn)還有很多可以改進的點。比如服務(wù)器程序在合并 JS 文件時可以自動在 JS 文件之間插入一個;來避免一些語法問題,比如服務(wù)器程序需要提供日志來統(tǒng)計訪問量,比如服務(wù)器程序需要能充分利用多核 CPU,等等。而此時的你,在學習了這么久 NodeJS 之后,應(yīng)該已經(jīng)知道該怎么做了。

上一篇:小結(jié)下一篇:文件拷貝