从源码角度分析nodejs如何处理一个HTTP请求

本文从源码层面探讨Node.js处理HTTP请求的细节,包括createServer、listen方法的工作原理。分析了req/res的来源、头部信息创建、connection事件触发、http_parser解析过程及请求事件的触发。阐述了Node.js如何封装复杂流程,使其对外显得简单。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用nodejs创建一个http服务器是非常简单的。突然想到同学的一句话,“经济基础决定上层建筑”。这句话似乎是个万金油,在任何事情上都可以评论一二。所以这种简单其实是因为nodejs在内部帮我们封装了很多,隐藏了非常多的细节。本文旨在深入到这些细节,拨开这个面纱。

为了行文方便,每段代码都加了标志CS

开始

CS1

var http = require('http');
http.createServer(function(req, res) {
  res.end('ok');
}).listen(3000);

创建一个http服务器就这么简单。http模块是nodejs内置的模块。这段代码执行了两个函数,分别是http.createServer.listen。那么问题来了。

  • 问题1:req/res是从哪里来的
  • 问题2:相应头部信息是如何创建的
  • 问题3:listen函数发生了什么

带着这些问题来看源码到底发生了什么。我看的源码是https://github.com/nodejs/node/tree/master/lib

createServer

CS2

/lib/http.js

const server = require('_http_server');

const Server = server.Server;
const ClientRequest = client.ClientRequest;

function createServer(requestListener) {
  return new Server(requestListener);
}

这里的requestListener就是我们代码中传入的回调函数。

function(req, res) {
  res.end('ok');
}

createServer函数返回的是Server的一个实例。所以需要到_http_server文件中去找Server这个构造函数。

CS3

/lib/_http_server.js

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

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

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

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

可以看到,Server借用了net.Server的构造函数。这是由于Server继承自net.Server。后者的构造函数设置了一些属性并继承自EventEmitter。所以可以使用emit/on等。另外,从nodejs官方文档看到net.Server可以创建一个TCP或者IPC服务器。

Server构造函数中设置了requestconnection事件的回调函数(重要)。不要忘了requestListener哦,这是我们在createServer中设置的回调。然后还有个connectionListener。这是触发connection事件时的回调。

  • 问题4:什么时候触发connection事件

CS4

/lib/_http_server.js

function connectionListener(socket) {
  debug('SERVER new http connection');

  httpSocketSetup(socket);

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

  var state = {
    // ...
  };

  parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);

  parser[kOnExecute] =
    onParserExecute.bind(undefined, this, socket, parser, state);
  // ...
}

这里需要关注的是parser这个对象和parserOnIncoming函数。后者使用了bind,并预先传入了三个参数(parser, socket, state)parser来自于parsers.alloc()。parsers来自于_http_common.js文件。

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

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

CS5

/lib/_http_common.js

var parsers = new FreeList('parsers', 1000, function() {
  var parser = new HTTPParser(HTTPParser.REQUEST);

  parser._headers = [];
  parser._url = '';
  parser._consumed = false;

  parser.socket = null;
  parser.incoming = null;
  parser.outgoing = null;

  // Only called in the slow case where slow means
  // that the request headers were either fragmented
  // across multiple TCP packets or too large to be
  // processed in a single run. This method is also
  // called to process trailing HTTP headers.
  parser[kOnHeaders] = parserOnHeaders; // 也就说这个函数不总是触发
  parser[kOnHeadersComplete] = parserOnHeadersComplete;
  parser[kOnBody] = parserOnBody;
  parser[kOnMessageComplete] = parserOnMessageComplete;
  parser[kOnExecute] = null;

  return parser;
});

最主要的就是parserOnHeadersComplete,请求头解析完成后会触发这个函数。parserOnMessageComplete是接收body完成后触发。最终 会触发end事件。表明数据接收完成。

http默认创建了1000个http_parser实例。每当有http请求请求连接成功后,都会从数组中取出一个http_parser分配给当前socket。1000个分配完,则会创建新的。

c/c++模块解析完请求头会执行回调函数parserOnHeadersComplete, 当不是udp类型的请求时,执行onIncoming,触发request事件。

CS6

/lib/internal/freelist.js

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

  alloc() {
    return this.list.length ?
      this.list.pop() :
      this.ctor.apply(this, arguments);
  }

  free(obj) {
    if (this.list.length < this.max) {
      this.list.push(obj);
      return true;
    }
    return false;
  }
}

再回到CS4部分,在connectionListener函数中设置了

parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);

再来查看parserOnIncoming函数。

CS7

/lib/_http_server.js

function parserOnIncoming(server, socket, state, req, keepAlive) {
  resetSocketTimeout(server, socket, state);

  var res = new ServerResponse(req);

  if (socket._httpMessage) { // 这里如果为真,说明有其他res在占用socket
    // There are already pending outgoing res, append.
    state.outgoing.push(res);
  } else {
    res.assignSocket(socket);
  }


    server.emit('request', req, res);

  return false; // Not a HEAD response. (Not even a response!)
}
  • 问题5:parserOnIncoming函数中的第四第五个参数req和keepAlive是什么?

这里触发了请求事件。这也很好理解,上文讲到parserOnIncoming这个函数用来处理具体解析完毕的请求。这说明之前的步骤(解析请求头和请求体)已经完成了。那么,

  • 问题6:req和res参数是什么。

问题越来越多,然而还是要继续看源码。在parserOnHeadersComplete函数中有一行很重要的代码。

CS8

function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
                                 url, statusCode, statusMessage, upgrade,
                                 shouldKeepAlive) {
  var parser = this;

  parser.incoming = new IncomingMessage(parser.socket);
  // ...

  var skipBody = 0; // response to HEAD or CONNECT

  if (!upgrade) {
    // For upgraded connections and CONNECT method request, we'll emit this
    // after parser.execute so that we can capture the first part of the new
    // protocol.
    skipBody = parser.onIncoming(parser.incoming, shouldKeepAlive);
  }

  // ...
}

上面提到,在请求头解析完成后执行parserOnHeadersComplete函数。然后执行parser.onIncoming函数。在CS4中,有这样一行代码:

parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);

所以parser.onIncoming的函数体和parserOnIncoming的函数体是一行的,但是预先传入了三个参数。这里再传入两个参数(parser.incoming, shouldKeepAlive)。也就是req, res。这里parser.incomingIncomingMessage的一个实例。而后者继承自Stream.Readable。Stream(流)是nodejs中的一个非常重要的概念。

// _http_incoming.js

util.inherits(IncomingMessage, Stream.Readable);

所以请求头解析完成后执行parserOnHeadersComplete函数。然后执行parseOnIncoming函数(见CS7)。然后server.emit('request', req, res);resServerResponse的一个实例,

/lib/_http_server.js
util.inherits(ServerResponse, OutgoingMessage);

ServerResponse继承自OutgoingMessage。后者继承自Stream

/lib/_http_outgoing.js

util.inherits(OutgoingMessage, Stream);

CS3创建Server的过程中,request事件绑定了回调函数。

function(req, res) {
  res.end('ok');
}

所以会执行这个函数。

http_parser解析完header之后,就会触发request事件,那body数据放到哪里呢。其实body数据会一直放到流里面,直到用户使用data事件接收数据。

也就是说,触发request的时候,body并不会被解析。

触发request事件的时候,传入req,res参数。回调函数被执行。body并不会被解析。并且这里简单的请求也没有传输数据,所以不会执行

parser[kOnBody] = parserOnBody; 
parser[kOnExecute] = null; 

回调函数中会触发res.end事件。我用断点跑过后得出结论,parserOnMessageComplete的执行是在回调(res.end)之后的,而不是参考文章说的res.end之后执行parserOnMessageComplete

其中res.end又是一个非常复杂的过程,底层做了很多的封装。

OutgoingMessage.prototype.end(chunk, encoding, callback) 
  -> write_(this, chunk, encoding, null, true);
    // 实现响应头信息
    -> msg._implicitHeader(); // msg是write_的第一个参数this,也就是res
      -> ServerResponse.prototype.writeHead // "HTTP/1.1 200 OK"
        -> OutgoingMessage.prototype._storeHeader // 补充其他,如"Date、Connection、Content-Length"
    -> msg._send(chunk, encoding, callback); 
      -> OutgoingMessage.prototype._send(data, encoding, callback)
        -> OutgoingMessage.prototype._writeRaw(data, encoding, callback)
          -> conn.write(data, encoding, callback); // conn是socket
            -> Socket.prototype.write(chunk, encoding, cb)
              -> stream.Duplex.prototype.write.apply(this, arguments);
                -> Writable.prototype.write(chunk, encoding, cb)
                 -> writeOrBuffer(this, state, isBuf, chunk, encoding, cb); // state是WritableState

// ...........

我修改了我的Demo代码如下:

var http = require('http');
http.createServer(function(req, res) {
  res.writeHead(200, {
    'content-type': 'text/plain'
  })
  var str = '';
  req.on('data', function(chunk) {
    str += chunk;
  })
  req.on('end', () => console.log(str.toString()));
  res.write('hello world');
  res.end('ok');
}).listen(3000);

然后刷新页面。

①parserOnHeaders
②parserOnHeadersComplete
③parserOnBody
④parserOnMessageComplete
⑤onParserExecute

这五个函数执行的顺序依次是② -> req.on(‘data’, fn)-> ④ -> ⑤。
然后我在浏览器控制台运行以下代码:

fetch('/abc', {
  method: 'post',
  header: {
    'content-type': 'application/json'
  },
  body: JSON.stringify({name: 'real'})
})

这五个函数的执行顺序依次是② -> req.on(‘data’, fn) -> ③ -> ④ -> ⑤,然后nodejs控制台打印{"name":"real"}

可以看到,都没有执行①。源码中①给的说明是:

// Only called in the slow case where slow means
// that the request headers were either fragmented
// across multiple TCP packets or too large to be
// processed in a single run. This method is also
// called to process trailing HTTP headers.

翻译一下就是parserOnHeaders这个函数只在缓慢的(slow)条件下执行。缓慢(slow)意味着请求头或者被多个tcp分段或者一次请求无法处理。这个方法也处理随后的HTTP头(??)。所有①不执行是合理的。然而,当我设置了请求体为2400+字节时(超过MSS),①还是不执行。耐人寻味。
这里写图片描述

最后,以上所有的讨论都是基于connection事件触发的前提下。问题4:什么时候触发connection事件这里已经提出问题了。使用倒推法。http.createServer其中调用了net.Server的构造函数,返回的是net.Server的一个实例。

listen

net.Server中搜索.emit('connection(因为net.Server继承自eventEmitter)。

/lib/net.js

function onconnection(err, clientHandle) {
  // ...
  self.emit('connection', socket);
}

继续查找onconnection()

/lib/net.js

function setupListenHandle(address, port, addressType, backlog, fd) {
  // ...
  this._handle.onconnection = onconnection;
  // ...
}

Server.prototype._listen2 = setupListenHandle;  // legacy alias

继续查找_listen2

/lib/net.js

function listenInCluster(server, address, port, addressType,
                         backlog, fd, exclusive) {
  // ...
  if (cluster.isMaster || exclusive) {
    server._listen2(address, port, addressType, backlog, fd);
    return;
  }
  // ...

这里调用了server._listen2。继续查找listenInCluster

Server.prototype.listen = function(...args) {
  // ...
  options = options._handle || options.handle || options;
  // (handle[, backlog][, cb]) where handle is an object with a handle
  if (options instanceof TCP) {
    this._handle = options;
    this[async_id_symbol] = this._handle.getAsyncId();
    listenInCluster(this, null, -1, -1, backlogFromArgs);
    return this;
  }
  // ...
}

果然,在.listen函数中调用了listenInCluster。这是倒推法,从下往上看就是.listen方法最终.emit('connection', socket);触发了connection事件。所以才有上文的故事。这里令我不是很理解的一点是setupListenHandle函数中,只是设置了

this._handle.onconnection = onconnection;

但是不知道在某个地方执行了onconnection才触发了connection事件(或许是在nextTick?)。

总结

nodejs的一个简单的http服务器其实现过程竟然如此复杂。

参考

通过源码解析 Node.js 中一个 HTTP 请求到响应的历程
Node.js源码解析–http.js模块
Node.js v8.5.0 Documentation
node.js——http源码解读

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值