Node.js中的服务器架构&回调函数的非阻塞式应用

Posted poing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Node.js中的服务器架构&回调函数的非阻塞式应用相关的知识,希望对你有一定的参考价值。

Web架构的理解

以前也有学过一些Web的框架,但其实对一个Web框架的必要组件所完成的功能还是模棱两可的,在这里从零开始写一个用Node.js搭建的服务器架构,并重新理解一下每一个组件完成的功能。

首先要显示一个Web网页,那么就需要假设一个HTTP服务器,在php应用中,这个HTTP服务器一般用Apache或者nginx来架设,Node.js中使用模块http来完成;有了HTTP服务器,然后需要一个路由来处理Web资源的请求路径,即http://127.0.0.1/source_path中的/source_path,注意这里只是一个寻址的过程,并不涉及具体处理逻辑,路由并不涉及处理资源逻辑,例如我需要请求路由/add来计算1+1,路由仅完成处理成/add并交付待计算式1+1,但不会计算出1+1=2;然后是一个“路由处理程序”,这个程序会根据不同的路由规则来完成逻辑处理,并将结果返回来HTTP服务器来做展示(路由也被成为视图,路由处理程序也可以叫视图处理器)。这里就理清了一个Web应用的文件格式如下:

.
├── requestHandlers.js
├── router.js
├── server.js

这里三个都是一个Web应用的构成组件,为了统一性,假如一个index.js文件来做统一管理,HTTP服务器启动和路由应该在index.js申明,最终的文件格式如下

.
├── index.js
├── requestHandlers.js
├── router.js
├── server.js

Node.js服务器

我们采用requestHandlers.js->router.js->server.js->index.js的顺序来书写代码,在requestHandlers.js中定义两个路由startupload的处理方式,逻辑也很简单就是简单返回Hello 路由,那么requestHandlers.js的代码如下

function start() {
    console.log("Request handler ‘start‘ was called.")
    return ‘Hello /start!‘;
}

function upload() {
    console.log("Request handler ‘upload‘ was called.")
    return ‘Hello /upload‘;
}

exports.upload = upload;
exports.start = start;

router.js中要指定路由与“路由处理程序”的对应关系,在这里用映射在存储这种关系

var handle = {};

handle[‘/‘] = requestHandlers.start;
handle[‘/start‘] = requestHandlers.start;
handle[‘/upload‘] = requestHandlers.upload;

如上代码是处理程序和路由路径的对应关系,这部分选择放入到index.js中,统一做申明,在router.js

function route(pathname, handle) {
    console.log(‘About to route a request for ‘ + pathname);
    if (typeof handle[pathname] === ‘function‘){
        var content = handle[pathname]();
        return content;
    }
    else{
        return ‘404 not found!‘;
    }
}

exports.route = route;

判定路由处理方式是否存在,存在则是已定义路由则随后调用处理方法,如果没有定义的话就返回404。

server.js中利用http模块定义生成HTTP服务器,并利用url模块解析出路由路径

var http = require("http");
var url = require(‘url‘);

function start(route, handle) {
    function onRequest(request, response) {
        console.log(‘Start Request‘);
        var path = url.parse(request.url).pathname;
        console.log(‘Request for‘ + path + ‘ received‘);

        var content = route(path, handle);
        response.write(content);
        response.end();
    }

    http.createServer(onRequest).listen(8888);
    console.log(‘Server start‘)
}

exports.start = start;

content是经过路由程序响应之后的响应信息,将信息写入responseexports是Node.js导出模块函数的方法,将整个server的启动包装成一个start方法)。最后是index.js文件

var server = require(‘./server‘);
var router = require(‘./router‘);
var requestHandlers = require(‘./requestHandlers‘);

var handle = {};

handle[‘/‘] = requestHandlers.start;
handle[‘/start‘] = requestHandlers.start;
handle[‘/upload‘] = requestHandlers.upload;

server.start(router.route, handle);

包含了申明路由和路由处理程序的映射关系和HTTP服务器启动,这样就可以启动一个由Node.js驱动的Web服务器。

非阻塞式处理

但是如上的代码有个很大的问题,就是两个路由/start/upload不是并行的关系,即两个路由之间会相互阻塞,假设在requestHandlers.js修改为/start路由会等待10s(由于Node.js没有原生的sleep函数,这里要手写一个)

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!‘;
}

在等待的窗口期内去请求/upload路由,会发现也会被阻塞,即需要等待处理完/start路由以后才会接着处理/upload路由,显然这不是一个Web应用应该有的逻辑,所以这里引入回调函数这么一个概念。回调函数类似于如下这么一段对话

  • 主程序:/start你要等待执行这么久,后面还有那么多请求在等着我转发呢,你能给我一个函数,等你执行完了在处理你的请求么?这样也让后面的请求不用干等着。
  • /start:好的好的,那我给你个callBackFunction吧,等我这边等待完了你就按照这个方法帮我处理下数据就可以了。

回调函数最大的好处就在于可以保证程序的并发性了,不用使一个路由要等另外一个路由处理完才被执行,那这里就仍存在一个问题,假定有如下代码

var exec = require("child_process").exec;

function start() {
    console.log("Request handler ‘start‘ was called.")
    var content = ‘empty‘;
    exec(‘ls -af‘, function (error, stdout, stderr) {
        content = stdout;
        return content;
    });
    return content;
}

通过调用child_process来执行调用命令,按照我们的预期设想变量content原本为empty通过执行ls -af命令以后,应该是会输出当前目录下的一些文件信息,但可惜输出仍然是empty

技术图片

让我们加上一些输出来看一下运行的逻辑

console.log("Start process cmd.")
    exec(‘ls -as‘, function (error, stdout, stderr) {
        content = stdout;
        console.log("Content is: " + content);
        return content;
    });
    console.log("End process cmd.");
    return content;

预期的执行结果应该是

Start process cmd.
Content is: xxxxxxxx
End process cmd.

但实际情况是

Start process cmd.
End process cmd.
Content is: total 40
0 .
0 ..
8 index.js
8 requestHandlers.js
8 router.js
8 server.js

为什么会出现这种情况呢?因为我们的整体代码在执行时还是同步执行的,即调用exec()函数以后,Node.js会立刻执行return content;此时的content还仍然是empty,而且无论exec执行速度有多快速,实际上都会是这个结果,那这样看起来回调函数好像是没有作用的,因为执行exec()肯定是与return content;异步执行的,return content;每次都应该会返回empty,如果要返回exec()的执行结果好似成为了一个不可解决的问题。要解决这个问题实际上只需要重写一个返回包逻辑,因为我们开始的书写逻辑是根据content内容来编写response包,而编写response包代码逻辑是运行在同步执行流上的,而根据我们的预期逻辑,应该是exec()执行完以后再根据执行结果来编写response包,这样的话编写response包也应该放到exec()的回调函数中才能达到预期效果 ,即

var content = ‘empty‘;
    console.log("Start process cmd.")
    exec(‘ls -as‘, function (error, stdout, stderr) {
        content = stdout;
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write(stdout);
        response.end();
        console.log("Content is: " + content);
        return content;
    });
    console.log("End process cmd.");
    return content;

这里记得要将response作为参数传入requestHandlers.js,此时就实现了预期的异步处理逻辑了

技术图片

关于异步和同步处理的思考

因为对Node.js的这个异步处理还不是理解神透彻,这里我又做了一些实验,

假定1

/start已经设置了exec()的回调函数,但在回调函数中又设置等待时间10s,在这个等待时间内请求/upload会发生什么呢?

结果是/upload仍会被这个等待时间阻塞

假定2

/upload中也和/start中设置同样的exec()回调函数和10s等待时间,再先请求/start的情况下再请求/upload

实际情况/upload等待了20s左右才会收到响应内容

假定3

大家知道有些命令执行也会花费一些时间,例如执行find /命令,在我本地也是要执行好几分钟的,那我假如通过这个执行命令来构造时间开销,这种情况下仍会阻塞/upload

代码

exec(‘find /‘, {timeout:10000, maxBuffer: 20000*1024},function (error, stdout, stderr) {
        // sleep(10000);
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write(stdout);
        response.end();
    })

结果是/upload不会被阻塞,直接就返回了,而/start在等待10s后返回结果(因为timeout到时了,实际执行也是要几分钟的)

结论

根据实验,我再次理解Node.js的同步和异步处理关系,如果一个方法没有给定回调函数,那么这个方法的执行就是同步的(方法之间会相互阻塞),给定了回调函数的方法时异步的,不会阻塞其他流程运行,并且要指出的是回调函数的执行也是同步的,即也是相互阻塞的,可以用一个抽象图来理解

├── Function A -> callBackFunction A1        ├── Function A1
├── Function B -> callBackFunction B1        ├── Function B1
├── Function C

Function AFunction B由于有回调函数A1和B1,那么A和B是异步的,但假如A1和B1是没有回调函数的,那么A1和B1还是一个同步执行状态,相互之间会有阻塞。

以上是关于Node.js中的服务器架构&回调函数的非阻塞式应用的主要内容,如果未能解决你的问题,请参考以下文章

在回调函数之外访问由 node.js 中的 readline 和 fs 解析的数据

拓展阅读|理解Node.js事件驱动架构

node.js + MySQL & JSON-result - 回调问题 & 对客户端无响应

Node.js&Promise的新理解&记一次异步编程的错误尝试

Windows下Node.js中的非规范化路径分隔符

我们如何将字符串从回调函数返回到 node.js 中的根函数?