《Node学习指南》一1.3 异步函数及Node事件循环

1.3 异步函数及Node事件循环

Node学习指南
Node的基本设计原则是将应用程序放置在单线程(或单进程)中执行,同时异步处理所有事件。

考虑下典型的Web服务器(如Apache)是如何工作的。Apache可以采用两种不同的方式处理传入的请求:一种方式是将传入的每个请求分配到独立的进程中直至请求被处理完毕;另一种方式则是为每一个请求生成单独的处理线程。

第一种方式(也称为prefork multiprocessing model,或prefork MPM)可以根据Apache配置文件中指定的值创建多个子进程。使用进程的优势在于被请求的应用(如PHP应用)无需考虑线程安全问题;缺点是每个进程占用独立内存,内存消耗大,应用的扩展性也不是很好。

第二种方式(也称为worker MPM)是进程-线程混合方式。Apache为传入的每个请求创建一个新的处理线程,这样对内存的使用更加有效,但这种方式要求应用必须是线程安全的。虽然现在流行的PHP语言是线程安全的,但却无法保证和它一起被使用的各种库也是线程安全的。

不管哪种方法,它们都可以应对并发请求。如果五个用户在同一时间访问一个Web应用,并且服务器也进行了相应设置,那么Web服务器就可以同时处理五个请求。

Node的处理方式与上面两种不同。当您启动Node应用程序时,它会被创建并运行在一个单线程上。Node会等待应用程序启动完成并开始捕获请求。在未处理完当前请求时,其他请求是不能被处理的。

这种处理方式听起来并不是很有效率,如果Node是通过事件循环和回调函数实现异步运行(在Node中,事件循环一般指轮询指定事件类型并在合适的时间调用事件处理程序,而回调函数就是事件处理程序)的话,它是不应该低效的。

实际上,与一般单线程应用不同,当Node应用程序接收到用户请求时,虽然它会严格按照请求顺序初始化这些资源请求操作(如数据库请求或文件访问),但并不会一直等待操作完成或结果返回。相反,它会在操作请求中附加回调函数。当任何被请求的资源准备好或被请求的操作完成时,特定的事件会被触发,关联的回调函数也会被执行,回调函数会用请求到的资源或操作结果来做另一些事情。

如果五个用户在同一时间访问Node应用程序,并且该应用程序需要访问同一个文件中的资源时,Node会为每个文件访问请求附加一个回调函数但并不等待返回。当资源变为可用时,回调函数会被调用,最终依次满足每个用户的需求。在此期间,Node应用仍然可以处理其他同样或不同类型的用户请求。

尽管Node应用程序不是真正的并行处理用户请求,但其设计方式使得应用能繁忙且高效地处理用户请求,所以大多数人通常不会察觉到任何的响应延迟。最重要的是,它能非常有效地使用内存和其他有限的计算机资源。

1.3.1 使用异步方式读取文件

为了描述Node的异步特性,示例1-2修改了之前章节使用的Hello World程序。它不再输出“Hello,World!”,而是打开先前创建的helloworld.js文件并将其内容输出给客户端。

示例1-2 异步方式地打开文件并写入数据

本示例中使用了一个新的文件系统模块(fs)。该模块对标准的POSIX文件操作进行了封装,提供了包括打开文件和访问文件内容等操作。示例1-2使用了模块中的readFile方法,并传入了多个参数,包括文件名称、文件编码方式以及匿名回调函数。

在示例1-2中,我想指出两个有关异步行为的实例,它们分别是附加在readFile方法和listen方法上的回调函数。

正如前面所讨论的,使用listen方法可以告诉HTTP server对象监听指定端口上的连接。Node不会阻塞并等待连接建立,所以如果我们需要在连接建立时做些事情,就需要提供了一个回调函数,如示例1-2所示。

当网络连接建立时会触发监听事件,该事件会触发listen方法绑定的回调函数,进而将信息输出到控制台。

第二,也是更重要的实例是附加在readFile上的回调函数。相对来说,访问文件是一个耗时的操作。如果一个单线程应用程序被多个客户同时访问,而该应用处理每一个请求时都需要进行文件访问操作的话,它可能很快就会陷入瘫痪而无法使用。

解决方法就是采用异步方式打开文件和读取文件内容。只有当内容已经读入数据缓冲区(或读取失败时),附加在readFile方法上的回调函数才会被调用。错误信息(如果有的话)和读取到的数据(如果没有错误发生时)会作为参数传送给回调函数。

在回调函数中需要进行错误检查,如果不存在错误,则将读取到的数据返回给客户端。

1.3.2 观察异步程序流程

大多数人使用JavaScript编写客户端应用程序,这些程序只能被用户在单个浏览器中运行。而在服务端使用JavaScript编写程序可能看上去会有些古怪和陌生。创建允许多人同时访问的JavaScript服务应用可能就更让人觉得陌生了。

Node的事件循环和异步函数调用可以帮助我们,这让编写服务端JavaScript程序变得容易且更有信心。但一定要注意的是,我们正在一个新的不同以往的环境中做JavaScript开发。

为了更好地描述新环境的不同,我创建了两个新的应用:一个提供服务,另一个用于测试服务。示例1-3显示了服务程序的代码。

在代码中,一个函数被调用,以同步方式按顺序输出从1~100的数字。然后程序以类似于示例1-2的方式打开一个文件,但这次文件名是以字符串参数的形式传递给函数的。此外,程序还使用了一个定时器,文件打开操作被安排在定时器超时之后执行。

示例1-3 输出数字序列和文件内容的服务程序

输出数字的循环体起到了延迟应用程序执行的效果,以便模拟密集计算过程,该过程会引起应用程序阻塞直到计算完成。在这里setTimeout是另一个异步函数,它会紧接着调用第二个异步函数:readFile。所以该应用程序结合了异步和同步流程。

创建一个名为main.txt的文本文件,可以包含任何你想要的内容。运行应用程序并通过Chrome浏览器访问,访问时使用的url需要带有file=name的查询字段,应用程序将生成如下控制台输出:

Server running at 8124/
opening main.txt
opening undefined.txt

前两行输出信息很容易理解。第一行由程序末尾console.log输出,第二行是在文件被打开时输出的。但是,第三行的undefined.txt是怎么回事?

其实,当处理来自浏览器的Web请求时,浏览器可能会发送多个请求。例如,一般浏览器可以发送第二个请求,寻找一个叫favicon.ico的文件。正因为如此,当你在处理查询字符串时,你必须检查看看需要的数据是否被提供,并忽略没有数据的请求。

警告:
当期望从查询字符串中获取某些参数时,浏览器发送多个请求的特点可能会影响到你的应用程序。因此,必须相应的调整应用,并在几个不同的浏览器上进行测试。
到目前为止,我们对Node应用程序所做的所有测试都是从浏览器中进行的。这样我们无法对其进行压力测试来体现Node应用程序的异步特性。

示例1-4是一段非常简单的测试代码。它使用HTTP模块多次向服务程序发送请求。这些请求并不是按异步方式发送的。然而,我们同时也可以使用浏览器访问该服务。两者相结合,就可以达到异步测试应用程序的目的。

提示:
14章将介绍如何创建异步测试应用程序。
示例1-4 测试小程序,调用Node服务程序2000次

创建第二个文本文件,并命名为secondary.txt。内容与main.txt有显著不同即可。

在确定Node服务程序运行起来后,启动测试程序:

node test.js

在测试程序运行的同时,使用浏览器手动访问服务程序。观察服务程序在控制台的输出信息,你会看到来自浏览器的手动请求和来自测试程序的自动请求都能被处理。并且,结果与我们所期望的一致,请求到的页面中包含了如下信息:

  • 1到100的数字;
  • 文本文件的内容,在本示例中是main.txt的内容。
    现在,让我们尝试做一些改动。在示例1-3中,将循环体中计数用的局部变量counter改为全局变量,并重新启动应用程序。然后运行测试程序,并在浏览器中访问该页面。

输出结果显然发生改变。返回的页面内容不再是从1开始到100的数字,而是返回从类似2601和26301这样的数字开始的,按顺序排列的连续99个数字,只是初始值不同。

原因必然是因为使用了全局变量counter。因为在浏览器中手动访问页面时,自动测试程序也在做同样的事,他们都会更新counter。另外由于手动和自动测试程序的请求被按照顺序一个个的处理,因此没有争用共享数据的情况发生(在多线程环境中,并行访问共享数据同时保证线程安全是最主要的问题),如果你之前有期望输出一致的起始值,这里的结果可能会让你感到些许意外。

现在再次更改应用程序,但这次我们删除变量app之前的var关键字(“不小心的”使其成为一个全局变量)。曾几何时,在编写客户端JavaScript时,我们总是忘记使用var关键字。或许也只有当我们程序中用到的某些库使用了相同的变量名时,才会发现这种错误。

运行测试程序同时通过浏览器手动访问Node服务程序多次。你会发现浏览器得到的页面中偶尔会包含secondary.txt文件的内容,而不是期望的main.txt文件内容。这是因为在应用程序处理请求(带有文件名)和真正执行文件打开操作之间有一段时间间隔,在此间隔期间测试程序的持续访问会使得服务程序修改app变量。测试程序之所以能够引起这样的问题,是因为我们做了一个异步功能调用,在异步调用开始执行而没有完成前,Node会放弃对当前请求处理过程的控制权来处理另一个用户请求。

提示:
这个示例说明了正确使用var关键字在Node中是至关重要的。

时间: 2024-08-30 13:28:06

《Node学习指南》一1.3 异步函数及Node事件循环的相关文章

《Node学习指南》一导读

前 言 Node学习指南 非同寻常的JavaScript 目前正是学习Node的好时机. Node相关的技术依然年轻充满生机,经常出现有趣的变化和改动.同时,这项技术也达到了一定的成熟度,可以确保你在学习Node上花费的时间是值得的:即使在Windows上安装也非常简单:从成百上千的可用模块中涌现出了最佳组合模块:对于产品环境来说这种结构足够健壮. 当使用Node时需要记得两个要点.第一,Node是基于JavaScript的,与你之前用于客户端开发的JavaScript多少有些类似.当然,你也可

《Node学习指南》一2.3 多行以及更复杂的JavaScript

2.3 多行以及更复杂的JavaScript Node学习指南 你可以像写文件一样在REPL中输入JavaScript,包括导入module的require语句.以下代码显示了如何使用Query String(qs)module: $ node > qs = require('querystring'); { unescapeBuffer: [Function], unescape: [Function], escape: [Function], encode: [Function], stri

《Node学习指南》一1.2 开始Node开发

1.2 开始Node开发 Node学习指南现在你已经安装了Node,是时候开始编写第一个Node应用程序了. 1.2.1 Hello, World in Node 为了测试新的开发环境.语言或者工具,第一个写出来的程序往往是"Hello,World".我们同样也将使用Node创建一个"Hello,World"程序,它仅仅简单的向访问它的用户输出问候语. 示例1-1包含了使用Node创建Hello,World程序需要的全部文本代码. 示例1-1 Node版Hello,

《Node学习指南》一2.2 REPL的优势:更好地理解表层之下的JavaScript

2.2 REPL的优势:更好地理解表层之下的JavaScript Node学习指南下例是一个REPL的典型示范: > 3 > 2 > 1; false 这段代码很好地解释了REPL的工作原理.一眼看上去会认为期望的输出值为true,因为3大于2,2大于1.但是在JavaScript中,表达式是从左到右计算的,每个表达式的返回值作为下一个表达式的一部分进行计算. 以下REPL中的语句可以帮助你更好地理解前端代码: > 3 > 2 > 1; false > 3 >

《Node学习指南》一第2章 Node与REPL 2.1 REPL:先睹为快和未定义的表达式

第2章 Node与REPL Node学习指南尝试使用Node编写自定义的模块或者应用程序时,并不需要每次运行写好的JavaScript文件来测试代码功能.Node有一个交互式组件称为REPL(read-eval-print-loop,读取求值列印循环),这将是本章的主题. REPL(发音为"repple")支持简化的Emacs风格行编辑和一小部分基本命令.在REPL中输入任何内容都与用Node运行JavaScript编写的文件具有相同的处理方式.事实上,可以使用REPL编写整个应用程序

《Node学习指南》一2.4 不可预计的意外—记得经常保存

2.4 不可预计的意外-记得经常保存 Node学习指南Node的REPL是一个便捷的交互式工具,可以使开发任务变得简单点.REPL不仅可以在引入文件之前对JavaScript进行测试,并且可以边编写边测试直到完成时保存代码内容. REPL另一个有用的特性是可以创建自定义的REPL,减少无用的undefined输出,预加载模块以及修改提示符或者eval方法等. 我强烈推荐在REPL中使用rlwrap,可以跨session浏览历史命令.这一特性可以节省大量的时间.话说回来,我们之中谁不喜欢更多更强大

《写给PHP开发者的Node.js学习指南》一2.2 预定义的PHP变量

2.2 预定义的PHP变量 写给PHP开发者的Node.js学习指南 当一个支持PHP的Web服务器执行一个PHP页面时,它并不是仅提供一个未处理的对某个页面的HTTP request,然后执行这个页面.如果它这样做的话,那么每一个PHP页面都需要大量额外的代码来解析原始的HTTP request并且把这些值用更方便的方式存储起来.相反,PHP引擎解码原始的HTTP请求,并将数据填充到一堆众所周知的PHP全局变量中.这些全局变量被正确填充才能保证PHP页面正常工作. 由于我们采用的基本方法是将P

小结Node.js中非阻塞IO和事件循环_node.js

学习和使用Node.js已经有两个月,使用express结合mongoose写了一个web应用和一套RESTful web api,回过头来看Node.js官网首页对Node.js的介绍:Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.那么其中的non-blocking I/O model 意味着什么呢? 非阻塞的IO模型 首先,IO操作无疑是耗时的,当服务器

Node.js 非阻塞IO和事件循环

非阻塞的IO模型 首先,IO操作无疑是耗时的,当服务器端接收到大量请求时,为每一个请求创建进程或线程的同时,也增加了额外的内存开销,也可能浪费更多的时间资源. 由于Node.js是事件驱动的,于是它使用了事件循环来解决IO操作带来的瓶颈问题.在Node.js中,一个IO操作通常会带有一个回调函数,当IO操作完成并返回时,就会调用这个回调函数,而主线程则继续执行接下来的代码.简单的用一个例子来说明这个问题: request('http://www.google.com', function(err