HTTP 事务剖析

本指南旨在让您对 Node.js HTTP 处理的过程有一个扎实的理解。 我们假设您大致了解 HTTP 请求的工作方式,无论使用何种语言或编程环境。 我们还将假设您对 Node.js EventEmittersStreams 有一定的熟悉程度。 如果您不太熟悉它们,建议快速浏览一下它们的 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 会调用请求处理程序函数,并提供一些方便的对象来处理事务:requestresponse。 我们稍后会介绍这些。

为了实际处理请求,需要在 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 也可用。

请求体

当收到 POSTPUT 请求时,请求体可能对您的应用程序很重要。 获取主体数据比访问请求标头稍微复杂一些。 传递给处理程序的 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-streambody,可以帮助隐藏其中的一些逻辑。 在走这条路之前,务必充分了解正在发生的事情,这就是您来这里的原因!

关于错误的快速说明

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

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