記一次 Node.js 原始碼分析

迅雷前端發表於2019-03-02

作者: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嘗試一下,最新版本已經沒有問題了

掃一掃關注迅雷前端公眾號

記一次 Node.js 原始碼分析

相關文章