Node.js 异步编程基础理解

参考地址:《深入理解node.js异步编程:基础篇》

一、概述

目前开源社区最火热的技术当属 Node.js 莫属了,作为使用 Javascript 为主要开发语言的服务器端编程技术和平台,一开始就注定会引人瞩目。 当然能够吸引众人的目光,肯定不是三教九流之辈,必然拥有独特的优势和魅力,才能引起群猿追逐。其中当属异步 IO 和事件编程模型,本文据 Node.js 的异步 IO 和事件编程做深入分析。

1. 什么是异步

同步和异步是一个比较早的概念,大抵在操作系统发明时应该就出现了。举一个最简单的生活中的例子,比如发短信的情况会比较好说明他们的区别: 同步:正在处于苦逼工作状态中的我,但狗屎运的交到了女朋友并正处于处于热恋期,因此发送短信给她询问那个餐厅吃饭,急不可耐的看着手机等待短信回复,收到信息看完是否加班或者下班; 异步:正处于公司运营决策关键工作状态中的你,不可以被打断太久,随便发送了一条询问老婆什么时候做好晚饭然后吃饭的短信后立马返回工作,一边工作一边等待短信回复通知,根据通知决定是否再工作和下班。

由此可以看出,同步和异步的特点是:

  • 至少在两个对象之间需要协作(男朋友和女朋友,老公和老婆);
  • 两个对象都需要处理一系列的事情(工作和吃饭)。

另一个类似的关于CPU计算和磁盘操作编的例子: 同步:CPU需要计算10个数据,每计算一个结果后,将其写入磁盘,等待写入成功后,再计算下一个数据,直到完成。 异步:CPU需要计算10个数据,每计算一个结果后,将其写入磁盘,不等待写入成功与否的结果,立刻返回继续计算下一个数据,计算过程中可以收到之前写入是否成功的通知,直到完成。

2. 为什么需要异步

知其然,还要知其所以然,读者可能会问,为什么存在异步?根据上面发短信和磁盘操作的例子,答案很明显,为了提高办事的效率,CPU计算速度和磁盘的读写速度差太远了,磁盘供不应求,因此有了计算机的存储系统的分层设计,平衡了效率和成本。可以说懒惰推动人类的进步,任何可以降低花费时间而达到同等功效的方法肯定会被优先采用。发送短信时等待对方回复的时间纯粹的浪费掉了,CPU写入磁盘等待返回的结果的等待时间也被无情的消耗了,这是一个讲究效率的时代完全不能忍受的,因此让员工一直处于忙碌状态,最大限度的榨取员工价值是老板追求的,让CPU和磁盘都不停的满负荷处理事务也是效率需要的。因此,异步处理出现了。

二、Node.js 异步 IO 与事件

初次接触Node.js,恐怕任何人都会被先先灌输的第一条Node.js就与众不同的地方:异步IO和事件驱动。毫无疑问,这确实是Node.js最令人津津乐道的特色之处,也是本文重点分析的地方。

1. Node.js 异步机制

由于异步的高效性,node.js 设计之初就考虑做为一个高效的 web 服务器,作者理所当然地使用了异步机制,并贯穿于整个 node.js 的编程模型中,新手在使用 node.js 编程时,往往会羁绊于由于其他编程语言的习惯,比如 C/C++ ,觉得无所适从。我们可以从以下一段简单的睡眠程序代码窥视出他们的区别: 下面是摘自《linux 程序设计》打印10个时间的C代码:

#include <time.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
    int i;
    time_t the_time;
    for(i = 1; i <= 10; i++) {
        the_time = time((time_t *)0);
        printf("The time is %ld\n", the_time);
        sleep(2);
    }
    exit(0);
}

编译后打印结果为: The time is 1396492137 The time is 1396492139 The time is 1396492141 The time is 1396492143 The time is 1396492145 The time is 1396492147 The time is 1396492149 The time is 1396492151 The time is 1396492153 The time is 1396492155 从C语言的打印结果可以发现,是隔2秒打印一次,按照C程序该有的逻辑,代码逐行执行。 以下 Node.js 代码本意如同上述C代码,使用目的隔2秒打印一次时间,共打印10条(初次从 C/C++ 转来接触 Node.js 的程序员可能会写出下面的代码):

function test() {
    for (var i = 0; i < 10; i++) {
        console.log(new Date);
        setTimeout(function(){}, 2000); //睡眠2秒,然后再进行一下次for循环打印
    }
};
test();

打印结果如下: Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) Tue Apr 01 2014 14:53:22 GMT+0800 (中国标准时间) 观察结果发现都是在14:53:22同一个时间点打印的,根本就没有睡眠2秒后再执行下一轮循环打印! 这是为什么?从官方的文档我们看出 setTimeout 是第二个参数表示逝去时间之后在执行第一个参数表示的 callback 函数,因此我们可以分析, 由于 Node.js 的异步机制,setTimeout 每个 for 循环到此之后,都注册了一个2秒后执行的回调函数然后立即返回马上执行 console.log(new Date),导致了所有打印的时间都是同一个点,因此我们修改for循环的代码如下:

for (var i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(new Date);
    }, 2000);   
}

执行结果如下所示: Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:30:35 GMT+0800 (中国标准时间)

神奇,仍然是同一个时间点,见鬼! 冷静下来分析,时刻考虑异步,for 循环里每次 setTimeout 注册了2秒之后执行的一个打印时间的回调函数,然后立即返回,再执行 setTimeout,如此反复直到 for 循环结束,因为执行速度太快,导致同一个时间点注册了10个2秒后执行的回调函数,因此导致了2秒后所有回调函数的立即执行。 我们在 for 循环之前添加 console.log(“before FOR: ” + new Date) 和之后 console.log(“after FOR: ” + new Date),来验证我们的推测,代码如下:

console.log("before FOR: " + new Date);
for (var i = 0; i < 10; i++) {
    setTimeout(function(){
        console.log(new Date);
    }, 2000);   
}
console.log("after FOR: " + new Date);

打印结果如下(后面省略8条相同的打印行): before FOR: Thu Apr 03 2014 09:42:43 GMT+0800 (中国标准时间) after FOR: Thu Apr 03 2014 09:42:43 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:42:45 GMT+0800 (中国标准时间) Thu Apr 03 2014 09:42:45 GMT+0800 (中国标准时间) …… (省略与上一行8条相同的打印行) 由此可以窥视出Node.js异步机制的端倪了,在for循环中的代码于其后的代码几乎在一个单位秒内完成,而定时器中的回调函数则按要求的2秒之后执行,也是同一秒内执行完毕。 那么如何实现最初C语言每隔2秒打印一个系统时间的需求函数呢,作者实现了如下一个 wsleep 函数,放在 for 循环中,可以达到该目的:

function wsleep(milliSecond) {
    var startTime = new Date().getTime();
    while(new Date().getTime() <= milliSecond + startTime) {
    }
}

但这个写法不建议,会阻塞CPU,令 CPU 在 20s 内都不能做别的事情,推荐用下面写法:

for (var i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(new Date());
    }, 2000*(i+1));
}

2. Node.js事件编程

事件编程并不是一个新的概念,做过界面 UI 编程的程序猿们可以觉得事件再熟悉不过了,特别是客户端开发和 web 开发的感触颇深吧,如 Android、ios、或是 javascript 前端编程的工程师们,一个按钮、一个列表项、一个长按操作等等,每次按下都会由操作系统或者浏览器产生一个事件,你需要做的工作就是编写和注册这个事件的回调函数(可能各自领域内不称为回调函数,但是从操作系统的角度考虑其实就是一个回调函数),当这个事件发生时,执行你的回调函数。 Node.js 与众不同的是,它基因里就是由事件和异步组成的。请看用于生产环境中的真实项目代码的一个片段(略去了一些不相关的代码),我加上一段关于事件信息的注释,让读者更清晰:

// 监听socket连接事件
self.sio.sockets.on('connection', function(socket) {        
    var addr = socket.handshake.address;
    var limiter = new RateLimiter(constant.RL_MAXREQRATELIMIT, constant.RL_RATELIMITUNIT, true);
    var connect = new Connection(socket);
    then(function(defer) {  
        if (ipLimit) {
        // 结果回调处理事件
            throttle.throttleHandle(connect, null, defer);  
        } else {
            // 发送处理结果事件
            defer(null);    
        }
    // 收到处理结果事件
    }).all(function(defer) {    
        // 监听数据传输事件
        socket.on('message', function(data) {   
                    cloudKeyMain(connect, 1, data, cloudKeyApi);
        });
    });
    // 监听socket离线事件
    socket.on("disconnect", function(data) {    
        var currentSockClient = connect.client;
        if (currentSockClient) {
            // 发送客户端离线事件
            currentSockClient.signalOffline();  
        }

    });
});

从上面的代码,我们可以看出 Node.js 无所不在的事件机制,事件机制让我们专注与代码业务的处理流程,提高了软件开发的效率,降低了代码之间的耦合,让人不被琐事缠绕,编程更有趣。如何开始一个简单的 Node.js 事件编程呢?答案是使用 Node.js 的 javascript API 核心模块 events 的 events.EventEmitter 类即可完成,下面以一个 QQ 的在线和离线来说明,事件机制的使用主要包括3个方面的内容:

  • 继承events.EventEmitter事件类,主要是屏蔽事件机制的实现(其实原理很简单),让我们直接使用;
  • 事件的注册;
  • 事件的发布;
var events = require('events');
var util = require('util');
    function MyQQ() {
    events.EventEmitter.call(this);
    //……
}
util.inherits(MyQQ, events.EventEmitter);

OK,上述代码就完成了事件机制的添加,此时,我们的工作为 QQ 添加事件注册函数进行事件的注册,事件注册主要是使用 EventEmitter 的 on() 完成,因为我们继承了 EventEmitter,可以直接使用 on 函数,我们在 on 函数的第二个参数 callback 函数中自定义处理业务,并注册自己的上线事件(类似于 Qt 的信号槽机制)。以下是一个 QQ 上线时简单的处理业务:

function onlineHandle(QQNumber) {
    //获取和QQNumber的联系人列表
    //获取离线消息
    //……
}
var myQQ = new MyQQ();
myQQ.on(“onLine”, onlineHandle);

上述代码完成了事件的处理,下面轮到在什么时候发布这个事件。下述的一个业务场景中可能是需要发布该事件的,发布事件用emit()函数:

function main() {
    //连接服务器
    //检测登录状态
    //登录服务器成功后发布事件
    myQQ.emit(“onLine”,123655245);
}

上述 myQQ.emit() 函数执行后发布了 onLine 事件后,会立即执行 onlineHandle() 函数,处理我们注册的业务逻辑,需要注意的是,事件发布函数 emit 第二个参数后的参数个数需要和我们注册时的处理函数参数个数相同并且顺序一致才能正确处理,为什么有这样的要求?这需要从 Node.js 事件的原理说起。基本上所有的事件机制都是用设计模式中观察者模式实现,观察者模式网络资料一大堆,如何想要深入了解的话可以网络搜索或者阅读权威书籍,可以参考《设计模式:可复用面向对象软件的基础》和《Head First 设计模式》。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏斑斓

设计匠艺 | 对象的角色

若要获得良好的对象设计,就必须对职责进行合理的分配。每个对象承担的职责不能太多,也不能太少,恰如其分即可。职责分配如乐谱中对音符的组织,高明的音乐家总是能让不同...

2475
来自专栏跨界架构师

如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑

在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码:

583
来自专栏前端杂货铺

deno深入揭秘及未来展望

node.js之父Ryan Dahl在一个月前发起了名为deno的项目,项目的初衷是打造一个基于v8引擎的安全的TypeScript运行时,同时实现HTML5...

711
来自专栏EAWorld

元数据如何驱动微服务报文架构?

随着微服务的概念逐渐被人们接受,大家都在努力将自己的应用系统向微服务框架转型。在我们研发微服务框架的时候,就发现随着服务数量的增多,服务接口定义就需要一套统一数...

3308
来自专栏跟着阿笨一起玩NET

《我的WCF之旅》博文系列汇总

WCF是构建和运行互联系统的一系列技术的总称,它是建立在Web Service架构上的一个全新的通信平台。你可以把它看成是.NET平台上的新一代的Web Ser...

301
来自专栏云成本管理

云成本管理方法论(四)——云优化管理之管理措施

因为判定规则分析中的判定结果较分散,为便于后继的分析和使用,我们将判定结果进行分类,不同的类别称为“问题类型”。

4169
来自专栏技术博客

Entity Framework 关系约束配置

简单的说一下自己的理解,大家应该都很明白ADO.NET,也就是原生态的数据库操作,直接通过拼接SQL语句,表与表之间通过链接(inner join  left ...

581
来自专栏禁心尽力

数据库设计

杨鑫奇数据库设计经验之谈 一个成功的管理系统,是由:[50% 的业务 + 50% 的软件] 所组成,而 50% 的成功软件又有 [25% 的数据库 + 25% ...

1798
来自专栏平凡文摘

关于垃圾回收被误解的 7 件事

1033
来自专栏Ceph对象存储方案

对象存储基础概念

对象存储诞生之初 谈到为什么要有对象存储,必须聊聊对象存储诞生之前的两大存储模型:块存储和文件存储。 块存储主要是将存储介质的空间整个映射给主机使用的,主机如果...

3674

扫码关注云+社区