专栏首页全栈修仙之路深入学习 Node.js Http

深入学习 Node.js Http

预备知识

HTTP

请求报文示例
GET / HTTP/1.1
Host: www.baidu.com
Connection: keep-alive
Cache-Control: max-age=0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
响应报文示例
HTTP/1.1 200 OK
Server: bfe/1.0.8.18
Date: Thu, 30 Mar 2017 12:28:00 GMT
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Cache-Control: private
Expires: Thu, 30 Mar 2017 12:27:43 GMT
Expect 请求头

Expect 是一个请求消息头,包含一个期望条件,表示服务器只有在满足此期望条件的情况下才能妥善地处理请求。规范中只规定了一个期望条件,即 Expect: 100-continue,对此服务器可以做出如下回应:

  • 100:表示消息头中的期望条件可以得到满足,请求可以顺利进行。
  • 417 (Expectation Failed) 表示服务器不能满足期望的条件,也可以是其他任意表示客户端错误的状态码(4xx)。

常见的浏览器不会发送 Expect 消息头,但是其他类型的客户端如 cURL 默认会这么做。目前规范中只规定了 Expect: 100-continue 这一个期望条件。100-continue 握手的目的是允许客户端在发送包含请求体的消息前,判断源服务器是否愿意在客户端发送请求体前接收请求。

在实际开发过程中,需谨慎使用 Expect: 100-continue,因为如果遇到不支持 HTTP/1.1协议的服务器或代理服务器可能会引起问题。

FreeList

在 Node.js 中为了避免频繁创建和销毁对象,实现了一个通用的 FreeList 机制。在 http 模块中,就利用到了 FreeList 机制,即用来动态管理 HTTPParser 对象:

var parsers = new FreeList('parsers', 1000, function() {
  var parser = new HTTPParser(HTTPParser.REQUEST);
  //...
}

是不是感觉很高大尚,其实 FreeList 的内部实现很简单,具体如下:

class FreeList {
  constructor(name, max, ctor) {
    this.name = name; // 管理的对象名称
    this.ctor = ctor; // 管理对象的构造函数
    this.max = max; // 存储对象的最大值
    this.list = []; // 存储对象的数组
  }

  alloc() {
    return this.list.length ?
      this.list.pop() :
      this.ctor.apply(this, arguments);
  }

  free(obj) {
    if (this.list.length < this.max) {
      this.list.push(obj);
      return true;
    }
    return false;
  }
}

在处理 HTTP 请求的场景下,当新的请求到来时,我们通过调用 parsers.alloc() 方法来获取 HTTPParser 对象,从而解析 HTTP 请求。当完成 HTTP 解析任务后,我们可以通过调用 parsers.free() 方法来归还 HTTPParser 对象。

IncomingMessage

在 Node.js 服务器接收到请求时,会利用 HTTPParser 对象来解析请求报文,为了便于开发者使用,Node.js 会基于解析后的请求报文创建 IncomingMessage 对象,IncomingMessage 构造函数(代码片段)如下:

function IncomingMessage(socket) {
  Stream.Readable.call(this);

  this.socket = socket;
  this.connection = socket;

  this.httpVersion = null;
  this.complete = false;
  this.trailers = {}; 
  this.headers = {}; // 解析后的请求头
  this.rawHeaders = []; // 原始的头部信息

  // request (server) only
  this.url = ''; // 请求url地址
  this.method = null; // 请求地址
}
util.inherits(IncomingMessage, Stream.Readable);

Http 协议是基于请求和响应,请求对象我们已经介绍了,那么接下来就是响应对象。在 Node.js 中,响应对象是 ServerResponse 类的实例。

ServerResponse

function ServerResponse(req) {
  OutgoingMessage.call(this);

  if (req.method === 'HEAD') this._hasBody = false;

  this.sendDate = true;
  this._sent100 = false;
  this._expect_continue = false;

  if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
    this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
    this.shouldKeepAlive = false;
  }
}
util.inherits(ServerResponse, OutgoingMessage);

通过以上代码,我们可以发现 ServerResponse 继承于 OutgoingMessage。在 OutgoingMessage 对象中会包含用于生成响应报文的相关信息,这里就不详细展开,有兴趣的小伙伴可以查看 _http_outgoing.js 文件。

Node.js Http

Http 基本使用

simple_server.js

const http = require("http");

const server = http.createServer((req, res) => {
  res.end("Hello Semlinker!");
});

server.listen(3000, () => {
  console.log("server listen on 3000");
});

当运行完 node simple_server.js 命令后,你可以通过 http://localhost:3000/ 这个 url 地址来访问我们本地的服务器。不出意外的话,你将在打开的页面中看到 “Hello Semlinker!”。

虽然以上的示例很简单,但对于之前没有服务端经验或者刚接触 Node.js 的小伙伴来说,可能会觉得这是一个很神奇的事情。接下来我们来通过以上简单的示例,分析一下 Node.js 的 Http 模块。

Http 服务器

显而易见,http.createServer() 方法用来创建服务器,该方法的实现如下:

function createServer(requestListener) {
  return new Server(requestListener);
}

createServer 函数内部,我们通过调用 Server 构造函数来创建服务器。因此,接下来的重点就是分析 Server 构造函数了,该函数的内部实现如下:

function Server(options, requestListener) {
  if (!(this instanceof Server)) return new Server(options, requestListener);

  if (typeof options === 'function') {
    requestListener = options;
    options = {};
  } else if (options == null || typeof options === 'object') {
    options = util._extend({}, options);
  }

  net.Server.call(this, { allowHalfOpen: true });

  if (requestListener) {
    this.on('request', requestListener);
  }

  this.on('connection', connectionListener);
  this.timeout = 2 * 60 * 1000; // 设置超时时间
}
util.inherits(Server, net.Server);

看到 this.on('request',requestListener)this.on('connection',connectionListener) 这两行,不知道小伙伴们有没有想起我们的 EventEmitter。

通过以上源码,目前我们得出了一个结论,在触发 request 事件后,就会调用我们设置的 requestListener 函数,即执行以下代码:

(req, res) => {
  res.end("Hello Semlinker!");
}

那么什么时候会触发 request 事件呢?而 connection 事件和 connectionListener 又是什么?带着这些问题,我们来继续学习 Http 模块。

connection 事件,顾名思义用来跟踪网络连接。这里,我们重点来看一下 connectionListener 函数:

function connectionListener(socket) {
  defaultTriggerAsyncIdScope(
    getOrSetAsyncId(socket), connectionListenerInternal, this, socket
  );
}

该函数内竟然还有一个 connectionListenerInternal,那只能继续往下分析了,connectionListenerInternal 函数(代码片段)的内部实现如下:

function connectionListenerInternal(server, socket) {
  httpSocketSetup(socket);

  if (socket.server === null)
    socket.server = server;
  
  if (server.timeout && typeof socket.setTimeout === 'function')
    socket.setTimeout(server.timeout);
  socket.on('timeout', socketOnTimeout); // 处理超时情况

  var parser = parsers.alloc(); // 获取parser对象
  parser.reinitialize(HTTPParser.REQUEST);
  parser.socket = socket;
  socket.parser = parser;
  parser.incoming = null;

  var state = {
    outgoing: [],
    incoming: [], 
    //...
  };
  parser.onIncoming = parserOnIncoming.bind(undefined, server, socket, state);
}

在 connectionListenerInternal 函数内部,我们终于见到了 “预备知识” 章节中介绍的 parsers 对象(FreeList 实例)。现在是时候来目睹一下 HTTPParser 对象的芳容了:

var parsers = new FreeList('parsers', 1000, function() {
  var parser = new HTTPParser(HTTPParser.REQUEST);

  parser._headers = [];
  parser._url = '';
  parser._consumed = false;

  parser.socket = null;
  parser.incoming = null;
  parser.outgoing = null;
  
  parser[kOnHeaders] = parserOnHeaders;
  parser[kOnHeadersComplete] = parserOnHeadersComplete;
  parser[kOnBody] = parserOnBody;

  return parser;
});

以 parser 开头的这些对象,都是定义在 _http_common.js 文件中的函数对象。这里我就不罗列出相关的代码了,只对它们的作用做一些简单的总结:

  • parserOnHeaders:当请求头跨多个 TCP 数据包或者过大无法再一个运行周期内处理完才会调用该方法。
  • kOnHeadersComplete:请求头解析完成后,会调用该方法。方法内部会创建 IncomingMessage 对象,填充相关的属性,比如 url、httpVersion、method 和 headers 等。
  • parserOnBody:不断解析已接收的请求体数据。

这里需要注意的是,请求报文的解析工作是由 C++ 来完成,内部通过 binding 来实现,具体参考 deps/http_parser 目录。

const { methods, HTTPParser } = process.binding('http_parser');

介绍完 HTTPParser 对象,我们继续回到 connectionListenerInternal 函数中,在最后一行我们设置 parser 对象的 onIncoming 属性为绑定后的 parserOnIncoming 函数,该函数的实现如下(代码片段):

function parserOnIncoming(server, socket, state, req, keepAlive) {
  state.incoming.push(req); // 缓冲IncomingMessage实例

  var res = new server[kServerResponse](req);

  if (socket._httpMessage) {
    state.outgoing.push(res); // 缓冲ServerResponse实例
  } else {
    res.assignSocket(socket);
  }

  // 判断请求头是否包含expect字段且http协议的版本为1.1
  if (req.headers.expect !== undefined &&
      (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) {
    // continueExpression: /(?:^|\W)100-continue(?:$|\W)/i
    // Expect: 100-continue
    if (continueExpression.test(req.headers.expect)) {
      res._expect_continue = true;

      if (server.listenerCount('checkContinue') > 0) {
        server.emit('checkContinue', req, res);
      } else {
        res.writeContinue();
        server.emit('request', req, res);
      }
    } else if (server.listenerCount('checkExpectation') > 0) {
      server.emit('checkExpectation', req, res);
    } else {
      // HTTP协议中的417Expectation Failed 状态码表示客户端错误,意味着服务器无法满足
      // Expect请求消息头中的期望条件。
      res.writeHead(417);
      res.end();
    }
  } else {
    server.emit('request', req, res);
  }
  return 0;  
}

通过观察上面的代码,我们终于发现了 request 事件的踪迹。在 parserOnIncoming 函数内,我们会基于 req 请求对象创建 ServerResponse 响应对象,在创建响应对象后,会判断请求头是否包含 expect 字段,然后针对不同的条件做出不同的处理。对于之前最早的示例来说,程序会直接走 else 分支,即触发 request 事件,并传递当前的请求对象和响应对象。

现在我们来回顾一下整个流程:

  • 调用 http.createServer() 方法创建 server 对象,该对象创建完后,我们调用 listen() 方法执行监听操作。
  • 当 server 接收到客户端的连接请求,在成功创建 socket 对象后,会触发 connection 事件。
  • connection 事件触发后,会执行对应的 connectionListener 回调函数。在函数内部会利用 HTTPParser 对象,对请求报文进行解析。
  • 在完成请求头的解析后,会创建 IncomingMessage 对象,并填充相关的属性,比如 url、httpVersion、method 和 headers 等。
  • 在配置完 IncomingMessage 对象后,会调用 parserOnIncoming 函数,在该函数内会构建 ServerResponse 响应对象,如果请求头不包含 expect 字段,则 server 就会触发 request 事件,并传递当前的请求对象和响应对象。
  • request 事件触发后,就会执行我们设定的 requestListener 函数。

其实我们不但可以通过 Node.js 的 Http 模块创建 Http 服务器,也可以利用该模块提供的 request() 或 get() 方法,向其它的 Http 服务器发送 Http 请求。比如:

const http = require("http");

http.get('http://jsonplaceholder.typicode.com/users', (res) => {
  console.log(`Got response: ${res.statusCode}`);
  let data = '';

  res.on('data', (chunk) => {
    data += chunk;
  });

  res.on('end', () => {
    console.log(JSON.parse(data));
  });
}).on('error', (e) => {
  console.log(`Got error: ${e.message}`);
});

不过在实际项目中,我们一般会使用其它功能更加完善的第三方 Http 客户端库,比如 RequestAxiosSuperAgent 等。

总结

本文基于一个简单的服务器示例,一步一步分析了 Node.js Http 模块中请求对象、响应对象内部的创建过程,此外还介绍了 Server 内部两个重要的事件:connectionrequest

在文中我们只分析 request 事件的触发时机,并未介绍 connection 事件的触发时机。此外也没有继续深入分析 server 对象 listen() 方法内部执行流程。

参考资源

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Angular 6 HttpClient 快速入门

    本教程将介绍如何在 Angular 6.x 中使用 HttpClient 发送 Http 请求,如 get、post、put 和 delete 请求。在 Ang...

    semlinker
  • RxJS mergeMap和switchMap

    假设现在有一个简单的任务:页面上有一个按钮,当你点击按钮的时候,需要启动一个定时器。使用 RxJS 我们可以可以很方便地实现上述功能:

    semlinker
  • TypeScript 函数中的 this 参数

    从 TypeScript 2.0 开始,在函数和方法中我们可以声明 this 的类型,实际使用起来也很简单,比如:

    semlinker
  • 【漏洞预警】PostgreSQL任意代码执行 漏洞(CVE-2019-9193)预警通告

    近日,安全研究人员披露了PostgreSQL提权代码执行漏洞(CVE-2019-9193)的漏洞细节,具有数据库服务端文件读权限的攻击者利用此漏洞,可执行任意系...

    绿盟科技安全情报
  • ZOOKEEPER集群搭建及测试

    ①. zk是由java编写的需要java运行环境,所以大家首先要安装JDK 具体安装步骤,不再赘述      ②. 首先进入zk的conf目录,将zoo_sam...

    用户1215919
  • 聊聊spring cloud的EurekaServerInitializerConfiguration

    本文主要研究一下spring cloud的EurekaServerInitializerConfiguration

    codecraft
  • Fiddler 抓包工具总结(APP接口分析)

    Fiddler是一个蛮好用的抓包工具,可以将网络传输发送与接受的数据包进行截获、重发、编辑、转存等操作。也可以用来检测网络安全。反正好处多多,举之不尽呀!当年学...

    ZhangXianSheng
  • 分布式链路追踪系统原来是这么一回事

    分布式系统变得日趋复杂,越来越多的组件开始走向分布式化,如微服务、分布式数据库、分布式缓存等,使得后台服务构成了一种复杂的分布式网络。在服务能力提升的同时,复杂...

    黄泽杰
  • 《Java从入门到放弃》框架入门篇:springMVC数据传递 (二)

    十方上下
  • 金九银十求职季,前端面试大全送给你

    最近好多小伙伴都跳槽去找工作,我只能在心里默默的支持他们能找到一份好的工作,这份前端面试大全送给我的小伙伴们,主要说的是前端一些常用的一些知识,说的不对的地方请...

    Javanx

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动