Node.js自学笔记

Node.JS学习笔记 REPL:Read-Evaluate-Print-Loop 输入- 求值 - 输出- 循环 即交互式命令行解析器

一、事件循环机制,编写健壮的node程序

node 与js一样都是单线程方式运行,即同一时间只能处理一件事情;就如同邮递员送信,每封信都是一个事件,他有一堆信需要按照顺序去送,每封信都会通过相应的路径进行投递。路径就是对应事件的回调函数(通常不止一条路径)。 我们从简单的情形入手想,对比一下邮递员的行为和一般程序的做法。假设web服务器(HTTP)被请求要从数据库中读取一些数据,然后返回给用户。在这种情况下,我们只要处理很少的事件。首先,用户的请求多是要Web服务器返回一个网页。处理这个初始请求的回调函数(回调函数A)会现从请求的对象中确定它要从数据库读取什么内容,然后向数据库发起具体的请求,并传入一个函数(回调函数B)供请求完成时使用。处理完请求后,回调函数A结束并返回当数据库找到需要的内容后,在触发相应的事件。事件循环队列则调用回调函数B,让他把数据发送给用户。

1、 为什么说Node更加高效?

以往如PHP等Web平台所使用的方法。就类似于在快餐店点餐服务员先招待你,待你点完后才服务下一个客人,他输入了你的单子后,可以收款,为你倒饮料等。但是他还不知道厨房要多久才能把你的汉堡做好。在传统的Web服务框架下,每个服务程序(线程),每次只能服务一个请求。唯一增加处理能力的方法就是加入更多的线程。很显然这样的做法并不是那么的高效,服务员在等待厨房做菜时浪费了很多时间。而Node使用的方式是类似于餐馆的更加高效的模式。你点完餐后,服务员会给你一个号码,在菜做好后通知你,你可以称之为回调号码。需要注意的是与邮递员的例子一样,Node绝不会同时处理两个事件。 Node.js程序本身需要把每一个回调函数都写得运行迅速,防止把事件循环给堵塞住。 这意味着在编写Node.js服务器程序的时候需要遵循以下两个策略:

  • ①在设置完成以后,所有的操作都是事件驱动的。
  • ②如果Node.js需要长时间处理数据,就需要考虑把它分配给web worker去处理。

事件驱动方法配合事件循环工作起来非常高效,但编写容易阅读和理解的事件驱动代码也同样重要。如果我们使用匿名函数作为事件回调,这会导致几点不便。

  • ①我们无法控制代码在哪里使用。
  • ②匿名函数只有在被使用的地方才存活,而不是在绑定事件回调时存活,这回影响调试。
  • ③如果所有的东西都是匿名事件,当异常发生时,就很难分辨出是哪个回调函数导致了问题。
2、模式

①无序的并行I/O
在Node.js中最好实现的。事实上,Node中所有的I/O操作默认都是无序并行的,因为Node的所有I/O都是异步非阻塞的。我们操作I/O时,只要扔出请求然后等待结果就行了。所有的请求可能按我们操作的顺序执行,也可能不是。我们指的无序,并不是指乱序,而是指顺序没有保证。
例:

fs.readFile('foo.txt','utf8', function (err, data) {
    console.log(data);
});
fs.readFile('bar.txt','utf8', function (err, data) {
    console.log(data); 
})

简单地调用I/O请求并制定回调函数就会创建无序并行I/O操作。在未来的某一时刻,所有的这些回调函数都会被处罚,但哪一个先被触发是未知的。而且如果某一个请求返回了错误而非数据,也不会影响其他请求。 ②顺序串行I/O 在这个模式里,我们希望按顺序执行一些I/O任务。每一个任务都必须在上一个任务完成后才能开始。在Node里,这意味着使用嵌套回调,这样可以在每个人物的回调函数里发起下一个任务。
例:

server.on('request', function (req, res) {
    //从memcached里获取session信息
    memcached.getSession(req, function (session) {
        //从db获取信息
        db.get(session.user, function (userData) {
            //其他的Web服务调用
            ws.get(req, function (wsData) {
                //渲染页面
                page = pageRender(req,session,userData,wsData);
                //输出相应内容
                res.write(page);
            })
        })
    })
})

虽然嵌入回调函数很容易穿件顺序串行的I/O,但它的代码看起来很像“金字塔”。这样的代码很难阅读和理解,也难以维护。比如,扫一眼上述实例并不能看清楚memcached.getSession请求完成后发起db.get请求,等db.get完成后由发起ws.get请求,等等。要让代码可读又不会破坏顺序串行模式,有几种方法。 第一,我们可以继续使用内联函数声明,弹药给它们增加名字,这样容易调试,而且还表明了该回调函数的目的。
例:

server.on('request', getMemCached (req, res) {
    //从memcached里获取session信息
    memcached.getSession(req, getDbInfo (session) {  
        //从db获取信息  
        db.get(session.user, getWsInfo (userData) {  
            //其他的Web服务调用  
            ws.get(req, render (wsData) {  
                //渲染页面  
                page = pageRender(req,session,userData,wsData);
                //输出相应内容
                res.write(page);
            })
        })
    })
})

另一种方法需要改变代码风格,用提前声明的函数代替匿名函数灬命名函数。这回把金字塔拆散,改为按执行顺序展示,并且代码呗拆分成更加可控的小块。如下例
例:

var render = function (wsData) {
    page = pageRender(req,session,userData,wsData);
}
var getWsInfo = function (userData) {
    ws.get(req, render)
}
var getDbInfo = function (session) {
    db.get(session.user, getWsInfo)
}
var getMemCached = function (req, res) {
    memcached.getSession(req,getDbInfo)
}

同样也可以采用展开的重构方法,但需要创建一个把原始请求都包含的共享作用域,用一个闭包把所有的回调函数都包含进去。这样,所有与初始请求相关的回调函数都被封装起来,并通过闭包内的变量共享状态。如下例:

server.on('request',function(req,res){
    var render = function(wsData){
        page = pageRender(req,session,userData,wsData);
    };
    var getWsInfo = function(userData){
        ws.get(req,render);
    };
    var getDbInfo = function(ssession){
        db.get(session.user,getWsInfo);
    }
    var getMemCached = function(req,res){
        memcached.getSession(req,getDbInfo);
    }
})

采用这样的方法,不但代码组织更有逻辑性,而且利用展开的方法避免了多层嵌套的困绕。
在JavaScript中,对象是以引用的方式传递。意思是,当你调用某个Function(someobject)时,对someobject的任何修改,都会影响你当前函数作用域中所有对someobject的引用。这样会存在潜在的风险,因为回调函数使用的对象被别人修改了,它难以确定是什么时候修改的,因为运行的次序是非线性的。
一般简单的做法就是,用某个东西来表示状态,然后把它在所有需要依赖此状态的函数间传递。这就需要所有依赖此状态的函数通过统一的接口来互相传递。这也是Connect(以及Express)中间件的形式都是function(req,res,next)的原因。
在函数中传递修改后的内容,代码如下:

var AwesomeClass = function(){
    this.awesomeProp = 'awesome!';
    this.awesomeFunc = function(text){
        console.log(text + ' is awesome!')
    }
}
var awesomeObject = new AwesomeClass()
function middleware(func){
    oldFunc = func.awesomeFunc;
    func.awesomeFunc = function(text){
        text = text + 'really';
        oldFunc(text)
    }
}
function anotherMiddleware(func){
    func.anotherProp = 'super duper'
}
function caller(input){
    input.awesomeFunc(input.anotherProp)
}
middleware(awesomeObiect);
anotherMiddleware(awesomeObject);
caller(awesomeObject)

3、编写产品代码

Node现在有一些限制,比如规定了最大的JavaScript堆栈的大小。这会影响你的部署方式,因为在使用Node的易编程,单线程模型来部署时,需要考虑如何充分的利用机器的CPU和内存。

(1)差错处理

JavaScript包含了try/catch功能,但这个方法只有当错误发生在内链位置时才有用。使用Node的非阻塞I/O时,你给函数传递了一个回调函数,这意味着回调函数被时间出发调用时,是不在try/catch代码块中的。我们需要为异步运行情景提供差错处理的方法。
在Node中,我们利用error事件来处理此问题。这是一个特殊的事件,当错误发生时他就会出发。这让参与I/O的模块出发另外一个事件给负责处理错误的回调函数。error事件让我们能够处理所有使用的模块中可能出现的问题。

var http = require('http');
var opts = {
    host:'jdsaflkjgdfahjrh.net',
    port:80,
    path:'/'
}
var req = http.get(opts,function(res){
    console.log('This will never get called')
});
req.on('error',functon(e){
    console.log('Got that pesky error trapped')
})
(2)使用多处理器

Node是单线程的,所以只能利用一个处理器来工作,但是多数服务器都有“多核”处理器,一个多核处理器就包含了几个处理器。要充分的发挥Node的作用,需要把这些处理器都利用起来。
Node提供了一个cluster模块,可以把任务分配给自己成,就是说Node把当前进程复制了一份给另一个进程(在Windows上,它其实是另外一个线程)。通过clusterAPI,就可以把工作分配给Node进程,并分布在服务器所有可用的处理器上,这能充分的利用资源。如下例:

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
if(cluster.isMaster){
    //创建工作进程
    for (var i = 0;i < numCPUs; i++){
        cluster.fork();
    }
    cluster.on('death',function(worker){
        console.log('worker ' + worker.pid +  ' died');
    });
}else{
    //工作进程穿件http服务器
    http.Server(function(req,res){
        res.writeHead(200);
        res.end("hello world\n");
    }).listen(8000);
}

在上面例子中,我们使用了Node的一些核心模块来把工作平均分配给所有可用的CPU。其中从os模块中,我们可以轻松得到系统CPU的数量。
cluster工作的原理是每一个Node进程要么是主进程,要么成为工作进程,当一个主进程调用cluster。fork()方法时,它会创建与主进程一模一样的子进程,除了两个让每个进程可以自己检查自己是父/子进程的属性以外。在主进程中(Node运行时直接调用的那个脚本),cluster.isMaster会返回true,而cluster.isWorker会返回false。而在子进程,cluster.isMaster返回false,且cluster.isWorker返回true。
通过上面的那个例子我们可以知道,主脚本为每个CPU创建了一个工作进程。每个紫禁城创建了一个HTTP服务器,这是cluster另一个独特的地方。在使用cluster的迪凡使用listen()监听一个socket的时候,多个进程可以同时监听同一个socket。如果通过调用nodemyscript.js的方法气动多个Node进程,会导致出错。cluster提供了跨平台时让多个进程共享socket的方法。即使多个子进程在共享一个端口上的链接,其中一个堵塞了,也不会影响其他工作进程的新连接。
cluster还可以检查子进程的健康状态,当子进程死亡时主进程会用console.log()输出死亡提醒。

//出现死亡进程后重新开启新的进程
if(cluster.isMaster){
    //创建工作进程
    for (var i = 0;i < numCPUs; i++){
        cluster.fork();
    }
    cluster.on('death',function(worker){
        console.log('worker ' + worker.pid +  ' died');
        cluster.fork();
    });

这个简单的改造让主进程会不停的把死掉的进程重启,从而保证所有的CPU都有我们的服务器在运行。
然而这只是对运行状态的基本检查。由于工作进程可以传递消息给主进程,所以可能让每个工作进程报告自己的状态,如内存使用量。这让主进程可以察觉哪些工作进程变得不稳定,确认哪些工作进程没有冻结,或者长时间运行的事件堵塞。

//通过消息传递来监控工作进程状态
var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
var rssWarn = (12 * 1024 * 1024),
    heapWarn = (10 * 1024 * 1024);
if(cluster.isMaster){
    for(var i=0;i<numCPUs;i++){
        var worker = cluster.fork();
        worker.on('message',function(m){
            if(m.memory){
                if(m.memory.rss>rssWarn){
                    console.log('Worker' + m.process + ' using too much memory')
                }
            }
        })
    }
}else{
    //服务器
    http.Server(function(req,res){
        res.writeHead(200);
        res.end('hello world\n');
    }).listen(8000);
    //每秒报告一次状态
    setInterval(function report(){
        process.send({memory: process.memoryUsage(),process: process.pid})
    },1000)
}

在这个例子里,工作进程报告自己的内存使用量,当子进程使用了过多的内存时,主进程会发送一条警告到日志中去。
如果我们识别了一个长时间运行的回调函数,我们也没有办法主动关闭它。因为我们发送给该进程的任何通知都会加到事件队列里,所以他需要等待已经在长时间运行的回调函数结束后才会被处理。因此,虽然我们能够让主进程识别僵尸进程,单位一的补救方法就是杀掉这个工作进程,而这会丢失他正在执行的工作。

//杀死僵尸进程
var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;
var rssWarn = (50 * 1024 * 1024),
    heapWarn = (50 * 1024 * 1024);
var worker = {};
if(cluster.isMaster){
    for(var i=0;i<numCPUs;i++){
        createWorker();
    }
    setInterval(function(){
        var time = new Date().getTime();
        for(pid in workers){
            if(workers.hasOwnProperty(pid)&&workers[pid].lastCb + 5000 < time){
                console.log('Long running worker ' + pid + ' killed');
                workers[pid].worker.kill();
                delete workers[pid];
                createWorker();
            }
        }
    },1000)
}else{
    //服务器
    http.Server(function(req,res){
        //打乱200个请求中的一个
        if(Math.floor(Math.random() * 200) ===4){
        console.log('Stopped' + process.pid + ' from ever finishing');
        while(true){continue}
        }
        res.writeHead(200);
        res.end('hello world from ' + process.pid + '\n');
    }).listen(8000);
    //每秒报告一次状态
    setInterval(function report(){
        process.send({cmd:"reportMem",memory: process.memoryUsage(),process: process.pid})
    },1000)
}
function createWorker(){
    var worker = cluster.fork();
    console.log('Created worker: ' + worker.pid);
    //允许开机时间
    workers[worker.pid] = {
        worker:worker,lastCb:new Date().getTime()-1000
    }
    worker.on('message',function(m){
        if(m.cmd === "reportMem") {
            workers[m.process].lastCb = new Date().getTime();
            if(m.memory.rss > rssWarn){
                console.log('Worker' + m.process + ' using too much memory');
            }
        }
    })
}
posted @ 2017-02-21 19:27  爬虫年纪  阅读(169)  评论(0编辑  收藏  举报