在线二区人妖系列_国产亚洲欧美日韩在线一区_国产一级婬片视频免费看_精品少妇一区二区三区在线

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

迭代

第一次迭代

快速迭代是一種不錯(cuò)的開發(fā)方式,因此我們?cè)诘谝淮蔚鷷r(shí)先實(shí)現(xiàn)服務(wù)器的基本功能。

設(shè)計(jì)

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


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

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

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

實(shí)現(xiàn)

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

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

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

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

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

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

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

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

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

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

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

第二次迭代

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

設(shè)計(jì)

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


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

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

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

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

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

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

按上述方式解決第一個(gè)問題后,因?yàn)榉?wù)器不需要完整地緩存每個(gè)請(qǐng)求的輸出數(shù)據(jù)了,第二個(gè)問題也迎刃而解。

實(shí)現(xiàn)

根據(jù)以上設(shè)計(jì),第二版代碼按以下方式調(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));
}

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

第三次迭代

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

設(shè)計(jì)

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

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

實(shí)現(xiàn)

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

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

我們可以把守護(hù)進(jìn)程的代碼保存為 daemon.js,之后我們可以通過 node daemon.js config.json 啟動(dòng)服務(wù),而守護(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ù)器程序就靠譜很多了。

第四次迭代

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

設(shè)計(jì)

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

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

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

實(shí)現(xiàn)

按以上目錄結(jié)構(gòu)分別存放對(duì)應(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

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

后續(xù)迭代

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

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