记一次 Node.js 源码分析

1,763 阅读2分钟

作者:chainhelen from 迅雷

本文讲述作者是如何定位发现 Node.js 存在的一处问题,Node.js 最新版本已经修复了该问题。本文主要分享定位问题的思路和方法,当你在开发当中遇到疑难问题的时候,不排除是依赖的技术和框架出现了问题,当你尝试找到并修复它,我相信不光可以收获到贡献代码的成就感,也会带来技术水平和信心的提升。

1. 问题

前几日,我在测试express框架的时候,构造了一个测试样例死活过不来,即便调试到测试框架superagent ,依然不对。最终发现是Node.js的"问题",而且最新版本的Node.js已经"修复"了,导致我中间饶了几圈都没发现是Node.js的事,下面来重现问题流程。

2. 环境预备

  1. 安装一下gnvm 地址,后面需要控制一下版本(windows10 需要用管理员权限的 cmd 或者 powershell)
  2. 安装 git 环境(主要要使用curl命令)
  3. 摘抄如下代码
// main.js
var http = require('http')

var tmpObject = Object
tmpObject.prototype['love'] = 'express'
var server = http.createServer(function (_, res) {
  res.setHeader("m", "w")
  res.end()
})
server.listen(3010)

3.问题复现

  1. 安装 Node.js (当前稳定版)版本
gnvm install 8.11.2
gnvm use 8.11.2
  1. 运行代码,Node.js main.js
  2. 使用curl -i 127.0.0.1:3010命令,得到如下
$ curl -i 127.0.0.1:3010
HTTP/1.1 200 OK
m: w
e: x
Date: Fri, 18 May 2018 14:06:47 GMT
Connection: keep-alive
Content-Length: 0

能理解有一个头 m: w,但是e: x是从哪来的?明明奇怪的改动只是 Object.prototype.love='express'

4.再次测试

修改一下main.js的代码,注释掉res.setHeader("m", "w")试试看

// main.js
var http = require('http')

var tmpObject = Object
tmpObject.prototype['love'] = 'express'
var server = http.createServer(function (_, res) {
  // res.setHeader("m", "w")
  res.end()
})
server.listen(3010)

$ curl -i 127.0.0.1:3010
HTTP/1.1 200 OK
Date: Fri, 18 May 2018 14:30:01 GMT
Connection: keep-alive
Content-Length: 0

竟然没有了

5.解释

翻阅v8.11.2代码 _http_outgoing.js#L497

OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
  ...
  if (!this[outHeadersKey])
    this[outHeadersKey] = {};

  const key = name.toLowerCase();
  this[outHeadersKey][key] = [name, value];
  ...
};

那么3测试里面代码执行的时候,保存 header 的数据是这样的

this[outHeaderKey] = {
    "m": ["m", "w"]
}

另外注意变量初始化this[outHeadersKey] = {},那么this[outHeaderKey]的原型链指向Object.prototype

有了上面的认知,来看下res.end()做了哪些事,写一下调用链 _http_outgoing.end() => _http_server._implicitHeader() => _http_server.writeHead() => _http_outgoing._storeHeader()

看一下_http_server.writeHead()_http_server#L202

    headers = this[outHeadersKey];
    this._storeHeader(statusLine, headers);

继续看一下_http_outgoing.storeHeader()_http_server#L307

if (headers === this[outHeadersKey]) {
    for (key in headers) {
      var entry = headers[key];
      field = entry[0];
      value = entry[1];
      ...
 }

1.当上述for in遍历到自定义res.setHeader("m", "w") 中的 "m":["m": "w"] key=mentry = [m, w] 则取出数据 field = mvalue = w,没毛病

2.但当for in遍历到原型链的时候,key = 'love'entry = 'express' 那么field = entry[0] = 'e'value = entry[1] = 'x' 故而响应头中的 e:x 就是这么来的

6.小结

本质上是for in遍历到原型链,加上Node.js保存 outHeadersKey 的"奇怪"数组方式

才会导致发包过程中出现了一个难以理解的header

另外,对于for in来说,项目中通常采用hasOwnProperty来规避问题,但是新版本Node.js不是这样做的,下面是最新的Node.js这块代码 _http_outgoing.js#L121

 const headers = this[outHeadersKey] = Object.create(null);

Object.create(null)会把创建出来的对象__proto__ 指向 null for in 就不会遍历到了,可以使用gnvm use v10.1.0尝试一下,最新版本已经没有问题了

扫一扫关注迅雷前端公众号