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

相关文章

来自专栏陈仁松博客

ASP.NET Core 'Microsoft.Win32.Registry' 错误修复

今天在发布Asp.net Core应用到Azure的时候出现错误InvalidOperationException: Cannot find compilati...

5248
来自专栏张善友的专栏

LINQ via C# 系列文章

LINQ via C# Recently I am giving a series of talk on LINQ. the name “LINQ via C...

3025
来自专栏闻道于事

js登录滑动验证,不滑动无法登陆

js的判断这里是根据滑块的位置进行判断,应该是用一个flag判断 <%@ page language="java" contentType="text/html...

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

c#实现打印功能

3782
来自专栏大内老A

The .NET of Tomorrow

Ed Charbeneau(http://developer.telerik.com/featured/the-net-of-tomorrow/) Exciti...

39210
来自专栏张善友的专栏

Silverlight + Model-View-ViewModel (MVVM)

     早在2005年,John Gossman写了一篇关于Model-View-ViewModel模式的博文,这种模式被他所在的微软的项目组用来创建Expr...

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

Luminous版本PG 分布调优

Luminous版本开始新增的balancer模块在PG分布优化方面效果非常明显,操作也非常简便,强烈推荐各位在集群上线之前进行这一操作,能够极大的提升整个集群...

3685
来自专栏ASP.NETCore

ASP.NET Core 整合Autofac和Castle实现自动AOP拦截

除了ASP.NETCore自带的IOC容器外,我们还可以使用其他成熟的DI框架,如Autofac,StructureMap等(笔者只用过Unity,Ninjec...

754
来自专栏pangguoming

Spring Boot集成JasperReports生成PDF文档

由于工作需要,要实现后端根据模板动态填充数据生成PDF文档,通过技术选型,使用Ireport5.6来设计模板,结合JasperReports5.6工具库来调用渲...

1.4K7
来自专栏一个爱瞎折腾的程序猿

sqlserver使用存储过程跟踪SQL

USE [master] GO /****** Object: StoredProcedure [dbo].[sp_perfworkload_trace_s...

2980

扫码关注云+社区