Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

通过源码解析 Node.js 中一个 HTTP 请求到响应的历程 #29

Open
DavidCai1111 opened this issue May 1, 2016 · 6 comments
Open
Labels

Comments

@DavidCai1111
Copy link
Owner

DavidCai1111 commented May 1, 2016

如果大家使用 Node.js 写过 web 应用,那么你一定使用过 http 模块。在 Node.js 中,起一个 HTTP server 十分简单,短短数行即可:

'use stirct'
const { createServer } = require('http')

createServer(function (req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' })
  res.end('Hello World\n')
})
.listen(3000, function () { console.log('Listening on port 3000') })
$ curl localhost:3000
Hello World

就这么简单,因为 Node.js 把许多细节都已在源码中封装好了,主要代码在 lib/_http_*.js 这些文件中,现在就让我们照着上述代码,看看从一个 HTTP 请求的到来直到响应,Node.js 都为我们在源码层做了些什么。

HTTP 请求的来到

在 Node.js 中,若要收到一个 HTTP 请求,首先需要创建一个 http.Server 类的实例,然后监听它的 request 事件。由于 HTTP 协议属于应用层,在下层的传输层通常使用的是 TCP 协议,所以 net.Server 类正是 http.Server 类的父类。具体的 HTTP 相关的部分,是通过监听 net.Server 类实例的 connection 事件封装的:

// lib/_http_server.js
// ...

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true });

  if (requestListener) {
    this.addListener('request', requestListener);
  }

  // ...
  this.addListener('connection', connectionListener);

  // ...
}
util.inherits(Server, net.Server);

这时,则需要一个 HTTP parser 来解析通过 TCP 传输过来的数据:

// lib/_http_server.js
const parsers = common.parsers;
// ...

function connectionListener(socket) {
  // ...
  var parser = parsers.alloc();
  parser.reinitialize(HTTPParser.REQUEST);
  parser.socket = socket;
  socket.parser = parser;
  parser.incoming = null;
  // ...
}

值得一提的是,parser 是从一个“池”中获取的,这个“池”使用了一种叫做 free listwiki)的数据结构,实现很简单,个人觉得是为了尽可能的对 parser 进行重用,并避免了不断调用构造函数的消耗,且设有数量上限(http 模块中为 1000):

// lib/freelist.js
'use strict';

exports.FreeList = function(name, max, constructor) {
  this.name = name;
  this.constructor = constructor;
  this.max = max;
  this.list = [];
};


exports.FreeList.prototype.alloc = function() {
  return this.list.length ? this.list.pop() :
                            this.constructor.apply(this, arguments);
};


exports.FreeList.prototype.free = function(obj) {
  if (this.list.length < this.max) {
    this.list.push(obj);
    return true;
  }
  return false;
};

由于数据是从 TCP 不断推入的,所以这里的 parser 也是基于事件的,很符合 Node.js 的核心思想。使用的是 http-parser 这个库:

// lib/_http_common.js
// ...
const binding = process.binding('http_parser');
const HTTPParser = binding.HTTPParser;
const FreeList = require('internal/freelist').FreeList;
// ...

var parsers = new FreeList('parsers', 1000, function() {
  var parser = new HTTPParser(HTTPParser.REQUEST);
  // ...
  parser[kOnHeaders] = parserOnHeaders;
  parser[kOnHeadersComplete] = parserOnHeadersComplete;
  parser[kOnBody] = parserOnBody;
  parser[kOnMessageComplete] = parserOnMessageComplete;
  parser[kOnExecute] = null;

  return parser;
});
exports.parsers = parsers;

// lib/_http_server.js
// ...

function connectionListener(socket) {
  parser.onIncoming = parserOnIncoming;
}

所以一个完整的 HTTP 请求从接收到完全解析,会挨个经历 parser 上的如下事件监听器:

  1. parserOnHeaders:不断解析推入的请求头数据。
  2. parserOnHeadersComplete:请求头解析完毕,构造 header 对象,为请求体创建 http.IncomingMessage 实例。
  3. parserOnBody:不断解析推入的请求体数据。
  4. parserOnExecute:请求体解析完毕,检查解析是否报错,若报错,直接触发 clientError 事件。若请求为 CONNECT 方法,或带有 Upgrade 头,则直接触发 connectupgrade 事件。
  5. parserOnIncoming:处理具体解析完毕的请求。

所以接下来,我们的关注点自然是 parserOnIncoming 这个监听器,正是这里完成了最终 request 事件的触发,关键步骤代码如下:

// lib/_http_server.js
// ...

function connectionListener(socket) {
  var outgoing = [];
  var incoming = [];
  // ...

  function parserOnIncoming(req, shouldKeepAlive) {
    incoming.push(req);
    // ...
    var res = new ServerResponse(req);

    if (socket._httpMessage) { // 这里判断若为真,则说明 socket 正在被队列中之前的 ServerResponse 实例占用
      outgoing.push(res);
    } else {
      res.assignSocket(socket);
    }

    res.on('finish', resOnFinish);
    function resOnFinish() {
      incoming.shift();
      // ...
      var m = outgoing.shift();
      if (m) {
        m.assignSocket(socket);
      }
    }
    // ...
    self.emit('request', req, res);
  }
}

可以看出,对于同一个 socket 发来的请求,源码中分别维护了两个队列,用于缓冲 IncomingMessage 实例和对应的 ServerResponse 实例。先来的 ServerResponse 实例先占用 socket ,监听其 finish 事件,从各自队列中释放该 ServerResponse 实例和对应的 IncomingMessage 实例。

比较绕,以一个简化的图示来总结这部分逻辑:
3.pic_hd.jpg

响应该 HTTP 请求

到了响应时,事情已经简单许多了,传入的 ServerResponse 已经获取到了 socket。http.ServerResponse 继承于一个内部类 http.OutgoingMessage,当我们调用 ServerResponse#writeHead 时,Node.js 为我们拼凑好了头字符串,并缓存在 ServerResponse 实例内部的 _header 属性中:

// lib/_http_outgoing.js
// ...

OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
  // ...
  if (headers) {
    var keys = Object.keys(headers);
    var isArray = Array.isArray(headers);
    var field, value;

    for (var i = 0, l = keys.length; i < l; i++) {
      var key = keys[i];
      if (isArray) {
        field = headers[key][0];
        value = headers[key][1];
      } else {
        field = key;
        value = headers[key];
      }

      if (Array.isArray(value)) {
        for (var j = 0; j < value.length; j++) {
          storeHeader(this, state, field, value[j]);
        }
      } else {
        storeHeader(this, state, field, value);
      }
    }
  }
  // ...
  this._header = state.messageHeader + CRLF;
}

紧接着在调用 ServerResponse#end 时,将数据拼凑在头字符串后,添加对应的尾部,推入 TCP ,具体的写入操作在内部方法 ServerResponse#_writeRaw 中:

// lib/_http_outgoing.js
// ...

OutgoingMessage.prototype.end = function(data, encoding, callback) {
  // ...
  if (this.connection && data)
    this.connection.cork();

  var ret;
  if (data) {
    this.write(data, encoding);
  }

  if (this._hasBody && this.chunkedEncoding) {
    ret = this._send('0\r\n' + this._trailer + '\r\n', 'binary', finish);
  } else {
    ret = this._send('', 'binary', finish);
  }

  if (this.connection && data)
    this.connection.uncork();

  // ...
  return ret;
}

OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) {
  if (typeof encoding === 'function') {
    callback = encoding;
    encoding = null;
  }

  var connection = this.connection;
  // ...
  return connection.write(data, encoding, callback);
};

最后

到这,一个请求就已经通过 TCP ,发回给客户端了。其实本文中,只涉及到了一条主线进行解析,源码中还考虑了更多的情况,如超时,socket 被占用时的缓存,特殊头,上游突然出现问题,更高效的已写头的查询等等。非常值得一读。

参考:

@fengmk2
Copy link

fengmk2 commented May 2, 2016

收藏。

@fengmk2
Copy link

fengmk2 commented May 2, 2016

图片挂了可以修复一下

@DavidCai1111
Copy link
Owner Author

@fengmk2 发现啦,已修复~

@hyj1991
Copy link

hyj1991 commented Oct 31, 2018

req.timeout 和 res.timeout 分别会用在什么情况下呢?

@atian25
Copy link

atian25 commented Oct 31, 2018

2018 年底发来贺电,写的不错,同问上面那个问题

eggjs/egg#3133

@seeia
Copy link

seeia commented Jan 21, 2019

有个疑问,在触发connection事件,并在socket上添加上data事件,其回调函数中,有这么一句,
var r = parser.execute(d),也就是说,当socket获取TCP推入的数据,触发data事件,然后会执行parser的周期函数。但当我debug的时候,我并没有看见调用socket的data事件,但他的确执行了parser的解析函数。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants