Skip to content

HTTP 事务剖析

本指南旨在深入讲解 Node.js HTTP 处理的过程。我们假设您总体上了解 HTTP 请求的工作方式,无论使用何种语言或编程环境。我们还假设您对 Node.js EventEmitters 和 Streams 有一定的了解。如果您不太熟悉它们,建议快速阅读一下每个 API 文档。

创建服务器

任何 Node.js Web 服务器应用程序最终都必须创建一个 Web 服务器对象。这通过使用 createServer 来完成。

javascript
const http = require('node:http')
const server = http.createServer((request, response) => {
  // 这里发生神奇的事情!
})

传递给 createServer 的函数会针对服务器发出的每个 HTTP 请求调用一次,因此它被称为请求处理程序。事实上,createServer 返回的 Server 对象是一个 EventEmitter,而这里只是创建服务器对象并在稍后添加侦听器的简写方式。

javascript
const server = http.createServer()
server.on('request', (request, response) => {
  // 这里发生同样神奇的事情!
})

当 HTTP 请求到达服务器时,Node.js 会调用请求处理程序函数,并提供一些方便的对象来处理事务,即 request 和 response。我们稍后会详细介绍这些对象。为了实际处理请求,需要在服务器对象上调用 listen 方法。在大多数情况下,您只需要将要监听的端口号传递给 listen 即可。还有一些其他选项,因此请查阅 API 参考文档。

方法、URL 和头部

处理请求时,你可能首先想查看方法和 URL,以便采取适当的操作。Node.js 通过在请求对象上添加便捷的属性使这相对容易。

javascript
const { method, url } = request

请求对象是 IncomingMessage 的一个实例。此处的 method 将始终是正常的 HTTP 方法/动词。url 是完整的 URL,不包括服务器、协议或端口。对于典型的 URL,这意味着从第三个正斜杠开始及之后的所有内容。

头部也不远。它们位于请求对象自身的 headers 对象中。

javascript
const { headers } = request
const userAgent = headers['user-agent']

这里需要注意的是,所有头部都只以小写形式表示,无论客户端实际如何发送它们。这简化了为任何目的解析头部的任务。

如果某些头部重复,则根据头部,其值会被覆盖或连接在一起作为逗号分隔的字符串。在某些情况下,这可能会出现问题,因此 rawHeaders 也可用。

请求体

当接收 POST 或 PUT 请求时,请求体可能对您的应用程序很重要。获取主体数据比访问请求头要复杂一些。传递给处理程序的请求对象实现了 ReadableStream 接口。这个流可以像其他任何流一样被监听或管道传输到其他地方。我们可以通过监听流的 'data''end' 事件来直接从流中获取数据。

每个 'data' 事件中发出的块是一个 Buffer。如果您知道它将是字符串数据,最好的做法是将数据收集到一个数组中,然后在 'end' 事件中,将其连接并转换为字符串。

javascript
let body = []
request.on('data', chunk => {
  body.push(chunk)
})
request.on('end', () => {
  body = Buffer.concat(body).toString()
  // 在这一点上,'body' 已经将整个请求体作为字符串存储在其中
})

注意

这看起来有点繁琐,在许多情况下确实如此。幸运的是,npm 上有 concat-streambody 等模块可以帮助隐藏一些逻辑。在走这条路之前,了解正在发生的事情很重要,这就是你来到这里的原因!

关于错误的简要说明

由于请求对象是一个 ReadableStream,它也是一个 EventEmitter,并在发生错误时像一个 EventEmitter 一样工作。

请求流中的错误通过在流上发出 'error' 事件来表现。如果您没有该事件的监听器,则会抛出错误,这可能会导致您的 Node.js 程序崩溃。因此,您应该在请求流上添加一个 'error' 监听器,即使您只是记录它并继续执行。(尽管最好发送某种 HTTP 错误响应。稍后将详细介绍。)

javascript
request.on('error', err => {
  // 这会将错误消息和堆栈跟踪打印到 stderr。
  console.error(err.stack)
})

还有其他方法可以处理这些错误,例如其他抽象和工具,但始终要注意错误确实会发生,并且您必须处理它们。

到目前为止我们已经做了什么

至此,我们已经介绍了创建服务器以及从请求中获取方法、URL、标头和正文。将所有这些放在一起,它可能看起来像这样:

javascript
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 属性。

javascript
response.statusCode = 404 // 告诉客户端资源未找到。

我们很快就会看到一些其他的快捷方式。

设置响应头

头信息通过一个名为 setHeader 的便捷方法设置。

javascript
response.setHeader('Content-Type', 'application/json')
response.setHeader('X-Powered-By', 'bacon')

在响应中设置头信息时,其名称不区分大小写。如果您重复设置头信息,则您设置的最后一个值是发送的值。

显式发送头数据

我们已经讨论过的设置头信息和状态码的方法假设您使用的是“隐式头信息”。这意味着您依赖 Node 在您开始发送正文数据之前正确的时间为您发送头信息。

如果您愿意,您可以显式地将头信息写入响应流。为此,有一个名为 writeHead 的方法,它将状态码和头信息写入流。

显式发送头部数据

javascript
response.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'bacon',
})

一旦你设置了头部信息(无论是隐式还是显式),你就可以开始发送响应数据了。

发送响应体

由于响应对象是一个 WritableStream,将响应体写入客户端只是使用通常的流方法的问题。

javascript
response.write('<html>')
response.write('<body>')
response.write('<h1>Hello, World!</h1>')
response.write('</body>')
response.write('</html>')
response.end()

流上的 end 函数也可以接收一些可选数据作为流的最后一点数据发送,因此我们可以简化上面的例子如下。

javascript
response.end('<html><body><h1>hello,world!</h1></body></html>')

注意

在开始将数据块写入正文之前,务必设置状态和头部信息。这是有道理的,因为在 HTTP 响应中,头部信息在正文之前。

关于错误的另一个快速说明

响应流也可以发出“error”事件,在某些时候你也必须处理它。所有关于请求流错误的建议在这里仍然适用。

将所有内容整合在一起

现在我们已经学习了如何创建 HTTP 响应,让我们将所有内容整合在一起。基于之前的示例,我们将创建一个服务器,它将发送回用户发送给我们的所有数据。我们将使用 JSON.stringify 将数据格式化为 JSON。

javascript
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 服务器示例

让我们简化之前的示例,创建一个简单的回显服务器,它只是将请求中接收到的任何数据直接发送回响应中。我们只需要从请求流中获取数据并将该数据写入响应流,这与我们之前所做的类似。

javascript
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。

javascript
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 将数据从一个定向到另一个。这正是我们对回显服务器所需要的!

javascript
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

javascript
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 对象。
  • 处理 requestresponse 流中的流错误。

从这些基础知识出发,可以构建许多典型用例的 Node.js HTTP 服务器。这些 API 提供了许多其他内容,因此请务必阅读 EventEmittersStreamsHTTP 的 API 文档。