HTTP 事务剖析

本指南旨在让您深入了解 Node.js HTTP 处理的过程。我们假设您对 HTTP 请求的工作原理有一个大致的了解,无论使用何种语言或编程环境。我们还假设您对 Node.js 的 EventEmitters(事件发射器)和 Streams(流)有一定的熟悉。如果您不太熟悉它们,建议快速阅读一下它们的 API 文档。

创建服务器

任何 Node Web 服务器应用程序在某个时刻都需要创建一个 Web 服务器对象。这可以通过使用 createServer 来完成。

const  = ('node:http');

const  = .((, ) => {
  // magic happens here!
});

传递给 createServer 的函数对于每个针对该服务器的 HTTP 请求都会被调用一次,因此它被称为请求处理程序。事实上,createServer 返回的 Server 对象是一个 EventEmitter,我们这里的写法只是创建一个 server 对象然后稍后添加监听器的简写方式。

const  = http.createServer();
.on('request', (, ) => {
  // the same kind of magic happens here!
});

当一个 HTTP 请求到达服务器时,Node 会调用请求处理函数,并提供几个方便处理事务的对象:requestresponse。我们稍后会详细介绍它们。

为了实际处理请求,需要在 server 对象上调用 listen 方法。在大多数情况下,您只需将希望服务器监听的端口号传递给 listen。还有一些其他选项,请查阅 API 参考

方法、URL 和请求头

处理请求时,您可能首先要查看的是请求的方法和 URL,以便采取适当的措施。Node.js 通过在 request 对象上放置一些方便的属性,使这项工作相对轻松。

const { ,  } = request;

request 对象是 IncomingMessage 的一个实例。

这里的 method 始终是一个标准的 HTTP 方法/动词。url 是不包含服务器、协议或端口的完整 URL。对于一个典型的 URL,这意味着第三个斜杠之后的所有内容。

请求头也不难获取。它们位于 request 对象上一个名为 headers 的独立对象中。

const {  } = request;
const  = ['user-agent'];

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

如果某些请求头重复出现,根据具体的请求头,它们的值会被覆盖或以逗号分隔的字符串形式连接起来。在某些情况下,这可能会有问题,因此也提供了 rawHeaders

请求体

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

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

let  = [];
request
  .on('data',  => {
    .();
  })
  .on('end', () => {
     = .().();
    // at this point, `body` has the entire request body stored in it as a string
  });

这可能看起来有点繁琐,而且在很多情况下确实如此。幸运的是,npm 上有像 concat-streambody 这样的模块,可以帮助隐藏部分逻辑。在走这条路之前,对底层原理有很好的理解是很重要的,而这正是您在这里的原因!

关于错误的一点提醒

由于 request 对象是一个 ReadableStream,它也是一个 EventEmitter,并且在发生错误时的行为也像一个事件发射器。

request 流中的错误通过在流上发出 'error' 事件来呈现。如果您没有为该事件设置监听器,错误将被抛出,这可能会导致您的 Node.js 程序崩溃。因此,您应该在您的请求流上添加一个 'error' 监听器,即使您只是记录它然后继续。 (不过,发送某种 HTTP 错误响应可能更好。后面会详细介绍。)

request.on('error',  => {
  // This prints the error message and stack trace to `stderr`.
  .(.stack);
});

还有其他处理这些错误的方法,例如使用其他抽象和工具,但请始终注意,错误会且确实会发生,您将不得不处理它们。

目前我们学到了什么

到目前为止,我们已经介绍了创建服务器,以及从请求中获取方法、URL、请求头和请求体。把这些都放在一起,它可能看起来像这样:

const  = ('node:http');


  .((, ) => {
    const { , ,  } = ;
    let  = [];
    
      .('error',  => {
        .();
      })
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        // 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.
      });
  })
  .(8080); // Activates this server, listening on port 8080.

如果我们运行这个例子,我们将能够接收请求,但无法响应它们。事实上,如果您在网页浏览器中访问这个例子,您的请求将会超时,因为没有任何东西被发送回客户端。

到目前为止,我们还没有接触到 response 对象,它是 ServerResponse 的一个实例,而它又是一个 WritableStream(可写流)。它包含了许多有用的方法,用于向客户端发送数据。我们接下来会介绍这个。

HTTP 状态码

如果您不设置它,响应的 HTTP 状态码将始终是 200。当然,并非每个 HTTP 响应都应该如此,并且在某些时候您肯定会想发送一个不同的状态码。为此,您可以设置 statusCode 属性。

. = 404; // Tell the client that the resource wasn't found.

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

设置响应头

响应头可以通过一个方便的方法 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',
});

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

发送响应体

由于 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  = ('node:http');


  .((, ) => {
    const { , ,  } = ;
    let  = [];
    
      .('error',  => {
        .();
      })
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        // BEGINNING OF NEW STUFF

        .('error',  => {
          .();
        });

        . = 200;
        .('Content-Type', 'application/json');
        // Note: the 2 lines above could be replaced with this next one:
        // response.writeHead(200, {'Content-Type': 'application/json'})

        const  = { , , ,  };

        .(.());
        .();
        // Note: the 2 lines above could be replaced with this next one:
        // response.end(JSON.stringify(responseBody))

        // END OF NEW STUFF
      });
  })
  .(8080);

回显服务器示例

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

const  = ('node:http');


  .((, ) => {
    let  = [];
    
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        .();
      });
  })
  .(8080);

现在让我们调整一下。我们希望只在以下条件下发送回显:

  • 请求方法是 POST。
  • URL 是 /echo

在任何其他情况下,我们只想响应一个 404。

const  = ('node:http');


  .((, ) => {
    if (. === 'POST' && . === '/echo') {
      let  = [];
      
        .('data',  => {
          .();
        })
        .('end', () => {
           = .().();
          .();
        });
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

通过这种方式检查 URL,我们实际上在做一种“路由”。其他形式的路由可以像 switch 语句一样简单,也可以像 express 这样的完整框架一样复杂。如果您正在寻找一个只做路由而不做其他事情的工具,可以试试 router

太棒了!现在让我们尝试简化这个。记住,request 对象是一个 ReadableStream,而 response 对象是一个 WritableStream。这意味着我们可以使用 pipe 将数据从一个流导向另一个流。这正是我们回显服务器所需要的!

const  = ('node:http');


  .((, ) => {
    if (. === 'POST' && . === '/echo') {
      .();
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

流真棒!

不过我们还没有完全完成。正如本指南中多次提到的,错误可能并且确实会发生,我们需要处理它们。

为了处理请求流上的错误,我们将错误记录到 stderr 并发送一个 400 状态码来表示一个 Bad Request(错误请求)。然而,在一个真实世界的应用程序中,我们会希望检查错误以确定正确的状态码和消息应该是什么。和往常一样,处理错误时,您应该查阅 Error 文档

对于响应,我们只将错误记录到 stderr

const  = ('node:http');


  .((, ) => {
    .('error',  => {
      .();
      . = 400;
      .();
    });
    .('error',  => {
      .();
    });
    if (. === 'POST' && . === '/echo') {
      .();
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

我们现在已经涵盖了处理 HTTP 请求的大部分基础知识。此时,您应该能够:

  • 使用请求处理函数实例化一个 HTTP 服务器,并让它监听一个端口。
  • request 对象中获取请求头、URL、方法和请求体数据。
  • 基于 URL 和/或 request 对象中的其他数据做出路由决策。
  • 通过 response 对象发送响应头、HTTP 状态码和响应体数据。
  • 将数据从 request 对象管道传输到 response 对象。
  • 处理 requestresponse 流中的流错误。

基于这些基础知识,可以构建适用于许多典型用例的 Node.js HTTP 服务器。这些 API 还提供了许多其他功能,所以请务必通读 EventEmittersStreamsHTTP 的 API 文档。