GET / HTTP/1.1
Host: www.baidu.com
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
HTTP/1.1 200 OK
Server: bfe/1.0.8.18
Date: Thu, 30 Mar 2017 12:28:00 GMT
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Cache-Control: private
Expires: Thu, 30 Mar 2017 12:27:43 GMT
Expect 是一个请求消息头,包含一个期望条件,表示服务器只有在满足此期望条件的情况下才能妥善地处理请求。规范中只规定了一个期望条件,即 Expect: 100-continue
,对此服务器可以做出如下回应:
常见的浏览器不会发送 Expect
消息头,但是其他类型的客户端如 cURL 默认会这么做。目前规范中只规定了 Expect: 100-continue
这一个期望条件。100-continue 握手的目的是允许客户端在发送包含请求体的消息前,判断源服务器是否愿意在客户端发送请求体前接收请求。
在实际开发过程中,需谨慎使用 Expect: 100-continue,因为如果遇到不支持 HTTP/1.1协议的服务器或代理服务器可能会引起问题。
在 Node.js 中为了避免频繁创建和销毁对象,实现了一个通用的 FreeList 机制。在 http 模块中,就利用到了 FreeList 机制,即用来动态管理 HTTPParser 对象:
var parsers = new FreeList('parsers', 1000, function() {
var parser = new HTTPParser(HTTPParser.REQUEST);
//...
}
是不是感觉很高大尚,其实 FreeList 的内部实现很简单,具体如下:
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;
}
}
在处理 HTTP 请求的场景下,当新的请求到来时,我们通过调用 parsers.alloc()
方法来获取 HTTPParser 对象,从而解析 HTTP 请求。当完成 HTTP 解析任务后,我们可以通过调用 parsers.free()
方法来归还 HTTPParser 对象。
在 Node.js 服务器接收到请求时,会利用 HTTPParser 对象来解析请求报文,为了便于开发者使用,Node.js 会基于解析后的请求报文创建 IncomingMessage 对象,IncomingMessage 构造函数(代码片段)如下:
function IncomingMessage(socket) {
Stream.Readable.call(this);
this.socket = socket;
this.connection = socket;
this.httpVersion = null;
this.complete = false;
this.trailers = {};
this.headers = {}; // 解析后的请求头
this.rawHeaders = []; // 原始的头部信息
// request (server) only
this.url = ''; // 请求url地址
this.method = null; // 请求地址
}
util.inherits(IncomingMessage, Stream.Readable);
Http 协议是基于请求和响应,请求对象我们已经介绍了,那么接下来就是响应对象。在 Node.js 中,响应对象是 ServerResponse 类的实例。
function ServerResponse(req) {
OutgoingMessage.call(this);
if (req.method === 'HEAD') this._hasBody = false;
this.sendDate = true;
this._sent100 = false;
this._expect_continue = false;
if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
this.shouldKeepAlive = false;
}
}
util.inherits(ServerResponse, OutgoingMessage);
通过以上代码,我们可以发现 ServerResponse 继承于 OutgoingMessage。在 OutgoingMessage 对象中会包含用于生成响应报文的相关信息,这里就不详细展开,有兴趣的小伙伴可以查看 _http_outgoing.js
文件。
simple_server.js
const http = require("http");
const server = http.createServer((req, res) => {
res.end("Hello Semlinker!");
});
server.listen(3000, () => {
console.log("server listen on 3000");
});
当运行完 node simple_server.js
命令后,你可以通过 http://localhost:3000/
这个 url 地址来访问我们本地的服务器。不出意外的话,你将在打开的页面中看到 “Hello Semlinker!”。
虽然以上的示例很简单,但对于之前没有服务端经验或者刚接触 Node.js 的小伙伴来说,可能会觉得这是一个很神奇的事情。接下来我们来通过以上简单的示例,分析一下 Node.js 的 Http 模块。
显而易见,http.createServer()
方法用来创建服务器,该方法的实现如下:
function createServer(requestListener) {
return new Server(requestListener);
}
在 createServer
函数内部,我们通过调用 Server 构造函数来创建服务器。因此,接下来的重点就是分析 Server 构造函数了,该函数的内部实现如下:
function Server(options, requestListener) {
if (!(this instanceof Server)) return new Server(options, requestListener);
if (typeof options === 'function') {
requestListener = options;
options = {};
} else if (options == null || typeof options === 'object') {
options = util._extend({}, options);
}
net.Server.call(this, { allowHalfOpen: true });
if (requestListener) {
this.on('request', requestListener);
}
this.on('connection', connectionListener);
this.timeout = 2 * 60 * 1000; // 设置超时时间
}
util.inherits(Server, net.Server);
看到 this.on('request',requestListener)
和 this.on('connection',connectionListener)
这两行,不知道小伙伴们有没有想起我们的 EventEmitter。
通过以上源码,目前我们得出了一个结论,在触发 request
事件后,就会调用我们设置的 requestListener 函数,即执行以下代码:
(req, res) => {
res.end("Hello Semlinker!");
}
那么什么时候会触发 request 事件呢?而 connection 事件和 connectionListener 又是什么?带着这些问题,我们来继续学习 Http 模块。
connection 事件,顾名思义用来跟踪网络连接。这里,我们重点来看一下 connectionListener 函数:
function connectionListener(socket) {
defaultTriggerAsyncIdScope(
getOrSetAsyncId(socket), connectionListenerInternal, this, socket
);
}
该函数内竟然还有一个 connectionListenerInternal,那只能继续往下分析了,connectionListenerInternal 函数(代码片段)的内部实现如下:
function connectionListenerInternal(server, socket) {
httpSocketSetup(socket);
if (socket.server === null)
socket.server = server;
if (server.timeout && typeof socket.setTimeout === 'function')
socket.setTimeout(server.timeout);
socket.on('timeout', socketOnTimeout); // 处理超时情况
var parser = parsers.alloc(); // 获取parser对象
parser.reinitialize(HTTPParser.REQUEST);
parser.socket = socket;
socket.parser = parser;
parser.incoming = null;
var state = {
outgoing: [],
incoming: [],
//...
};
parser.onIncoming = parserOnIncoming.bind(undefined, server, socket, state);
}
在 connectionListenerInternal 函数内部,我们终于见到了 “预备知识” 章节中介绍的 parsers 对象(FreeList 实例)。现在是时候来目睹一下 HTTPParser 对象的芳容了:
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;
parser[kOnHeaders] = parserOnHeaders;
parser[kOnHeadersComplete] = parserOnHeadersComplete;
parser[kOnBody] = parserOnBody;
return parser;
});
以 parser 开头的这些对象,都是定义在 _http_common.js
文件中的函数对象。这里我就不罗列出相关的代码了,只对它们的作用做一些简单的总结:
这里需要注意的是,请求报文的解析工作是由 C++ 来完成,内部通过 binding 来实现,具体参考 deps/http_parser
目录。
const { methods, HTTPParser } = process.binding('http_parser');
介绍完 HTTPParser 对象,我们继续回到 connectionListenerInternal 函数中,在最后一行我们设置 parser 对象的 onIncoming 属性为绑定后的 parserOnIncoming 函数,该函数的实现如下(代码片段):
function parserOnIncoming(server, socket, state, req, keepAlive) {
state.incoming.push(req); // 缓冲IncomingMessage实例
var res = new server[kServerResponse](req);
if (socket._httpMessage) {
state.outgoing.push(res); // 缓冲ServerResponse实例
} else {
res.assignSocket(socket);
}
// 判断请求头是否包含expect字段且http协议的版本为1.1
if (req.headers.expect !== undefined &&
(req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) {
// continueExpression: /(?:^|\W)100-continue(?:$|\W)/i
// Expect: 100-continue
if (continueExpression.test(req.headers.expect)) {
res._expect_continue = true;
if (server.listenerCount('checkContinue') > 0) {
server.emit('checkContinue', req, res);
} else {
res.writeContinue();
server.emit('request', req, res);
}
} else if (server.listenerCount('checkExpectation') > 0) {
server.emit('checkExpectation', req, res);
} else {
// HTTP协议中的417Expectation Failed 状态码表示客户端错误,意味着服务器无法满足
// Expect请求消息头中的期望条件。
res.writeHead(417);
res.end();
}
} else {
server.emit('request', req, res);
}
return 0;
}
通过观察上面的代码,我们终于发现了 request
事件的踪迹。在 parserOnIncoming 函数内,我们会基于 req 请求对象创建 ServerResponse 响应对象,在创建响应对象后,会判断请求头是否包含 expect 字段,然后针对不同的条件做出不同的处理。对于之前最早的示例来说,程序会直接走 else
分支,即触发 request
事件,并传递当前的请求对象和响应对象。
现在我们来回顾一下整个流程:
http.createServer()
方法创建 server 对象,该对象创建完后,我们调用 listen()
方法执行监听操作。connection
事件。connection
事件触发后,会执行对应的 connectionListener
回调函数。在函数内部会利用 HTTPParser 对象,对请求报文进行解析。request
事件,并传递当前的请求对象和响应对象。request
事件触发后,就会执行我们设定的 requestListener
函数。其实我们不但可以通过 Node.js 的 Http 模块创建 Http 服务器,也可以利用该模块提供的 request() 或 get() 方法,向其它的 Http 服务器发送 Http 请求。比如:
const http = require("http");
http.get('http://jsonplaceholder.typicode.com/users', (res) => {
console.log(`Got response: ${res.statusCode}`);
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log(JSON.parse(data));
});
}).on('error', (e) => {
console.log(`Got error: ${e.message}`);
});
不过在实际项目中,我们一般会使用其它功能更加完善的第三方 Http 客户端库,比如 Request、Axios 和 SuperAgent 等。
本文基于一个简单的服务器示例,一步一步分析了 Node.js Http 模块中请求对象、响应对象内部的创建过程,此外还介绍了 Server 内部两个重要的事件:connection
与 request
。
在文中我们只分析 request
事件的触发时机,并未介绍 connection
事件的触发时机。此外也没有继续深入分析 server 对象 listen()
方法内部执行流程。