资讯专栏INFORMATION COLUMN

通过源码解析 Node.js 中一个 HTTP 请求到响应的历程

qqlcbb / 733人阅读

摘要:请求的来到在中,若要收到一个请求,首先需要创建一个类的实例,然后监听它的事件。不断解析推入的请求体数据。处理具体解析完毕的请求。比较绕,以一个简化的图示来总结这部分逻辑响应该请求到了响应时,事情已经简单许多了,传入的已经获取到了。

如果大家使用 Node.js 写过 web 应用,那么你一定使用过 http 模块。在 Node.js 中,起一个 HTTP server 十分简单,短短数行即可:

"use stirct"
const { createServer } = require("http")

createServer(function (req, res) {
  res.writeHead(200, { "Content-Type": "text/plain" })
  res.end("Hello World
")
})
.listen(3000, function () { console.log("Listening on port 3000") })
$ curl localhost:3000
Hello World

就这么简单,因为 Node.js 把许多细节都已在源码中封装好了,主要代码在 lib/_http_*.js 这些文件中,现在就让我们照着上述代码,看看从一个 HTTP 请求的到来直到响应,Node.js 都为我们在源码层做了些什么。

HTTP 请求的来到

在 Node.js 中,若要收到一个 HTTP 请求,首先需要创建一个 http.Server 类的实例,然后监听它的 request 事件。由于 HTTP 协议属于应用层,在下层的传输层通常使用的是 TCP 协议,所以 net.Server 类正是 http.Server 类的父类。具体的 HTTP 相关的部分,是通过监听 net.Server 类实例的 connection 事件封装的:

// lib/_http_server.js
// ...

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true });

  if (requestListener) {
    this.addListener("request", requestListener);
  }

  // ...
  this.addListener("connection", connectionListener);

  // ...
}
util.inherits(Server, net.Server);

这时,则需要一个 HTTP parser 来解析通过 TCP 传输过来的数据:

// lib/_http_server.js
const parsers = common.parsers;
// ...

function connectionListener(socket) {
  // ...
  var parser = parsers.alloc();
  parser.reinitialize(HTTPParser.REQUEST);
  parser.socket = socket;
  socket.parser = parser;
  parser.incoming = null;
  // ...
}

值得一提的是,parser 是从一个“池”中获取的,这个“池”使用了一种叫做 free list(wiki)的数据结构,实现很简单,个人觉得是为了尽可能的对 parser 进行重用,并避免了不断调用构造函数的消耗,且设有数量上限(http 模块中为 1000):

// lib/freelist.js
"use strict";

exports.FreeList = function(name, max, constructor) {
  this.name = name;
  this.constructor = constructor;
  this.max = max;
  this.list = [];
};


exports.FreeList.prototype.alloc = function() {
  return this.list.length ? this.list.pop() :
                            this.constructor.apply(this, arguments);
};


exports.FreeList.prototype.free = function(obj) {
  if (this.list.length < this.max) {
    this.list.push(obj);
    return true;
  }
  return false;
};

由于数据是从 TCP 不断推入的,所以这里的 parser 也是基于事件的,很符合 Node.js 的核心思想。使用的是 http-parser 这个库:

// lib/_http_common.js
// ...
const binding = process.binding("http_parser");
const HTTPParser = binding.HTTPParser;
const FreeList = require("internal/freelist").FreeList;
// ...

var parsers = new FreeList("parsers", 1000, function() {
  var parser = new HTTPParser(HTTPParser.REQUEST);
  // ...
  parser[kOnHeaders] = parserOnHeaders;
  parser[kOnHeadersComplete] = parserOnHeadersComplete; 
  parser[kOnBody] = parserOnBody; 
  parser[kOnMessageComplete] = parserOnMessageComplete;
  parser[kOnExecute] = null; 

  return parser;
});
exports.parsers = parsers;

// lib/_http_server.js
// ...

function connectionListener(socket) {
  parser.onIncoming = parserOnIncoming;
}

所以一个完整的 HTTP 请求从接收到完全解析,会挨个经历 parser 上的如下事件监听器:

parserOnHeaders:不断解析推入的请求头数据。

parserOnHeadersComplete:请求头解析完毕,构造 header 对象,为请求体创建 http.IncomingMessage 实例。

parserOnBody:不断解析推入的请求体数据。

parserOnExecute:请求体解析完毕,检查解析是否报错,若报错,直接触发 clientError 事件。若请求为 CONNECT 方法,或带有 Upgrade 头,则直接触发 connectupgrade 事件。

parserOnIncoming:处理具体解析完毕的请求。

所以接下来,我们的关注点自然是 parserOnIncoming 这个监听器,正是这里完成了最终 request 事件的触发,关键步骤代码如下:

// lib/_http_server.js
// ...

function connectionListener(socket) {
  var outgoing = [];
  var incoming = [];
  // ...
  
  function parserOnIncoming(req, shouldKeepAlive) {
    incoming.push(req);
    // ...
    var res = new ServerResponse(req);
    
    if (socket._httpMessage) { // 这里判断若为真,则说明 socket 正在被队列中之前的 ServerResponse 实例占用
      outgoing.push(res);
    } else {
      res.assignSocket(socket);
    }
    
    res.on("finish", resOnFinish);
    function resOnFinish() {
      incoming.shift();
      // ...
      var m = outgoing.shift();
      if (m) {
        m.assignSocket(socket);
      }
    }
    // ...
    self.emit("request", req, res);
  }
}

可以看出,对于同一个 socket 发来的请求,源码中分别维护了两个队列,用于缓冲 IncomingMessage 实例和对应的 ServerResponse 实例。先来的 ServerResponse 实例先占用 socket ,监听其 finish 事件,从各自队列中释放该 ServerResponse 实例和对应的 IncomingMessage 实例。

比较绕,以一个简化的图示来总结这部分逻辑:

响应该 HTTP 请求

到了响应时,事情已经简单许多了,传入的 ServerResponse 已经获取到了 socket。http.ServerResponse 继承于一个内部类 http.OutgoingMessage,当我们调用 ServerResponse#writeHead 时,Node.js 为我们拼凑好了头字符串,并缓存在 ServerResponse 实例内部的 _header 属性中:

// lib/_http_outgoing.js
// ...

OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
  // ...
  if (headers) {
    var keys = Object.keys(headers);
    var isArray = Array.isArray(headers);
    var field, value;

    for (var i = 0, l = keys.length; i < l; i++) {
      var key = keys[i];
      if (isArray) {
        field = headers[key][0];
        value = headers[key][1];
      } else {
        field = key;
        value = headers[key];
      }

      if (Array.isArray(value)) {
        for (var j = 0; j < value.length; j++) {
          storeHeader(this, state, field, value[j]);
        }
      } else {
        storeHeader(this, state, field, value);
      }
    }
  }
  // ...
  this._header = state.messageHeader + CRLF; 
}

紧接着在调用 ServerResponse#end 时,将数据拼凑在头字符串后,添加对应的尾部,推入 TCP ,具体的写入操作在内部方法 ServerResponse#_writeRaw 中:

// lib/_http_outgoing.js
// ...

OutgoingMessage.prototype.end = function(data, encoding, callback) {
  // ...
  if (this.connection && data)
    this.connection.cork();
    
  var ret;
  if (data) {
    this.write(data, encoding);
  }
  
  if (this._hasBody && this.chunkedEncoding) {
    ret = this._send("0
" + this._trailer + "
", "binary", finish);
  } else {
    ret = this._send("", "binary", finish);
  }
  
  if (this.connection && data)
    this.connection.uncork();
    
  // ...
  return ret;
}

OutgoingMessage.prototype._writeRaw = function(data, encoding, callback) {
  if (typeof encoding === "function") {
    callback = encoding;
    encoding = null;
  }

  var connection = this.connection;
  // ...
  return connection.write(data, encoding, callback);
};
最后

到这,一个请求就已经通过 TCP ,发回给客户端了。其实本文中,只涉及到了一条主线进行解析,源码中还考虑了更多的情况,如超时,socket 被占用时的缓存,特殊头,上游突然出现问题,更高效的已写头的查询等等。非常值得一读。

参考:

https://github.com/nodejs/node/blob/master/lib/_http_common.js

https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js

https://github.com/nodejs/node/blob/master/lib/_http_server.js

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79356.html

相关文章

  • 最近学前后端分离知识

    摘要:文本已收录至我的仓库,欢迎前后端分离这个词相信大家都听过,不知道大家是怎么理解的呢。流下不学无术的泪水目前我了解到的前后端分离,首先部署是分离的至少不会跟绑定在一起部署接口只返回数据关于前端这几大框架这几个我都是没有写过的,所以也就不多了。 前言 只有光头才能变强。文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y ...

    MoAir 评论0 收藏0
  • Node.js 系列:原生 Node.js 应用

    摘要:原生应用是一个基于引擎的运行环境使用了一个事件驱动非阻塞式的模型,使其轻量又高效的包管理器,是全球最大的开源库生态系统本文主要介绍构建一个应用的基本步骤和模块,并假定你已经对有一定的了解本文引用部分代码作为例子,如果希望参看全部源码,欢迎去 原生 Node.js 应用 Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境Node.js 使用了一个事件驱...

    Ocean 评论0 收藏0
  • node学习之路(一)—— 网络请求

    摘要:域套接字使用或指定请求方法的字符串。请求路径包含非法字符时抛出异常。保持资源池周围的套接字在未来被用于其它请求。默认值为当使用的时候,通过正在保持活动的套接字发送包的频繁程度。 文章来源:小青年原创发布时间:2016-09-29关键词:JavaScript,nodejs,http,url ,Query String,爬虫转载需标注本文原始地址: http://zhaomenghuan....

    bovenson 评论0 收藏0
  • 走进Node.jsHTTP实现分析

    摘要:事实上,协议确实是基于协议实现的。的可选参数用于监听事件另外,它也监听事件,只不过回调函数是自己实现的。并且会把本次连接的套接字文件描述符封装成对象,作为事件的参数。过载保护理论上,允许的同时连接数只与进程可以打开的文件描述符上限有关。 作者:正龙(沪江Web前端开发工程师)本文为原创文章,转载请注明作者及出处 上文走进Node.js启动过程中我们算是成功入门了。既然Node.js的强...

    April 评论0 收藏0
  • Koa源码解析

    摘要:若大于时,将赋予,此时与相等。通过源码分析,我们知道了的核心思想建立于中间件机制,它是一个设计十分简洁巧妙的框架,扩展性极强,就是建立于之上的上层框架。 Koa是一款设计优雅的轻量级Node.js框架,它主要提供了一套巧妙的中间件机制与简练的API封装,因此源码阅读起来也十分轻松,不论你从事前端或是后端研发,相信都会有所收获。 目录结构 首先将源码下载到本地,可以看到Koa的源码只包含...

    CarterLi 评论0 收藏0

发表评论

0条评论

qqlcbb

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<