HTTP 事务剖析
本指南旨在深入讲解 Node.js HTTP 处理的过程。我们假设您总体上了解 HTTP 请求的工作方式,无论使用何种语言或编程环境。我们还假设您对 Node.js EventEmitters 和 Streams 有一定的了解。如果您不太熟悉它们,建议快速阅读一下每个 API 文档。
创建服务器
任何 Node.js Web 服务器应用程序最终都必须创建一个 Web 服务器对象。这通过使用 createServer
来完成。
const http = require('node:http')
const server = http.createServer((request, response) => {
// 这里发生神奇的事情!
})
传递给 createServer
的函数会针对服务器发出的每个 HTTP 请求调用一次,因此它被称为请求处理程序。事实上,createServer
返回的 Server 对象是一个 EventEmitter,而这里只是创建服务器对象并在稍后添加侦听器的简写方式。
const server = http.createServer()
server.on('request', (request, response) => {
// 这里发生同样神奇的事情!
})
当 HTTP 请求到达服务器时,Node.js 会调用请求处理程序函数,并提供一些方便的对象来处理事务,即 request 和 response。我们稍后会详细介绍这些对象。为了实际处理请求,需要在服务器对象上调用 listen
方法。在大多数情况下,您只需要将要监听的端口号传递给 listen
即可。还有一些其他选项,因此请查阅 API 参考文档。
方法、URL 和头部
处理请求时,你可能首先想查看方法和 URL,以便采取适当的操作。Node.js 通过在请求对象上添加便捷的属性使这相对容易。
const { method, url } = request
请求对象是 IncomingMessage
的一个实例。此处的 method 将始终是正常的 HTTP 方法/动词。url 是完整的 URL,不包括服务器、协议或端口。对于典型的 URL,这意味着从第三个正斜杠开始及之后的所有内容。
头部也不远。它们位于请求对象自身的 headers
对象中。
const { headers } = request
const userAgent = headers['user-agent']
这里需要注意的是,所有头部都只以小写形式表示,无论客户端实际如何发送它们。这简化了为任何目的解析头部的任务。
如果某些头部重复,则根据头部,其值会被覆盖或连接在一起作为逗号分隔的字符串。在某些情况下,这可能会出现问题,因此 rawHeaders
也可用。
请求体
当接收 POST 或 PUT 请求时,请求体可能对您的应用程序很重要。获取主体数据比访问请求头要复杂一些。传递给处理程序的请求对象实现了 ReadableStream
接口。这个流可以像其他任何流一样被监听或管道传输到其他地方。我们可以通过监听流的 'data'
和 'end'
事件来直接从流中获取数据。
每个 'data'
事件中发出的块是一个 Buffer
。如果您知道它将是字符串数据,最好的做法是将数据收集到一个数组中,然后在 'end'
事件中,将其连接并转换为字符串。
let body = []
request.on('data', chunk => {
body.push(chunk)
})
request.on('end', () => {
body = Buffer.concat(body).toString()
// 在这一点上,'body' 已经将整个请求体作为字符串存储在其中
})
注意
这看起来有点繁琐,在许多情况下确实如此。幸运的是,npm 上有 concat-stream
和 body
等模块可以帮助隐藏一些逻辑。在走这条路之前,了解正在发生的事情很重要,这就是你来到这里的原因!
关于错误的简要说明
由于请求对象是一个 ReadableStream
,它也是一个 EventEmitter
,并在发生错误时像一个 EventEmitter
一样工作。
请求流中的错误通过在流上发出 'error'
事件来表现。如果您没有该事件的监听器,则会抛出错误,这可能会导致您的 Node.js 程序崩溃。因此,您应该在请求流上添加一个 'error'
监听器,即使您只是记录它并继续执行。(尽管最好发送某种 HTTP 错误响应。稍后将详细介绍。)
request.on('error', err => {
// 这会将错误消息和堆栈跟踪打印到 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));
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
// 在这一点上,我们有了标头、方法、URL 和正文,现在可以做任何我们需要做的事情来响应此请求。
});
});
.listen(8080); // 激活此服务器,监听端口 8080。
如果我们运行此示例,我们将能够接收请求,但无法响应它们。事实上,如果您在 Web 浏览器中访问此示例,您的请求将会超时,因为没有任何内容被发送回客户端。
到目前为止,我们还没有接触到响应对象,它是一个 ServerResponse
的实例,它是一个 WritableStream
。它包含许多用于将数据发送回客户端的有用方法。我们将在下一节中介绍这一点。
HTTP 状态码
如果您不设置它,响应中的 HTTP 状态码将始终为 200。当然,并非每个 HTTP 响应都保证是这个状态码,在某些时候您肯定想要发送不同的状态码。为此,您可以设置 statusCode
属性。
response.statusCode = 404 // 告诉客户端资源未找到。
我们很快就会看到一些其他的快捷方式。
设置响应头
头信息通过一个名为 setHeader
的便捷方法设置。
response.setHeader('Content-Type', 'application/json')
response.setHeader('X-Powered-By', 'bacon')
在响应中设置头信息时,其名称不区分大小写。如果您重复设置头信息,则您设置的最后一个值是发送的值。
显式发送头数据
我们已经讨论过的设置头信息和状态码的方法假设您使用的是“隐式头信息”。这意味着您依赖 Node 在您开始发送正文数据之前正确的时间为您发送头信息。
如果您愿意,您可以显式地将头信息写入响应流。为此,有一个名为 writeHead
的方法,它将状态码和头信息写入流。
显式发送头部数据
response.writeHead(200, {
'Content-Type': 'application/json',
'X-Powered-By': 'bacon',
})
一旦你设置了头部信息(无论是隐式还是显式),你就可以开始发送响应数据了。
发送响应体
由于响应对象是一个 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 响应中,头部信息在正文之前。
关于错误的另一个快速说明
响应流也可以发出“error”事件,在某些时候你也必须处理它。所有关于请求流错误的建议在这里仍然适用。
将所有内容整合在一起
现在我们已经学习了如何创建 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()
// 新内容的开始
response.on('error', err => {
console.error(err)
})
response.statusCode = 200
response.setHeader('Content-Type', 'application/json')
// 注意:上面两行可以用下一行代替:
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body }
response.write(JSON.stringify(responseBody))
response.end()
// 注意:上面两行可以用下一行代替:
// response.end(JSON.stringify(responseBody))
// 新内容的结束
})
})
.listen(8080)
Echo 服务器示例
让我们简化之前的示例,创建一个简单的回显服务器,它只是将请求中接收到的任何数据直接发送回响应中。我们只需要从请求流中获取数据并将该数据写入响应流,这与我们之前所做的类似。
const http = require('node:http');
http.createServer((request, response) => {
let body = [];
request.on('data', chunk => {
body.push(chunk);
});
request.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 状态代码以指示 错误请求
。但是,在实际应用中,我们需要检查错误以确定正确的状态代码和消息是什么。与往常一样,遇到错误时,您应该查阅 错误文档。
对于响应,我们只需将错误记录到 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 请求的大部分基础知识。此时,您应该能够:
- 使用
request
处理程序函数实例化 HTTP 服务器,并使其监听某个端口。 - 从
request
对象中获取标头、URL、方法和正文数据。 - 基于
request
对象中的 URL 和/或其他数据做出路由决策。 - 通过
response
对象发送标头、HTTP 状态代码和正文数据。 - 将数据从
request
对象传输到 response 对象。 - 处理
request
和response
流中的流错误。
从这些基础知识出发,可以构建许多典型用例的 Node.js HTTP 服务器。这些 API 提供了许多其他内容,因此请务必阅读 EventEmitters
、Streams
和 HTTP
的 API 文档。