跳到内容

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.js 网络服务器应用在某个时候都需要创建一个网络服务器对象。这可以通过使用 createServer 来完成。

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

const  = ('node:http');

const  = .((, ) => {
  // 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  = http.createServer();
.on('request', (, ) => {
  // 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 { ,  } = request;

request 对象是 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.

Headers 也不远。它们在 request 的一个名为 headers 的对象中。

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

const {  } = request;
const  = ['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  = [];
request
  .on('data',  => {
    .();
  })
  .on('end', () => {
     = .().();
    // at this point, `body` has the entire request body stored in it as a string
  });

这可能看起来有点乏味,在许多情况下确实如此。幸运的是,有一些模块,比如 concat-streambodynpm 上可以帮助隐藏一些这些逻辑。在走那条路之前,理解发生了什么是很重要的,这也是你在这里的原因!

关于错误的一个快速说明

🌐 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',  => {
  // This prints the error message and stack trace to `stderr`.
  .(.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  = ('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.

如果我们运行这个示例,我们将能够收到请求,但无法对其进行响应。实际上,如果你在网页浏览器中访问这个示例,你的请求将超时,因为没有任何内容会发送回客户端。

🌐 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.

. = 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 响应中头信息位于主体之前。

关于错误的另一件快速事情

🌐 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  = ('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);

回声服务器示例

🌐 Echo Server Example

让我们简化之前的示例,创建一个简单的回声服务器,它只是将请求中接收到的任何数据直接返回到响应中。我们需要做的就是从请求流中获取数据,然后将这些数据写入响应流,就像我们之前做的那样。

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


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

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

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

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

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

🌐 In any other case, we want to simply respond with a 404.

const  = ('node:http');


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

通过这种方式检查 URL,我们实际上是在进行一种“路由”。 其他形式的路由可以像使用 switch 语句一样简单,也可以像整个框架(例如 express)一样复杂。如果你只需要一个只做路由的工具,可以试试 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  = ('node:http');


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

耶,直播!

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


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

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

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

  • 实例化一个 HTTP 服务器并设置请求处理函数,然后让它监听一个端口。
  • request 对象获取头部、URL、方法和主体数据。
  • 根据 URL 和/或 request 对象中的其他数据做路由决策。
  • 通过 response 对象发送头信息、HTTP 状态码和主体数据。
  • 将数据从 request 对象传输到 response 对象。
  • 处理 requestresponse 流中的流错误。

从这些基础知识出发,可以为许多典型用例构建 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.