使用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
构造函数中设置了request
和connection
事件的回调函数(重要)。不要忘了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.incoming
是IncomingMessage
的一个实例。而后者继承自Stream.Readable
。Stream(流)是nodejs中的一个非常重要的概念。
// _http_incoming.js
util.inherits(IncomingMessage, Stream.Readable);
所以请求头解析完成后执行parserOnHeadersComplete
函数。然后执行parseOnIncoming
函数(见CS7)。然后server.emit('request', req, res);
。res
是ServerResponse
的一个实例,
/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源码解读