深入探究http模块,关于并发的几个疑问
发布于 12 天前 作者 WuHuaJi0 464 次浏览 来自 问答

相信每个接触过node的人,都写过类似的代码,启动一个最基本的http服务器:

var http = require('http')
http.createServer(function (req, res) {
    res.send('hello world')
}).listen(8888)

想想这样一个场景,通常响应不是即时的,会有一定的阻塞,稍微改造一下上面的代码,延迟2秒返回响应:

http.createServer(function (req, res) {
    setTimeout(function () {
        res.end("hello world")
    },2000)
}).listen(8888)

这样,如果通过浏览器访问127.0.0.1:8888,会发现两秒后页面才打印出hello world,与我们设想的一致。 以上是背景介绍,下面是我发现的几个问题:

  • 如果这个时候,浏览器开多个标签页(假设十个),同时访问这个地址,那这十个不是并发处理的,而是按照先后顺序处理,也就是说第一个标签页得到helloworld之后,第二个标签页才开始处理…依次类推,第十个需要等待20s,而不是2s。 如果按照上面这个情况观察的话,每次只能处理一个请求,那如何并发呢?
  • 但是这时候又发现,同一个标签页不停刷新,则停下刷新之后,只需要等待2s就能获取结果!如果按照上面的观察,那不停刷新时发出去的请求肯定还没处理完,那如何能做到停下刷新2s后就能获取结果呢?
  • 这个时候又发现,如果我打开另一个浏览器b,不管a浏览器的请求是否完成,b浏览器无需等待a的请求是否完成,例如:a有十个标签页正在发请求,则需要20s才能处理完,此都但是此时b浏览器发请求,能两秒就能获取响应,这又是为什么呢?

我又考虑,会不会是和keep-alive响应头有关,毕竟平常也没怎么注意过这个响应头,然后尝试设置connection:close:

http.createServer(function (req, res) {
    setTimeout(function () {
        res.setHeader("connection","close")
        res.end("hello world")
    },2000)
}).listen(8888)

但是问题的表现依然和之前的一样,我觉得应该是有某个我不知道的知识盲点,想不通这个问题,还请指教

16 回复

同学,这个问题说明你对 nodejs 甚至 js 的理解都很不够啊。

先说这个问题,对于以下代码,你觉得执行结果是什么呢

setTimeout(function () {
    console.log("hello world")
},2000);
setTimeout(function () {
    console.log("hello world")
},2000);
setTimeout(function () {
    console.log("hello world")
},2000);

是每隔 2s 输出一个 hello world ,还是 2s 后一起输出 hello world?

答案是后者。

setTimeout 是非阻塞的,然后结合起来想一下你应该懂了。不懂再问。

@sharejishu 多谢指点,我这里使用setTimeout就是想模拟页面阻塞的情况,居然忘记了setTimeout不会阻塞掉(敲脑袋。)

如果这里的代码换成:

http.createServer(function (req, res) {
    var start=new Date().getTime();
    while(true){
        if(new Date().getTime()-start>5000) break;
    }
    res.setHeader("connection","close")
    res.end("hello world")
}).listen(8888)

现在就是阻塞的了,得到的表现就是

  • 同时只处理一个请求,浏览器开多个页面请求,则会依次等待
  • 同一个标签页刷新多次,也会等待之前的请求处理完,才能响应
  • 在两个浏览器同时发请求,也会按先后依次处理。

那么如何才能做到支持并发呢? 我隐约能想到的是,在处理请求中,将请求分发到异步处理函数中:

http.createServer(function (req, res) {
    console.log(req.url)
	//通过deal来处理请求,但是这里是同步的,如何写出一个异步函数来处理呢?
    deal(res)
}).listen(8888)

function deal(response){
    res.setHeader("connection","close")
    res.end("hello world")
}

如代码所示,通过deal来处理请求,但是这里是同步的,如何写出一个异步函数来处理呢?一时绕不过来,还请指点。

@WuHuaJi0 你想要的并发效果具体是怎么样的呢,如果你不阻塞请求的话,本身就是支持并发的。 你是想研究 NodeJS 在读写数据库或者网络延迟很大时的并发效果吗?

NodeJS 是单线程,所以如果你用一个 while 循环就会让 http 模块卡住,没办法支持并发。但是如果是读写数据库或者网络,即使延迟很大,也不会影响并发,因为 NodeJS 的 IO 是异步的,也就是在另外的线程里跑。

我记得有一本js异步模式的书,可以看看

@sharejishu 明白你的意思了,多谢!

@sharejishu 我又仔细回想了一下,你说的settimeout非阻塞有道理,但是并未能解决我所有的困惑。 还是用setTimeout的代码来请教:

http.createServer(function (req, res) {
    setTimeout(function () {
        res.end("hello world")
    },2000)
}).listen(8888)

我这里的困惑是,为什么同一个标签页中反复刷新,最后只需要等待两秒?(这似乎可以解释通,因为非阻塞嘛),但是如果此时是打开多个标签页发请求,则需要依次等待,这两种情况下有什么区别吗?

打开调试工具看网络请求

看起来是浏览器的行为

@sharejishu setTimeout 虽然是异步的,但是人家在回调里执行 res.end("hello world"); 那这个请求就是会阻塞2秒的。

@WuHuaJi0 楼主同学,所以说你的疑问与 setTimout 的异步没有半毛钱关系呀,你咋就恍然大悟了呢?

下面回答你几个疑问:

  • 为什么打开10个标签页给你的感觉是依次执行?

这是浏览器的锅,如果你每个标签页的url后面都加个不同的随机数,如: http://localhost:8888?1 http://localhost:8888?2 http://localhost:8888?3 http://localhost:8888?4 那么你会发现所有的标签页都只需要等待2秒就返回了。 另外,测并发你可以用ab工具,如执行:ab -c 1000 -n 1000 http://localhost:8888/ 你会发现,1000次并发请求,只需2秒就全部处理完了。

  • 为什么快速刷新,停止后只需等待2秒即可返回?

那是因为如果强刷页面会导致前一个还未返回的请求abort,那么就不会有1的情况。

  • 对于第三点疑问,那是因为不是同一家公司生产的浏览器呀。

@WuHuaJi0 @sarike 楼主提出了 3 条疑问,setTimeout 非阻塞是用来解释后 2 点的,关于第一点我上次没注意看,那个确实是浏览器的锅。

@sarike 哈,多谢“拨乱反正”,是这样的,我之所以“恍然大悟”是因为@sharejishu 指出settimeout非阻塞之后,我立马试了一下阻塞的版本,发现这时候观察到的情况和正是页面阻塞掉了,白天要工作也没深究。但晚上回味的时候,觉得不太对,这又提出了疑问。

另外你提到的这是浏览器的锅,请问这是什么情况,浏览器会对相同的请求进行什么处理吗,这一点能否细说一下?

@sharejishu @wong2 你们也提到了,是浏览器的锅,请问这里有什么缘故吗?

@sharejishu @wong2 @sarike 浏览器的锅,经过搜索,我明白了:浏览器会将相同的请求,依次发出(前一个请求响应后,再发出下一个请求)。 这也解决了我另一个困惑:命令行启动服务之后,通过观察,浏览器多个窗口发请求,请求并不是实时进来的,而是依次进来,但是如果是刷新行为,请求则会实时进来。之前不知道这一点,多谢:)

感谢各位的回复,关于这个问题,疑问都清楚了,写了一篇总结,https://whj.site/post/node-concurrency.html 如有错误请指正

@WuHuaJi0 楼主很用心,加油

回到顶部