最近工作上碰到个需求,需要自己写一个http服务来做一些逻辑处理,因为这个服务涉及到websocket,node支持比较好,就选择用node来做了。

过程中碰到之前没有深入理解的问题:node如何支持并发?这里略去于重点无关的业务逻辑,着重记录思考与试错的过程:

那就先启动一个最简单的http服务吧,相信很多人学node都写过这样的代码:

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

这段代码运行之后,监听8888端口,这时候在浏览器访问127.0.0.1:8888,就能在页面上输出hello world。

这很简单,但是一般的请求,不会这么平铺秩序,立马得到响应,可能会有复杂的逻辑处理,数据库查询等,于是又可以模拟延迟响应的情况:

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

这样,两秒钟后才返回结果。

但是此时访问页面,和想象中不一样,现象比较复杂,总结了一下三点:

  • 1,如果在浏览器中打开多个标签页请求127.0.0.1:8888,那么他们不是同时需要等待2s,而是逐个等待,上一个请求得到hello world之后,下一个请求才开始等待2s,是顺序执行的。
  • 2,如果在同一标签页发请求,然后不停刷新,停下刷新之后,只需要两秒钟就能得到结果。而如果按照1的观察,同时只能处理一个请求的话,那不停刷新时发出去的请求,肯定还没处理完,为什么只需等待2s就能获得结果呢?显然与1矛盾。
  • 3,如果在两个不同的浏览器同时发请求,也是同时执行的,不是依次执行,也与1矛盾。

这里的疑问主要是:如果按照1所观察的,一次只能处理一个请求,那node如何支持并发呢?2、3两点和1 为什么会矛盾呢?

首先猜测的是,是否http有某种机制,来对某种情况下的请求进行一些特异处理呢(比如多个标签页发出同样一个请求的情况下)?当然只是猜测,无从验证。

然后又想到,是不是connect:keep-alive 这个header属性的问题,这个属性与http连接有关,于是做了一下尝试,设置响应头res.setHeader("connection","close"):

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

很可惜,表现的和之前一样。

实在想不明白,到cnode社区发了一个提问,有一位朋友回复我,setTimeout是非阻塞的,所以2、3的表现是同时执行。看到这个点拨,这才想到setTimeout函数不会阻塞运行(为什么想不到呢!),所以2,3两点表现是没问题的。

于是写了一个真正的阻塞版本来验证:

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

这里,通过不断的while循环来阻塞代码,然后观察网页:

  • 浏览器开多个标签页同时发请求,顺序执行的
  • 一个标签页不断刷新,停下来之后也需要等前面刷新时发出的请求完成之后,才返回结果,也是顺序执行的
  • 两个浏览器发请求,也是之前的请求完成,后一个请求才处理,也是顺序执行的。

很有道理,和观察到的也是相符的,但是仔细回想一下,还是没解决我的疑问:为什么使用setTimeout的情况下,多个标签页会依次等待才能获取响应呢?

这时又有一个朋友提醒了我:这是浏览器的锅!经过搜索得知:
浏览器对相同的请求,会依次发出去!
也就是说,如果有十个标签页同时访问http://127.0.0.1:8888/ 那么,浏览器会等前一个请求获取响应之后,才把请求发出去(为什么要这么做呢?),而不是在node层面对请求逐个执行。经过测试ie11/chrome都是这样的表现。

这时所有的情况就都能解释了!

其实浏览器的这个特性,我在调试的时候“差一点点”就能够独立发现了,我在调试的时候,尝试打印出请求的相关信息:

http.createServer(function (req, res) {
//例如这个打印出请求的url
    console.log(req.url)
    setTimeout(function () {
        res.setHeader("connection","close")
        res.end("hello world")
    },2000)
}).listen(8888)

例如在这里,每次请求进来就打印请求的URL,就发现一个页面反复刷新能及时打印出请求URL。多个页面同时请求却是间隔两秒才能打印出,说明请求是隔两秒进来的,答案简直呼之欲出了,可就是没想到是浏览器的问题。

另一个可能看出端倪的地方:虽然同时发请求时,后面的请求可能等待多时,但是获取响应之后,查看网络请求中的timing选项,发现也只是等了2s,也就是说从浏览器发出到响应只是2s。只可惜当时怎么都没想到可能是浏览器的问题,有点遗憾。

总结

针对这个简单的http服务器的情况,node是支持并发的,一个请求进来,就进行相关的处理,下一个请求进来也即进行处理,只要代码不阻塞,就能同时处理多个请求,如果阻塞了,那就得等待了。

为什么说针对这个简单的http服务器呢?因为我对node的并发模型,单线程等概念还没很深的理解,不敢乱说:)

相关内容:
cnode社区的帖子 https://cnodejs.org/topic/585aa762e4da3ae3630edb93