HTTP 事务的剖析

¥Anatomy of an HTTP Transaction

本指南的目的是让你深入了解 Node.js HTTP 处理的过程。我们假设你大致了解 HTTP 请求的工作原理,无论语言或编程环境如何。我们还假设你对 Node.js EventEmittersStreams 有一点熟悉。如果你对它们不太熟悉,值得快速阅读每个 API 文档。

¥The purpose of this guide is to impart a solid understanding of the process of Node.js HTTP handling. We'll assume that you know, in a general sense, how HTTP requests work, regardless of language or programming environment. We'll also assume a bit of familiarity with Node.js EventEmitters and Streams. If you're not quite familiar with them, it's worth taking a quick read through the API docs for each of those.

创建服务器

¥Create the Server

任何 node Web 服务器应用在某个时候都必须创建一个 Web 服务器对象。这是通过使用 createServer 来实现的。

¥Any node web server application will at some point have to create a web server object. This is done by using createServer.

const http = require('node:http');

const server = http.createServer((request, response) => {
  // magic happens here!
});

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

¥The function that's passed in to createServer is called once for every HTTP request that's made against that server, so it's called the request handler. In fact, the Server object returned by createServer is an EventEmitter, and what we have here is just shorthand for creating a server object and then adding the listener later.

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

当 HTTP 请求到达服务器时,node 会使用一些方便的对象调用请求处理程序函数来处理事务,requestresponse。我们很快就会讲到这些。

¥When an HTTP request hits the server, node calls the request handler function with a few handy objects for dealing with the transaction, request and response. We'll get to those shortly.

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

¥In order to actually serve requests, the listen method needs to be called on the server object. In most cases, all you'll need to pass to listen is the port number you want the server to listen on. There are some other options too, so consult the API reference.

方法、URL 和标头

¥Method, URL and Headers

处理请求时,你可能想要做的第一件事就是查看方法和 URL,以便采取适当的操作。Node.js 通过将方便的属性放到 request 对象上,使这个过程相对轻松。

¥When handling a request, the first thing you'll probably want to do is look at the method and URL, so that appropriate actions can be taken. Node.js makes this relatively painless by putting handy properties onto the request object.

const { method, url } = request;

request 对象是 IncomingMessage 的一个实例。

¥The request object is an instance of IncomingMessage.

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

¥The method here will always be a normal HTTP method/verb. The url is the full URL without the server, protocol or port. For a typical URL, this means everything after and including the third forward slash.

标头也不远。它们位于 request 上名为 headers 的自己的对象中。

¥Headers are also not far away. They're in their own object on request called headers.

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

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

¥It's important to note here that all headers are represented in lower-case only, regardless of how the client actually sent them. This simplifies the task of parsing headers for whatever purpose.

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

¥If some headers are repeated, then their values are overwritten or joined together as comma-separated strings, depending on the header. In some cases, this can be problematic, so rawHeaders is also available.

请求正文

¥Request Body

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

¥When receiving a POST or PUT request, the request body might be important to your application. Getting at the body data is a little more involved than accessing request headers. The request object that's passed in to a handler implements the ReadableStream interface. This stream can be listened to or piped elsewhere just like any other stream. We can grab the data right out of the stream by listening to the stream's 'data' and 'end' events.

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

¥The chunk emitted in each 'data' event is a Buffer. If you know it's going to be string data, the best thing to do is collect the data in an array, then at the 'end', concatenate and stringify it.

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 这样的模块可以帮助隐藏其中的一些逻辑。在走这条路之前,重要的是要很好地了解正在发生的事情,这就是你来这里的原因!

¥This may seem a tad tedious, and in many cases, it is. Luckily, there are modules like concat-stream and body on npm which can help hide away some of this logic. It's important to have a good understanding of what's going on before going down that road, and that's why you're here!

关于错误的简要说明

¥A Quick Thing About Errors

由于 request 对象是 ReadableStream,因此它也是 EventEmitter,并且在发生错误时表现得像 EventEmitter

¥Since the request object is a ReadableStream, it's also an EventEmitter and behaves like one when an error happens.

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

¥An error in the request stream presents itself by emitting an 'error' event on the stream. If you don't have a listener for that event, the error will be thrown, which could crash your Node.js program. You should therefore add an 'error' listener on your request streams, even if you just log it and continue on your way. (Though it's probably best to send some kind of HTTP error response. More on that later.)

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

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

¥There are other ways of handling these errors such as other abstractions and tools, but always be aware that errors can and do happen, and you're going to have to deal with them.

我们目前所获得的信息

¥What We've Got so Far

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

¥At this point, we've covered creating a server, and grabbing the method, URL, headers and body out of requests. When we put that all together, it might look something like this:

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 浏览器中点击此示例,你的请求将超时,因为没有任何内容被发送回客户端。

¥If we run this example, we'll be able to receive requests, but not respond to them. In fact, if you hit this example in a web browser, your request would time out, as nothing is being sent back to the client.

到目前为止,我们还没有触及 response 对象,它是 ServerResponse 的一个实例,它是 WritableStream。它包含许多用于将数据发送回客户端的有用方法。我们接下来会介绍这一点。

¥So far we haven't touched on the response object at all, which is an instance of ServerResponse, which is a WritableStream. It contains many useful methods for sending data back to the client. We'll cover that next.

HTTP 状态代码

¥HTTP Status Code

如果你不费心设置它,响应上的 HTTP 状态代码将始终为 200。当然,并非每个 HTTP 响应都值得这样做,在某些时候你肯定会想要发送不同的状态代码。为此,你可以设置 statusCode 属性。

¥If you don't bother setting it, the HTTP status code on a response will always be 200. Of course, not every HTTP response warrants this, and at some point you'll definitely want to send a different status code. To do that, you can set the statusCode property.

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

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

¥There are some other shortcuts to this, as we'll see soon.

设置响应标头

¥Setting Response Headers

标头通过一种名为 setHeader 的便捷方法设置。

¥Headers are set through a convenient method called setHeader.

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

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

¥When setting the headers on a response, the case is insensitive on their names. If you set a header repeatedly, the last value you set is the value that gets sent.

明确发送标头数据

¥Explicitly Sending Header Data

我们已经讨论过的设置标头和状态代码的方法假设你正在使用 "隐式标头"。这意味着你指望 node 在你开始发送正文数据之前在正确的时间为你发送标头。

¥The methods of setting the headers and status code that we've already discussed assume that you're using "implicit headers". This means you're counting on node to send the headers for you at the correct time before you start sending body data.

如果你愿意,你可以明确地将标头写入响应流。为此,有一个名为 writeHead 的方法,它将状态代码和标头写入流。

¥If you want, you can explicitly write the headers to the response stream. To do this, there's a method called writeHead, which writes the status code and the headers to the stream.

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

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

¥Once you've set the headers (either implicitly or explicitly), you're ready to start sending response data.

发送响应正文

¥Sending Response Body

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

¥Since the response object is a WritableStream, writing a response body out to the client is just a matter of using the usual stream methods.

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

流上的 end 函数还可以接收一些可选数据作为流上的最后一位数据发送,因此我们可以简化上述示例如下。

¥The end function on streams can also take in some optional data to send as the last bit of data on the stream, so we can simplify the example above as follows.

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

在开始将数据块写入正文之前,设置状态和标头非常重要。这是有道理的,因为 HTTP 响应中的标头位于正文之前。

¥It's important to set the status and headers before you start writing chunks of data to the body. This makes sense, since headers come before the body in HTTP responses.

关于错误的另一件事

¥Another Quick Thing About Errors

response 流也可以触发 'error' 事件,在某些时候你也必须处理这个问题。所有针对 request 流错误的建议仍然适用于此处。

¥The response stream can also emit 'error' events, and at some point you're going to have to deal with that as well. All of the advice for request stream errors still applies here.

整合所有内容

¥Put It All Together

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

¥Now that we've learned about making HTTP responses, let's put it all together. Building on the earlier example, we're going to make a server that sends back all of the data that was sent to us by the user. We'll format that data as JSON using JSON.stringify.

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);

Echo 服务器示例

¥Echo Server Example

让我们简化前面的示例,制作一个简单的 echo 服务器,它只会在响应中发送请求中收到的任何数据。我们需要做的就是从请求流中获取数据并将该数据写入响应流,类似于我们之前所做的。

¥Let's simplify the previous example to make a simple echo server, which just sends whatever data is received in the request right back in the response. All we need to do is grab the data from the request stream and write that data to the response stream, similar to what we did previously.

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);

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

¥Now let's tweak this. We want to only send an echo under the following conditions:

  • 请求方法是 POST。

    ¥The request method is POST.

  • URL 是 /echo

    ¥The URL is /echo.

在任何其他情况下,我们只想用 404 进行响应。

¥In any other case, we want to simply respond with a 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,我们正在进行 "routing" 的形式。其他形式的路由可以像 switch 语句一样简单,也可以像 express 这样的整个框架一样复杂。如果你正在寻找只执行路由而不执行其他操作的东西,请尝试 router

¥By checking the URL in this way, we're doing a form of "routing". Other forms of routing can be as simple as switch statements or as complex as whole frameworks like express. If you're looking for something that does routing and nothing else, try router.

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

¥Great! Now let's take a stab at simplifying this. Remember, the request object is a ReadableStream and the response object is a WritableStream. That means we can use pipe to direct data from one to the other. That's exactly what we want for an echo server!

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);

Yay 流!

¥Yay streams!

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

¥We're not quite done yet though. As mentioned multiple times in this guide, errors can and do happen, and we need to deal with them.

为了处理请求流上的错误,我们将错误记录到 stderr 并发送 400 状态代码以指示 Bad Request。然而,在实际应用中,我们希望检查错误以找出正确的状态代码和消息是什么。与错误一样,你应该咨询 Error 文档

¥To handle errors on the request stream, we'll log the error to stderr and send a 400 status code to indicate a Bad Request. In a real-world application, though, we'd want to inspect the error to figure out what the correct status code and message would be. As usual with errors, you should consult the Error documentation.

在响应中,我们只会将错误记录到 stderr

¥On the response, we'll just log the error to 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 请求的大部分基础知识。此时,你应该能够:

¥We've now covered most of the basics of handling HTTP requests. At this point, you should be able to:

  • 使用请求处理程序函数实例化 HTTP 服务器,并让其监听端口。

    ¥Instantiate an HTTP server with a request handler function, and have it listen on a port.

  • request 对象获取标头、URL、方法和正文数据。

    ¥Get headers, URL, method and body data from request objects.

  • 根据 URL 和/或 request 对象中的其他数据做出路由决策。

    ¥Make routing decisions based on URL and/or other data in request objects.

  • 通过 response 对象发送标头、HTTP 状态代码和正文数据。

    ¥Send headers, HTTP status codes and body data via response objects.

  • 将数据从 request 对象传输到 response 对象。

    ¥Pipe data from request objects and to response objects.

  • 处理 requestresponse 流中的流错误。

    ¥Handle stream errors in both the request and response streams.

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

¥From these basics, Node.js HTTP servers for many typical use cases can be constructed. There are plenty of other things these APIs provide, so be sure to read through the API docs for EventEmitters, Streams, and HTTP.