解剖 HTTP Transaction

Posted on 2018 September 15
Words 798

原文链接:Anatomy of an HTTP Transaction | Node.js

本指南的目的是让您充分了解 Node.js HTTP 处理的过程。假设你大概知道 HTTP 请求的工作方式,无论语言或编程环境如何。 我们还假设你对 Node.js EventEmittersStreams 有点熟悉。 如果你对它们不太熟悉,那么请先阅读它们的 API 文档。

创建一个服务器

每个 node web 服务器应用都必须创建一个 web 服务器对象。通过 createServer 来创建服务器对象:

const http = requires('http');

const server = http.createServer((reques, response) => {
  // 这里将发生奇迹!
});

每个 HTTP 请求到达服务器的时候,传递给 createServer 的函数会被调用一次,所以它叫请求处理函数。实际上,createServer 返回的 Server 对象是一个 EventEmitter,上面的例子只是对创建一个服务器然后添加一个监听器的简写而已,等价于:

const server = http.createServer();
server.on('request', (reques, response) => {
  // 同样的奇迹也会在这里发生!
});

当一个 HTTP 请求命中了服务器,node 会使用一些有用的对象(即 requestresponse)去调用请求处理函数来处理。

为了真正地服务这些请求,需要在 server 对象上调用 listen 方法。在大多数情况下,你只需要传递服务器想要监听的端口数字给 listen 方法即可,但,还有一些其他的参数也可以用,详情看 HTTP API

HTTP 方法、URL 和头部

当处理 HTTP 请求时,你最想做的可能是查看 HTTP 的方法和 URL,这样你才能判断如何处理这次请求。Node 已经封装好了这些有用的属性在 request 对象里了:

const { method, url } = request;

注意: request 对象是一个 IncomingMessage 的实例。

这里的 method 是 HTTP 的方法(或称动词、谓语)。url 是完整的 URL,不包括服务器、协议和端口号。对于一个典型的 URL,即是包括第三个正斜杆后面的所有内容。

HTTP 头部也非常容易可以获得,它们在 request 对象中的 headers 对象里:

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

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

如果有一些 HTTP 头部被重复发送了,那么它们的值就会被重写或者以逗号为分隔符连接起来,具体取决于头部。但在某些情况下,这不符合我们的预期,所以 rawHeaders 也可以使用,它代表着客户端发送的原生 HTTP 头部。

请求的 body

当收到 POSTPUT 请求时,请求的 body 是非常重要的。获取请求的 body 的数据比获取请求的 headers 要麻烦。request 对象实现了 ReadableStream 的接口。就像其他的 stream 一样,request 也可以 listened 和 piped,我们可以通过监听 dataend 事件来获取 stream 中的数据。

每个 data 事件 emit 出的 chunk 是 Buffer。如果你知道传输的 body 是字符串事件,那么最好是收集数据在一个数组里,最后 concatenate 它们起来成为字符串:

let body = [];
request.on('data', chunk => {
  body.push(chunk);
}).on('end', () => {
  body = Buffer.concat(body).toString();
  // 这时,请求的 body 存储在 `body` 变量中
});

注意: 这起来相当麻烦,但在大多数情况下,你要获取请求的 body 的话你就要这样做。幸运的是,在 npm 上有很多的模块可以帮我们隐藏掉这些麻烦的逻辑,例如 concat-streambody。尽管这些模块可以帮助我们,但是理解底层原理也是很重要的。

关于 Error 的快速预览

因为 request 对象是一个 ReadableStream,也是一个 EventEmitter,所以当有一个 error 发送时,request 就会和 EventEmitter 一样。

请求流中的错误通过 emit 一个 error 事件来表现。如果你没有监听 error 事件,那就会抛出错误,这可能会 crash 你的 Node.js 应用。 因此,你应该在添加一个 error 监听函数,即使你只是 log 它,然后继续下一步。(最好是发送某种 HTTP 的错误响应给客户端):

request.on('error', error => {
  // 这将会打印 error message 和 stack 到 `stderr`
  console.error(error);
})

还有很多方法来 处理这些错误,但是要一直注意错误总是会发生,并且你必须去处理这些错误。

到目前为止,我们获得了什么

到目前为止,我们已经介绍了如何创建服务器、获取请求的 HTTP 方法、URL、headers 和 body。当我们把它们组合起来,就会得到:

const http = require('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();
    // 到这里,我们获得了请求的 headers、HTTP 方法、URL 和 body
    // 接下来,我们需要做的是响应这个请求
  });
}).listen(8080); // 激活这个服务器,监听 8080 端口

如果你运行上面的例子,我们可以 接收 到请求,但是没有 响应 该请求。实际上,如果你访问浏览器,你的请求将会超时,因为没有任何东西发回给客户端。

到目前为止,我们一点都没有接触到 response 对象,它是一个 ServerResponse 的实例,ServerResponse 是一个 WritableStream。它包含了很多将数据发回给客户端的方法。We’ll cover that next.

HTTP 状态码

如果你不设置响应的 HTTP 状态码,那么它将会是 200。当然,不是每个 HTTP 响应都想要返回 200 状态码,可以通过设置 statusCode 属性来设置它:

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

还有一些快速设置状态码的方法,我们很快就会接触到。

设置响应 headers

Headers 可以通过 setHeader 方法来设置:

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

在 HTTP 响应上设置 headers 时,它们的名字是不区分大小写的。如果你重复设置了一个 header,那么最后一个设置的值是发送的值。

显式地(explicitly)发送 Header 数据

上面设置状态码和头部的方法都是假设你使用的是 “隐式的头部(implicit headers)”。也就是说你指望着 node 在开始发送 body 数据之前发送 headers 数据。

如果需要,可以通过 writeHeader 显式地写入 headers 到响应流中,它将状态码和 headers 写入流中:

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

一旦设置了 headers(不管是隐式地还是显式地),你就可以开始发送响应数据了。

发送响应 body

因为 response 对象是一个 WritableStream,所以写入一个响应 body 发送给客户端只要 stream 的方法即可:

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

stream 的 end 函数可以传递一些额外的数据给 stream 作为数据最后的 bits,所以我们可以简化上面的例子为:

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

注意: 设置状态码和 headers 要在你写数据的 chunk 到 body 之前 。这很合理,因为在 HTTP 响应中,headers 在 body 的前面。

另一个关于 Error 的快速预览

response stream 也可以 emit 'error' 事件,所有对 request stream 的建议对 response stream 也适用。

把它们组合在一起

现在我们已经学过了如何生成 HTTP 响应,让我们把它们组合在一起。在之前的例子中,我们已经创建了一个服务器来将所有发给我们的数据发回给客户端。现在,我们会使用 JSON.stringify 来格式化数据成 JSON:

const http = require('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');
    // 注意:上面的 2 行可以替换成下面 1 行
    // response.writeHead(200, {'Content-Type': 'application/json'})

    const responseBody = { headers, method, url, body };

    response.write(JSON.stringify(responseBody));
    response.end();
    // 注意:上面的 2 行可以替换成下面 1 行
    // response.end(JSON.stringify(responseBody))

    // 结束添加新的东西
  });
}).listen(8080);

一个 echo 服务器例子

让我们简化之前的例子来创建一个 echo 服务器的例子,它只是在响应中发送请求中收到的数据。我们需要做的是在 request stream 中获取数据,然后在 response stream 中写入数据,就像我们之前做的那样:

const http = require('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);

现在,让我们来微调一下。我们只想要满足下面的条件时才发回 echo:

  • HTTP 的请求方法是 POST
  • URL 是 /echo

否则的话,我们简单地返回 404:

const http = require('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

Great!现在让我们来简化一下。记住,request 对象是一个 ReadableStreamresponse 对象是一个 WritableStream。意味着我们可以直接使用管道 pipe 将数据从一边引导到另一边。这才是我们想要的 echo 服务器:

const http = require('http');

http.createServer((request, response) => {
  if (request.method === 'POST' && request.url === '/echo') {
    request.pipe(response);
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);

Yay streams!

我们做的还不够好。在这份指南中,我们多次提到错误的出现,所以我们需要处理出错的情况。

为了处理 request stream 上的错误,我们会将错误 log 到 stderr 中并返回一个 400 状态码表明这是一个 Bad Request。但在实际的应用中,我们需要审查这些错误,然后返回对应的状态码和消息给客户端。如果出错了,你可以查看 Error 文档

在响应中,我们只是将错误 log 到 stderr 中:

const http = require('http');

http.createServer((request, response) => {
  request.on('error', err => {
    console.error(err);
    response.statusCode = 404;
    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 对象中获取 headers、URL、HTTP 方法和 body 的数据。
  • 基于 request 对象中的 URL 或其他数据来做出路由决策。
  • 通过 response 对象发送 headers、HTTP 状态码和 body 数据。
  • 使用管道 pipe 从 request 对象引流数据到 response 对象。
  • requestresponse stream 中处理 stream errors。

使用这些基础知识可以构建大多典型的 Node.js HTTP 服务器了。这些 API 还提供了许多其他的功能,因此请务必阅读有关 EventEmitterStreamsHTTP 的 API 文档。