前言
前两天 node.js 发布了新版本,想看看具体更新了啥,于是去官网找 changelog 看了看,顺便逛了逛其它栏目。没想到,在 DOCS 下的Guides发现了一篇好文,讲的是 node.js 对 http 请求的处理过程,虽然不是很适合初学者,但顺藤摸瓜,能挖掘出许多知识点,串联起来,干货满满。下面是译文,没有逐字逐句翻译,有添油加醋的地方,但不影响原文的表达。
译文
温馨提示
这篇文章目的在于阐释 HTTP 请求在 node.js 中的处理过程。所以前提是假定你知道 HTTP 为何物,并且对 node.js 的EventEmitters和Streams有所了解,否则,最好快速过一下有关的API。
创建服务器
任何一个 node web server 在代码某一处都会通过createServer创建一个 web 服务器对象.
var http = require("http");
var server = http.createServer(function(request, response) {
// 见证奇迹的时刻
});
作为参数传入createServer
的函数是 http 请求必由之路,因此也叫作请求处理函数。事实上,createServer
返回的server
对象是一个EventEmitter
,因此,上面那段代码也可以这么写:
var server = http.createServer();
server.on("request", function(request, response) {
// 见证奇迹的时刻
});
当请求来临时,node.js 会调用请求处理函数,并且封装好了两个常用对象:request和response。稍后我们会经常碰到这两个家伙的。
花开两朵,各表一枝。为了能够接收到 http 请求,还需要调用server
对象的listen
方法。多数情况下,你只需要传给listen
一个端口号。还有一些其他设置,感兴趣的话请参考这里
Method-URL-和-Headers
处理一个请求时,你想知道的第一件事可能就是看一下这个请求的method
和url
,然后才会有相应的处理。node.js 把这两个信息放在了request
对象里了,直接调用即可:
var method = request.method;
var url = request.url;
注:request 对象是 IncommingMessage的一个实例
Headers
也在request
对象里:
var headers = request.headers;
var userAgent = headers["user-agent"];
需要注意的是,无论客户端发送的是什么,node.js 把所有的头信息关键词都小写化了。变单一的同时也就减少了因分歧出错的可能性。还有,如果有重复的头信息,有些会重写,有些会使用,
合并成字符串。在一些场景可能会出现问题,没关系,request
中还有个rawHeaders,你值得拥有。
Request-Body(请求体)
当请求方法是PUT
或者POST
时,请求体就成了重点关注对象。获取请求体,相对于获取上面那三个值,就需要多知道点了:request
对象实现了ReadableStream接口,所以能够被监听或者管道化。因此,我们可以通过监听data
和end
事件来获取流内数据。
data
过来的数据块都是Buffer。如果你清楚的知道传输过来的数据是字符串,那么最好将它们存放在一个数组里,在end
事件中,合并(concatenate)并字符串化(stringify)。
var body = [];
request
.on("data", function(chunk) {
body.push(chunk);
})
.on("end", function() {
body = Buffer.concat(body).toString();
// 代码执行到这里,body就拥有了整个字符串形式的数据了。
});
注:多数情况下,这样做有些啰嗦。幸运的是,npm 上有许多能将这些逻辑隐藏的优秀模块,比如concat-stream和body。即便如此,还是希望能够好好理解一下这个细节,因为这属于基础。
有关错误(Errors)
既然request
是一个EventEmitter
,那么当有错误时,就可以触发error
事件。**如果你没有监听这个事件,错误会被抛出,进而很可能导致 node.js 程序的崩溃。**所以,最佳实践便是给request
增加error
事件,在事件回调函数里面做一下日志记录的同时,最好给客户端返回对应的错误码,这个在后面会提到。
request.on("error", function(err) {
// console的错误标准输出
console.error(err.stack);
});
有关错误的处理,还有其它方式,可以参考这里。记住,错误随时会发生,要对此有所警惕,对其有专门的处理总是好的。
小结一下
走到这里,我们已经创建了一个 web 服务器,获取到了请求的method
,url
和headers
,哦,还有请求体内容。现在我们将这些放在一起:
var http = require("http");
http
.createServer(function(request, response) {
var headers = request.headers;
var method = request.method;
var url = request.url;
var body = [];
request
.on("error", function(err) {
console.error(err);
})
.on("data", function(chunk) {
body.push(chunk);
})
.on("end", function() {
body = Buffer.concat(body).toString();
// 至此,我们就获取到了所有需要响应给客户端的数据
});
})
.listen(8080); // Activates this server, listening on port 8080.
很显然,如果运行这个代码,服务器能接收到请求(request),但没发出响应(response)。也就是说,在浏览器里面发出请求,会超时。
目前为止,我们还未碰触response
对象,它是ServerResponse的一个实例,也是一个WritableStream,为了将数据传回客户端,其中包含了许多实用方法。好吧,依旧是花开两朵,各表一枝,我们先认识下 http 状态码,待会儿再谈response
对象。
HTTP 状态码
response
默认状态码是200
。当然,有些情况下,你需要返回不同的状态码。response
中的statusCode
属性就是为此存在的:
response.statusCode = 404; //告诉客户端资源未找到...
设置响应头
response
中的setHeader
该出场了:
response.setHeader("Content-Type", "application/json");
response.setHeader("X-Powered-By", "bacon");
需要注意的是,响应头关键词对大小写不敏感,如果重复设置一个响应头,那么客户端取到的是你最后一个。
显式发送响应头
上面提到的statusCode
和setHeader
属于隐式头部:意思是在发送 body 数据前,依赖的是 node.js 来发送头部数据。
如果你愿意,也可以显式地将头部信息写到响应流里。writeHead
便是为此而生:
response.writeHead(200, {
"Content-Type": "application/json",
"X-Powered-By": "bacon"
});
设置完头部,接下来便是发送响应数据了。
发送响应数据
既然response
对象是个WritableStream
,那么就可以使用流方法来向客户端写数据了。
response.write("<html>");
response.write("<body>");
response.write("<h1>Hello, World!</h1>");
response.write("</body>");
response.write("</html>");
response.end();
以上代码也可以简写成以下形式:
response.end("<html><body><h1>Hello, World!</h1></body></html>");
注:响应体在响应头之后,因此往 response 里写数据之前就设置好状态码和头信息,一切才会有意义。
Response 的错误处理
与request
一样,response
也会触发error
事件。所以,有关request
错误处理最佳实践,同样也适用于response
。
再来小结一下
目前来讲,我们已经不会让浏览器傻等了。那么,把所有代码放在一起,我们可以做到让服务端把浏览器过来的请求组织下数据再传送过去,注意,使用JSON.stringify
格式化了下数据:
var http = require("http");
http
.createServer(function(request, response) {
var headers = request.headers;
var method = request.method;
var url = request.url;
var body = [];
request
.on("error", function(err) {
console.error(err);
})
.on("data", function(chunk) {
body.push(chunk);
})
.on("end", function() {
body = Buffer.concat(body).toString();
// BEGINNING OF NEW STUFF
response.on("error", function(err) {
console.error(err);
});
response.statusCode = 200;
response.setHeader("Content-Type", "application/json");
// 注:上面两行代码可以用下面一行替换
// response.writeHead(200, {'Content-Type': 'application/json'})
var responseBody = {
headers: headers,
method: method,
url: url,
body: body
};
response.write(JSON.stringify(responseBody));
response.end();
// 注:同样,可以这样替换
// response.end(JSON.stringify(responseBody))
// END OF NEW STUFF
});
})
.listen(8080);
Echo-服务器
基于上面代码,我们可以简化一下,做出一个 Echo 服务器,即请求什么数据,就返回什么数据。我们只需要从请求里面获取数据并写到响应里,和上面代码差不多:
var http = require("http");
http
.createServer(function(request, response) {
var body = [];
request
.on("data", function(chunk) {
body.push(chunk);
})
.on("end", function() {
body = Buffer.concat(body).toString();
response.end(body);
});
})
.listen(8080);
好吧,有些过于简单,我们再增加两个需求,满足下面两个条件才给出正确响应:
- 请求的
method
是GET
- URL 是
/echo
,否则给出404
。
var http = require("http");
http
.createServer(function(request, response) {
if (request.method === "GET" && request.url === "/echo") {
var body = [];
request
.on("data", function(chunk) {
body.push(chunk);
})
.on("end", function() {
body = Buffer.concat(body).toString();
response.end(body);
});
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
注:检查 URL,实质上就是一种路由
routing
形式。其它形式有简单如swtich
语句,复杂如**Express**框架。如果需要纯路由功能,可以试试[Router][https://www.npmjs.com/package/router]。](https://www.npmjs.com/package/router]。)
上面的代码能不能再精简下呢?别忘了,request
对象是一个ReadableStream
,response
对象是一个WritableStream
。这意味着可以使用管道(pipe)直接将数据从一端传到另一端。所以,更为精简的代码诞生了:
var http = require("http");
http
.createServer(function(request, response) {
if (request.method === "GET" && request.url === "/echo") {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
事情还没完,程序出错了怎么办?好吧,加上错误处理机制:在此,我们仅仅打印出错误,并将状态码置为404
。(更为详细的错误处理机制可以参考这里)
var http = require("http");
http
.createServer(function(request, response) {
request.on("error", function(err) {
console.error(err);
response.statusCode = 400;
response.end();
});
response.on("error", function(err) {
console.error(err);
});
if (request.method === "GET" && request.url === "/echo") {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
OK,node.js 如何处理 http 请求,目前为止,我们已经把大部分的基础知识讲解到了。最后,我们总结下这些知识点:
- 实例化一个 HTTP 服务器,并设置一个请求处理函数,另外别忘了监听一个端口
- 从
request
获取headers
,url
,method
,body
等信息 - 根据
url
或者其它信息路由 - 通过
response
发送响应头、状态码和数据 request
数据管道化到response
- 对
request
和response
设置错误处理机制