《深入浅出Node.js》:Node的异步I/O流程原理解析

异步I/O、事件驱动和单线程构成了Node的基调。与Node的事件驱动和异步I/O设计理念相接近的是Nginx,它采用纯C编写,性能非常优异。两者区别在于,Nginx具备面向客户端管理连接的强大能力,但它背后依然受限于各种同步方式的编程语言。而Node却是全方位的,既可以作为服务器去处理客户端带来的大量并发请求,也能作为客户端向网络中的各个应用进行并发请求。这就体现了Node名字的含义,是网络中灵活的一个节点。

Node中完整的异步I/O环节包括事件循环、观察者、请求对象和执行回调。

事件循环

事件循环是一个类似于while(true)的循环,每执行一次循环体的过程称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出进程。

在这里插入图片描述

观察者

在每个事件循环(Tick)的过程中,判断是否有事件需要处理的就是“观察者”。每个事件循环中有一个或多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

参考浏览器中的事件机制,其中的事件可能来自用户的点击或加载某些文件或代码时产生,而这些产生的事件都有对应的观察者。在Node中,事件的产生主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等。观察者将事件进行分类。

事件循环就是一个包含若干个典型的发布/订阅模式的模型。其中异步I/O、网络请求等都是事件的发布者(trigger),这些发布请求被传递到对应的订阅者(listen)那里时,事件循环就会从订阅者那里取出事件并处理。在Windows下,这个循环基于IOCP创建,而在*nix下则基于多线程创建。

下面给出一个观察者模式代码实现以供理解,代码解释看注释,注意下面这个是用js的面向对象委托写的,感兴趣的也可以改写成面向对象类的形式:

// 定义发布/订阅对象
var ObserverEvent = (function (){
    var cacheList = {},     // 缓存列表,存放已订阅的事件回调
        listen,             // 订阅命名事件和对应事件回调
        trigger,            // 触发命名事件,必传第一个参数为事件的命名,其后参数为选传,数量不限,用于作为事件回调的实参传入
        remove;             // 取消命名事件订阅,并清除该命名事件对应的事件回调

    listen = function( key, fn ){
        //如果还没有订阅过此命名事件,就给该命名事件创建一个数组型的缓存列表
        if( !cacheList[key] ){  
            cacheList[key] = [];
        }

        //将对应的事件回调传入该命名事件的缓存列表中
        cacheList[key].push( fn );
    };

    trigger = function(){
            // 取出事件命名
        var key = Array.prototype.shift.call( arguments ),
            // 取出该命名事件对应的事件回调缓存列表
            fns = cacheList[key];

        // 如果没有订阅该命名事件或对应的事件回调缓存列表为空数组,则直接返回false
        if( !fns || fns.lenght === 0 ){
            return false;
        }

        // 遍历该命名事件对应的事件回调缓存列表数组,对数组中的每个事件回调传入处理后的实参列表,然后执行
        for(var i=0; i<fns.length; i++){
            fns[i].apply( this, arguments );
        }
    };

    remove = function( key, fn ){
        var fns = cacheList[key];

        if( !fns || fns.length === 0 ){
            return false;
        }

        if( !fn ){
            // 如果没有显式传入具体的事件回调函数,则清除该命名事件对应的所有事件回调缓存
            fns.length = 0;
        }else {
            for( var l=fns.length-1; l>=0; l-- ){
                var _fn = fns[l];
                if( _fn === fn ){
                    fns.splice( l, 1 );
                }
            }
        }
    };

    return {
        cacheList,
        listen,
        trigger,
        remove
    }
})()

// 定义发布订阅对象安装函数,该函数可以为指定对象安装发布-订阅功能
var installEvent = function( obj ){
    for( var i in ObserverEvent ){
        obj[ i ] = ObserverEvent[ i ]
    }
}

// 为pageData对象安装发布订阅功能
var pageData = {};
installEvent( pageData );

pageData.listen( "test", function(msg){
    console.log( msg );
} )

setTimeout( function(){
    pageData.trigger( "test", "发布-订阅模式测试成功!" )
}, 3000 )

// 在未被其他循环占用的情况下,3秒后打印字符串结果:
// 发布-订阅模式测试成功!

以上就是一个完整的发布-订阅模式,通过实践,可以看到,事件循环中有订阅者pageData.listen(...),也有发布者pageData.trigger(...),当3秒后发布请求被传递到对应的订阅者那时,事件循环就从订阅者那里取出事件并处理。

请求对象

Node中请求对象其实就是JavaScript发起调用到内核执行完I/O操作过程的过渡中间产物,它是保存所有状态的一个对象,包括送入线程池等待执行以及I/O操作完毕后的执行回调处理。

fs.open()为例,它的作用是根据指定路径和参数打开一个文件,从而得到一个文件描述符,这是所有后续I/O操作的初始操作。JavaScript层面的代码通过调用C/C++核心模块进行下层的操作,下面是调用示意图:
在这里插入图片描述

Node先从JavaScript核心模块所处的lib文件夹中调用fs.js模块,然后再调用C/C++核心模块所处的src文件夹中调用node_file.cc这个C++内建模块,再接下来就是进行系统平台的判定,然后继续执行下层操作。

执行回调

当组装好保有状态的请求对象、送往I/O线程池(这块我看不懂,应是C/C++内建模块涉及的操作)等待执行,实际上就是完成了异步I/O的第一部分,回调通知是第二部分。

线程池中的I/O操作调用完毕后,会将获取的结果储存在req->request属性上,然后调用PostQueuedCompletionStatus()通知IOCP,告知当前对象操作已经完成。PostQueuedCompletionStatus()方法的作用是向IOCP提交执行状态,并将线程归还线程池。通过PostQueuedCompletionStatus()方法提交的状态,可以通过GetQueuedCompletionStatus()提取。

在这个过程中,其实还使用了事件循环的I/O观察者。在每次Tick执行中,它会调用IOCP相关的GetQueuedCompletionStatus()方法检查线程池中是否有执行完的请求,如果存在,则会将请求对象加入到I/O观察者队列中,然后将其当作事件处理。

I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出oncomplete_sym属性作为方法,然后调用执行,以此达到调用JavaScript中传入的回调函数的目的。

到此,整个异步I/O的流程结束,事件循环、观察者、请求对象和执行回调是整个异步I/O的四个基本要素。下面给出示意图:
在这里插入图片描述

在Node异步I/O的实现原理中,也基本弄清事件驱动的本质:通过主循环加事件触发的方式来运行程度

喜欢本文请扫下方二维码,关注微信公众号: 前端小二,查看更多我写的文章哦,多谢支持。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值