Node.js的阻塞与非阻塞

Node特性

在非阻塞I/O模型中,请求处理程序会继续执行下一行代码,而不会等待阻塞操作的完成。这意味着,在进行非阻塞I/O操作时,程序会继续响应其他请求,不会因为一个请求的阻塞操作而停滞不前。相反,在阻塞I/O模型中,程序会等待阻塞操作的完成,然后再继续执行后续的代码。这可能导致程序在等待I/O操作完成时无法响应其他请求,造成性能问题。

如果请求处理程序包含阻塞操作(等待10秒),则在执行这个操作期间,该请求处理程序将阻塞,不会响应其他请求。这可能导致在等待期间无法处理其他请求,从而降低了服务器的性能和响应速度。

当一个请求处理程序执行一个阻塞操作时,它会等待该操作完成,直到得到结果或满足某个条件。在这个等待期间,该请求处理程序不会响应其他请求。这意味着服务器在等待这个请求完成时无法处理其他的请求。在高并发的环境下,如果有多个请求同时被阻塞,服务器将无法有效地响应其他请求,导致性能下降,响应速度变慢。

这种情况下,服务器的处理能力受到了限制,因为每个请求都需要等待阻塞操作完成,这可能导致系统资源(如CPU和内存)的浪费,而无法充分发挥服务器的性能。

因此,在设计服务器端应用程序时,通常会采用非阻塞的异步I/O模型,以确保服务器在等待I/O操作完成时能够继续处理其他请求,提高系统的并发能力和响应速度。

为了避免阻塞操作造成的问题,通常会使用异步非阻塞的编程模型,例如Node.js中常用的事件驱动和回调函数机制,以确保在等待I/O操作完成时,程序能够继续响应其他请求,提高服务器的并发性能。

对于如下代码

javascript 复制代码
function start() {
  console.log("Request handler 'start' was called.");
  function sleep(milliSeconds) {
    var startTime = new Date().getTime();
    while (new Date().getTime() < startTime + milliSeconds);
  }
  sleep(10000);
  return "Hello Start";
}
function upload() {
  console.log("Request handler 'upload' was called.");
  return "Hello Upload";
}
exports.start = start;
exports.upload = upload;

start 函数:

当请求处理程序 start 被调用时,它会输出一条日志消息 "Request handler 'start' was called."。

然后,它调用了一个名为 sleep 的函数,该函数模拟了一个阻塞操作,使程序等待10秒钟(10000毫秒)。

在 sleep 函数中,startTime 记录了操作开始的时间,然后使用一个循环来检查当前时间是否超过 startTime + milliSeconds,如果没有超过,则程序一直在循环中等待,即实现了一个简单的等待操作。

当等待时间结束后,start 函数返回字符串 "Hello Start"。

upload 函数:

当请求处理程序 upload 被调用时,它会输出一条日志消息 "Request handler 'upload' was called."。

然后,它直接返回字符串 "Hello Upload"。

这两个函数的目的是模拟请求处理程序的行为。在实际应用中,sleep 函数的使用会导致阻塞,而在等待期间,服务器将无法处理其他请求。这种设计会导致性能问题,因为在等待期间,服务器资源被浪费,无法响应其他请求。在真实的服务器应用中,通常会使用非阻塞的异步I/O操作,以确保在等待I/O操作完成时,服务器能够继续处理其他请求,提高系统的并发能力和响应速度。

在这里讨论一下Node.js的单线程特性和事件驱动模型。在Node.js中,所有的I/O操作(包括文件读写、网络请求等)都是异步和非阻塞的,意味着当一个I/O操作被触发时,Node.js不会等待其完成,而是继续执行后续的代码。这种异步的特性使得Node.js能够在单线程的情况下处理大量的并发请求,提高了系统的吞吐量和性能。

也就是说当在Node.js中发起一个需要时间的操作(比如从磁盘读取文件或者从网络获取数据),Node.js不会停下来等待这个操作完成。在传统的同步编程中,程序会一直等待这个操作完成,然后才会执行下一步。但是在Node.js中,当这个耗时的操作被触发后,Node.js会立即转而执行后续的代码,而不会等待这个操作的结果。

为了处理这个异步操作的结果,你可以提供一个回调函数。当这个异步操作完成时,Node.js会调用你提供的回调函数,将操作的结果传递给它。这样,在等待耗时的操作时,Node.js可以继续执行其他任务,比如处理其他请求,而不会被阻塞住。

Node.js的事件驱动模型是基于事件循环(event loop)实现的。事件循环不断地检查事件队列中是否有待处理的事件,如果有,就会触发相应的回调函数进行处理。这种机制确保了在等待I/O操作的同时,Node.js能够继续执行其他任务,而不会阻塞整个应用程序。

事件循环是一个持续运行的过程,它不断地检查事件队列(event queue)中是否有待处理的事件。当一个事件发生时,例如用户请求到达服务器,Node.js会将事件放入事件队列。事件循环会不断地检查队列中是否有事件,如果有,就会触发相应的回调函数进行处理。

这种事件驱动模型的关键在于异步操作和回调函数。当Node.js执行某个异步操作(比如从磁盘读取文件或者从网络获取数据)时,它会继续执行后续的代码而不等待操作完成。一旦异步操作完成,Node.js会将对应的回调函数放入事件队列中。事件循环会在适当的时机触发这些回调函数,处理异步操作的结果。

这种机制确保了在等待I/O操作的同时,Node.js能够继续执行其他任务,而不会阻塞整个应用程序。这样,Node.js能够高效地处理大量的并发请求,提高了系统的吞吐量和性能。因此,Node.js适用于构建高性能、高并发的网络应用。

因此,开发者应该避免阻塞操作,尽量使用异步的、非阻塞的方式处理I/O操作,以充分利用Node.js的并发处理能力,确保应用程序的响应速度和性能。

常见的错误非阻塞代码

下列代码

ini 复制代码
var exec = require("child_process").exec;
function start() {
  console.log("Request handler 'start' was called.");
  var content = "empty";
  exec("ls -lah", function (error, stdout, stderr) {
    content = stdout;
  });
  return content;
}
function upload() {
  console.log("Request handler 'upload' was called.");
  return "Hello Upload";
}
exports.start = start;
exports.upload = upload;

在这个错误案例中,start函数使用了Node.js的child_process模块的exec方法来执行系统命令ls -lah,并试图将命令的输出作为content返回。然而,exec方法是异步执行的,它不会等待命令执行完成,而是立即返回 。因此,content在exec方法执行完之前就被返回了,此时content的值仍然是"empty"。

exec方法是一个异步执行的函数,它用于执行系统命令。异步意味着该方法会在后台执行,不会阻塞主程序的执行。在JavaScript中,异步操作通常是通过回调函数来处理的。

当执行exec方法时,它会开始执行系统命令,但是并不会等待命令执行完成。相反,它会立即返回,继续执行下面的代码,不等待命令的执行结果。在这个例子中,exec方法被调用,然后立即返回,接着执行return content;语句。由于exec方法还没有执行完成,content的值仍然是初始赋值的"empty"。

由于exec方法的回调函数是在命令执行完成后才被调用,所以在回调函数内部给content赋值的操作发生在函数返回之后,不会对content的返回值产生影响。换句话说,在start函数内部,content被赋值为"empty",然后exec方法开始执行,但是exec方法是异步的,它不会阻塞后续的代码执行,所以start函数会立即返回"empty",而不是命令的实际输出。

因此,无论exec方法执行的命令返回什么内容,start函数都会返回"empty"。这种写法并不能达到预期的效果,因为content的赋值操作在异步回调函数内部,而start函数会在异步操作完成前就返回,无法得到异步操作的结果。

正确的做法是将需要在exec回调函数内部处理的逻辑放入回调函数内部,或者使用Promise、async/await等异步编程方式来处理异步操作,确保在异步操作完成后再进行后续的处理。

正确处理

c 复制代码
function (error, stdout, stderr) {
content = stdout;
}

传入回调函数的目的是在exec方法执行完成后,将获取到的结果传递给回调函数进行处理。回调函数会在异步操作完成后被调用,这样你就可以在回调函数内部处理命令执行的结果,而不是在异步操作外部尝试获取结果。

在这个例子中,传入了一个回调函数,该函数有三个参数:error,stdout和stderr。这些参数分别代表了异步操作的错误信息、命令的标准输出和标准错误输出。

在回调函数内部,你将stdout的值赋给了content,这样当异步操作完成后,content的值会被更新为命令的标准输出。这样就确保了在异步操作完成后,content包含了正确的值。这种做法避免了在异步操作外部尝试获取结果,因为在异步操作外部,结果很可能还没有准备好。通过使用回调函数,你可以确保在异步操作完成后再处理结果,保证了代码的正确性。

非阻塞请求响应

ini 复制代码
var http = require("http");
var url = require("url");
function start(route, handle) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");
    route(handle, pathname, response);
  }
  http.createServer(onRequest).listen(8888);
  console.log("Server has started.");
}
exports.start = start;

var http = require("http");:这行代码引入了Node.js的http模块,使我们能够创建HTTP服务器。

var url = require("url");:这行代码引入了Node.js的url模块,该模块提供了处理URL的方法,用于解析URL中的路径名等信息。

function start(route, handle) { ... }:这是一个名为start的函数,接受两个参数route和handle。这两个参数是函数的回调函数,用于处理请求路由和请求处理。

function onRequest(request, response) { ... }:这是一个内部函数onRequest,当有请求进来时会被调用。它接受request和response两个参数,分别代表请求和响应对象。

var pathname = url.parse(request.url).pathname;:这行代码解析了请求的URL,提取了其中的路径名,存储在pathname变量中。例如,如果请求的URL是http://localhost:8888/some/path,那么pathname将是/some/path。

route(handle, pathname, response);:这行代码调用了传入的route函数,传递了handle、pathname和response三个参数。route函数通常用于根据路径名来选择合适的请求处理程序。

http.createServer(onRequest).listen(8888);:这行代码创建了一个HTTP服务器,并将onRequest函数作为请求处理函数传递给createServer方法。然后,服务器开始监听8888端口。

console.log("Server has started.");:这行代码在服务器启动时打印一条消息到控制台,用于指示服务器已经启动。

exports.start = start;:这行代码将start函数导出,使其可以在其他文件中被引用和使用。

response对象传递给请求处理程序,使得处理程序可以直接使用该对象上的函数来对请求作出响应。

在Node.js中,response对象是HTTP服务器响应的表示。当服务器收到请求并创建了response对象后,它可以通过该对象发送响应给客户端。通常,在请求处理程序中,我们会将response对象作为参数传递,这样请求处理程序就可以通过该对象发送响应数据。

scss 复制代码
// 服务器模块
var http = require("http");

function start(route, handle) {
    function onRequest(request, response) {
        // 将response对象传递给请求路由和处理程序
        route(handle, request, response);
    }

    http.createServer(onRequest).listen(8888);
    console.log("Server has started.");
}

exports.start = start;

response对象在onRequest函数内部被传递给了route函数,然后传递到了具体的请求处理程序。在请求处理程序内部,你可以使用response对象的函数(例如response.writeHead()和response.end())来发送响应给客户端。

这种做法的好处在于,它使得处理程序可以更灵活地控制响应的生成和发送,而不需要在服务器内部直接处理响应。这种分离的设计模式使得代码更易于维护和扩展。

相关推荐
sunshine6419 分钟前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻32 分钟前
Vue(四)
前端·javascript·vue.js
蜜獾云34 分钟前
npm淘宝镜像
前端·npm·node.js
dz88i835 分钟前
修改npm镜像源
前端·npm·node.js
Jiaberrr39 分钟前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook
顾平安2 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网2 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工2 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染
不是鱼2 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
沈剑心2 小时前
如何在鸿蒙系统上实现「沉浸式」页面?
前端·harmonyos