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
中定义两个路由start
、upload
的处理方式,逻辑也很简单就是简单返回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
是经过路由程序响应之后的响应信息,将信息写入response
(exports
是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 A
和Function B
由于有回调函数A1和B1,那么A和B是异步的,但假如A1和B1是没有回调函数的,那么A1和B1还是一个同步执行状态,相互之间会有阻塞。
以上是关于Node.js中的服务器架构&回调函数的非阻塞式应用的主要内容,如果未能解决你的问题,请参考以下文章
在回调函数之外访问由 node.js 中的 readline 和 fs 解析的数据
node.js + MySQL & JSON-result - 回调问题 & 对客户端无响应