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 条评论
登录 后参与评论

相关文章

来自专栏carven

实习中结

开始实习至今也有差不多有个月了(实际工作时间是一个多月),见识了很多新的事物,学到很多新的知识。公司搬到了T.I.T创意园。。。 等等,很多感觉是自己一个人在学...

630
来自专栏互联网研发闲思录

java多线程开发容易犯的错误

      昨天在社区上看到有人讨论多线程使用,多线程遇到一些问题以及一些使用技巧记录一下。为什么要使用多线程, 不能是为了用而用,和设计模式一样用的合理,会让...

2126
来自专栏陈本布衣

Spring基础篇——DI/IOC和AOP原理初识

前言   作为从事java开发的码农,Spring的重要性不言而喻,你可能每天都在和Spring框架打交道。Spring恰如其名的,给java应用程序的开发带了...

2137
来自专栏程序猿DD

这有一份廖雪峰大牛的Java高级架构师教程,请查收!

可以说,Java是现阶段中国互联网公司中,覆盖度最广的研发语言,掌握了Java技术体系,不管在成熟的大公司,快速发展的公司,还是创业阶段的公司,都能有立足之地。...

2133
来自专栏PHP在线

关于PHP程序员解决问题的能力

原文出处: 韩天峰(@韩天峰-Rango) 这个话题老生长谈了,在面试中必然考核的能力中,我个人认为解决问题能力是排第一位的,比学习能力优先级更高。解决问题...

3217
来自专栏服务端思维

提高服务端编码质量 - 预防BUG篇

需要团队定义一整套规范,包括注释,包名,类名,字段名,代码结构等等,从而保证代码清晰,良好的格式。

773
来自专栏芋道源码1024

Dubbo源码解析 —— Zookeeper 订阅

前言 上周写完了服务暴露总结之后发现遗漏了一个很重要的点,在dubbo源码解析-zookeeper连接中我们对面试高频题 dubbo中zookeeper做注册...

2707
来自专栏java达人

阿里面试题及相关参考链接(修订版)

似乎每个程序员都有一颗进阿里看看的好奇心,虽然很多人最后也从那座围城里走出来了,但没有去过阿里多多少少总有些遗憾吧。因此,我最近问了一些接到过阿里电话面试的朋友...

990
来自专栏杨建荣的学习笔记

远程协助解决重建索引的危机问题 (r8笔记第80天)

最近在工作忙碌之余也帮几位网友查看了几个问题,有一个问题让我印象挺深,其实也可以分享出来作为一些参考,问题之外还是有一些值得借鉴的地方。 首先是在周末的一...

3224
来自专栏jessetalks

前后端分离开发模式下后端质量的保证 —— 单元测试

概述   在今天, 前后端分离已经是首选的一个开发模式。这对于后端团队来说其实是一个好消息,减轻任务并且更专注。在测试方面,就更加依赖于单元测试对于API以及后...

36010

扫码关注云+社区