HTTP 事务剖析
本指南旨在让您对 Node.js HTTP 处理的过程有一个扎实的理解。 我们假设您大致了解 HTTP 请求的工作方式,无论使用何种语言或编程环境。 我们还将假设您对 Node.js EventEmitters
和 Streams
有一定的熟悉程度。 如果您不太熟悉它们,建议快速浏览一下它们的 API 文档。
创建服务器
任何 Node Web 服务器应用程序在某个时候都必须创建一个 Web 服务器对象。 这是通过使用 createServer
完成的。
const http = require('node:http');
const server = http.createServer((request, response) => {
// magic happens here!
});
传递给 createServer
的函数,会针对该服务器发出的每个 HTTP 请求调用一次,因此它被称为请求处理程序。 实际上,Server
对象由 createServer
返回,它是一个 EventEmitter
,而我们这里所做的只是创建 server
对象,然后再添加侦听器的简写方式。
const server = http.createServer();
server.on('request', (request, response) => {
// the same kind of magic happens here!
});
当 HTTP 请求到达服务器时,Node 会调用请求处理程序函数,并提供一些方便的对象来处理事务:request
和 response
。 我们稍后会介绍这些。
为了实际处理请求,需要在 server
对象上调用 listen
方法。 在大多数情况下,您只需要将您希望服务器侦听的端口号传递给 listen
。 还有其他一些选项,请查阅 API 参考。
方法、URL 和标头
在处理请求时,您可能首先想要做的是查看方法和 URL,以便采取适当的操作。 Node.js 通过将方便的属性放在 request
对象上,使此过程相对轻松。
const { method, url } = request;
request
对象是IncomingMessage
的一个实例。
此处的 method
将始终是普通的 HTTP 方法/动词。 url
是不包含服务器、协议或端口的完整 URL。 对于典型的 URL,这意味着第三个正斜杠之后(包括第三个正斜杠)的所有内容。
标头也不远。 它们位于 request
上名为 headers
的自己的对象中。
const { headers } = request;
const userAgent = headers['user-agent'];
需要注意的是,所有标头都仅以小写形式表示,无论客户端实际如何发送它们。 这简化了出于任何目的解析标头的任务。
如果某些标头重复,则根据标头,它们的值会被覆盖或连接为逗号分隔的字符串。 在某些情况下,这可能会有问题,因此 rawHeaders
也可用。
请求体
当收到 POST
或 PUT
请求时,请求体可能对您的应用程序很重要。 获取主体数据比访问请求标头稍微复杂一些。 传递给处理程序的 request
对象实现了 ReadableStream
接口。 可以像任何其他流一样监听或通过管道将此流传输到其他位置。 我们可以通过侦听流的 'data'
和 'end'
事件来直接从流中获取数据。
每个 'data'
事件中发出的块是 Buffer
。 如果您知道它将是字符串数据,最好的方法是将数据收集到一个数组中,然后在 'end'
时连接并字符串化它。
let body = [];
request
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
// at this point, `body` has the entire request body stored in it as a string
});
这可能看起来有点乏味,并且在很多情况下确实如此。 幸运的是,
npm
上有一些模块,例如concat-stream
和body
,可以帮助隐藏其中的一些逻辑。 在走这条路之前,务必充分了解正在发生的事情,这就是您来这里的原因!
关于错误的快速说明
由于 request
对象是一个 ReadableStream
,因此它也是一个 EventEmitter
,并且在发生错误时会像一个 EventEmitter 一样运行。
request
流中的错误通过在流上发出 'error'
事件来呈现。 如果您没有该事件的侦听器,则会抛出该错误,这可能会导致您的 Node.js 程序崩溃。 因此,您应该在您的请求流上添加一个 'error'
侦听器,即使您只是记录它并继续前进。 (不过,最好发送某种 HTTP 错误响应。稍后会详细介绍。)
request.on('error', err => {
// This prints the error message and stack trace to `stderr`.
console.error(err.stack);
});
还有其他 处理这些错误 的方法,例如其他抽象和工具,但始终要注意错误可能会并且确实会发生,并且您必须处理它们。
我们目前所拥有的
至此,我们已经介绍了创建服务器,以及从请求中获取方法、URL、标头和正文。 当我们将所有这些放在一起时,它可能看起来像这样
const http = require('node:http');
http
.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request
.on('error', err => {
console.error(err);
})
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
// At this point, we have the headers, method, url and body, and can now
// do whatever we need to in order to respond to this request.
});
})
.listen(8080); // Activates this server, listening on port 8080.
如果我们运行这个示例,我们将能够接收请求,但不能响应它们。 实际上,如果您在 Web 浏览器中访问此示例,您的请求将会超时,因为没有任何内容被发送回客户端。
到目前为止,我们根本没有提到 response
对象,它是 ServerResponse
的一个实例,它是一个 WritableStream
。 它包含许多有用的方法,用于将数据发送回客户端。 我们将在接下来介绍。
HTTP 状态代码
如果您不设置,响应上的 HTTP 状态代码将始终为 200。 当然,并非每个 HTTP 响应都需要这样做,并且在某个时候您肯定希望发送不同的状态代码。 为此,您可以设置 statusCode
属性。
response.statusCode = 404; // Tell the client that the resource wasn't found.
我们将很快看到一些其他的快捷方式。
设置响应标头
标头通过一个名为 setHeader
的便捷方法设置。
response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');
在设置响应上的标头时,其名称的大小写不敏感。 如果您重复设置一个标头,则您设置的最后一个值是发送的值。
显式发送标头数据
我们已经讨论过的设置标头和状态代码的方法假定您正在使用“隐式标头”。 这意味着您依靠节点在您开始发送主体数据之前,在正确的时间为您发送标头。
如果您愿意,您可以显式地将标头写入响应流。 为此,有一个名为 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();
流上的 end
函数也可以接受一些可选数据,作为流上的最后一点数据发送,因此我们可以按如下方式简化上面的示例。
response.end('<html><body><h1>Hello, World!</h1></body></html>');
重要的是在您开始向正文写入数据块之前设置状态和标头。 这是有道理的,因为在 HTTP 响应中,标头位于正文之前。
关于错误的另一个快速说明
response
流也可以发出 'error'
事件,并且在某个时候您也必须处理它。 关于 request
流错误的所有建议仍然适用于此处。
全部放在一起
现在我们已经学习了如何发出 HTTP 响应,让我们将它们组合在一起。 在之前的示例的基础上,我们将创建一个服务器,该服务器将发送用户发送给我们的所有数据。 我们将使用 JSON.stringify
将该数据格式化为 JSON。
const http = require('node:http');
http
.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request
.on('error', err => {
console.error(err);
})
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
// BEGINNING OF NEW STUFF
response.on('error', err => {
console.error(err);
});
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
// Note: the 2 lines above could be replaced with this next one:
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body };
response.write(JSON.stringify(responseBody));
response.end();
// Note: the 2 lines above could be replaced with this next one:
// response.end(JSON.stringify(responseBody))
// END OF NEW STUFF
});
})
.listen(8080);
回显服务器示例
让我们简化前面的示例来创建一个简单的回显服务器,它只是将请求中收到的任何数据直接发送回响应中。 我们需要做的就是从请求流中获取数据,并将该数据写入响应流,类似于我们之前所做的。
const http = require('node:http');
http
.createServer((request, response) => {
let body = [];
request
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
response.end(body);
});
})
.listen(8080);
现在让我们调整一下。 我们希望仅在以下情况下发送回显
- 请求方法是 POST。
- URL 是
/echo
。
在任何其他情况下,我们只想用 404 响应。
const http = require('node:http');
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
let body = [];
request
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
response.end(body);
});
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
通过以这种方式检查 URL,我们正在进行一种“路由”。 其他形式的路由可以像
switch
语句一样简单,也可以像express
这样的整个框架一样复杂。 如果您正在寻找只做路由的东西,请尝试router
。
太棒了! 现在让我们尝试简化它。 请记住,request
对象是一个 ReadableStream
,而 response
对象是一个 WritableStream
。 这意味着我们可以使用 pipe
将数据从一个流定向到另一个流。 这正是我们对回显服务器所需要的!
const http = require('node:http');
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
欢呼流!
不过我们还没有完全完成。 正如本指南中多次提到的,错误可能会发生并且确实会发生,我们需要处理它们。
为了处理请求流上的错误,我们将错误记录到 stderr
并发送 400 状态代码以指示 Bad Request
。 但是,在实际应用中,我们希望检查错误以确定正确的状态代码和消息应该是什么。 与错误一样,您应该查阅 Error
文档。
在响应中,我们将只将错误记录到 stderr
。
const http = require('node:http');
http
.createServer((request, response) => {
request.on('error', err => {
console.error(err);
response.statusCode = 400;
response.end();
});
response.on('error', err => {
console.error(err);
});
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
我们现在已经介绍了处理 HTTP 请求的大部分基础知识。 此时,您应该能够
- 实例化一个带有请求处理函数 HTTP 服务器,并使其在端口上侦听。
- 从
request
对象中获取标头、URL、方法和正文数据。 - 根据
request
对象中的 URL 和/或其他数据做出路由决策。 - 通过
response
对象发送标头、HTTP 状态代码和正文数据。 - 从
request
对象管道数据到response
对象。 - 处理
request
和response
流中的流错误。
从这些基础知识出发,可以构建用于许多典型用例的 Node.js HTTP 服务器。 这些 API 提供了许多其他内容,因此请务必阅读 EventEmitters
、Streams
和 HTTP
的 API 文档。