偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Node.js HTTP Client 內(nèi)存泄露問題

開發(fā) 前端
最近社區(qū)有一個開發(fā)者提交了一個關于 HTTP 模塊內(nèi)存泄露的問題(issue),這個問題影響的是 Node.js HTTP 客戶端,但是是比較特殊的場景,一般不會出現(xiàn),除非服務端惡意攻擊客戶端。

最近社區(qū)有一個開發(fā)者提交了一個關于 HTTP 模塊內(nèi)存泄露的問題(issue),這個問題影響的是 Node.js HTTP 客戶端,但是是比較特殊的場景,一般不會出現(xiàn),除非服務端惡意攻擊客戶端。最近提交了一個 PR 修復了這個問題,本文簡單介紹下這個問題和修復方案。

例子

先看下復現(xiàn)的代碼。

const http = require('http');
const gcTrackerMap = newWeakMap();
const gcTrackerTag = 'NODE_TEST_COMMON_GC_TRACKER';

function onGC(obj, gcListener) {
const async_hooks = require('async_hooks');
const onGcAsyncHook = async_hooks.createHook({
    init: function(id, type) {
      if (this.trackedId === undefined) {
        this.trackedId = id;
      }
    },
    destroy(id) {
      if (id === this.trackedId) {
        this.gcListener.ongc();
        onGcAsyncHook.disable();
      }
    },
  }).enable();
  onGcAsyncHook.gcListener = gcListener;

  gcTrackerMap.set(obj, new async_hooks.AsyncResource(gcTrackerTag));
  obj = null;
}


function createServer() {
const server = http.createServer((req, res) => {
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ hello: 'world' }));
    req.socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
  });

returnnewPromise((resolve) => {
    server.listen(0, () => {
      resolve(server);
    });
  });
}

asyncfunction main() {
const server = await createServer();
const req = http.get({
    port: server.address().port,
  }, (res) => {
    const chunks = [];
    res.on('data', (c) => chunks.push(c), 1);
    res.on('end', () => {
      console.log(Buffer.concat(chunks).toString('utf8'));
    });
  });
const timer = setInterval(global.gc, 300);
  onGC(req, {
    ongc: () => {
      clearInterval(timer);
      server.close();
    }
  });
}

main();

上面的代碼邏輯很簡單,首先發(fā)起一個 HTTP,然后拿到一個響應,特殊的地方在于服務器返回了兩個響應,從而導致了 request 對象不會被釋放,引起內(nèi)存泄露問題。

HTTP 響應解析過程

下面來分析下原因,分析這個問題需要對 Node.js HTTP 協(xié)議解析過程有一些了解。簡單來說,Node.js 收到數(shù)據(jù)后,會調(diào) parser.execute(data) 進行 HTTP 協(xié)議的解析。

// socket 收到數(shù)據(jù)時執(zhí)行
function socketOnData(d) {
const socket = this;
const req = this._httpMessage;
const parser = this.parser;
// 解析 HTTP 響應
const ret = parser.execute(d);
// 響應解析完成,做一些清除操作,釋放相關對象內(nèi)存
if (parser.incoming?.complete) {
    socket.removeListener('data', socketOnData);
    socket.removeListener('end', socketOnEnd);
    socket.removeListener('drain', ondrain);
    freeParser(parser, req, socket);
  }
}

function freeParser(parser, req, socket) {
if (parser) {
    cleanParser(parser);
    parser.remove();
    if (parsers.free(parser) === false) {
      // function closeParserInstance(parser) { parser.close(); }
      setImmediate(closeParserInstance, parser);
    } else {
      parser.free();
    }
  }
if (req) {
    req.parser = null;
  }
if (socket) {
    socket.parser = null;
  }
}

function cleanParser(parser) {
  parser.socket = null;
  parser.incoming = null;
  parser.outgoing = null;
  parser[kOnMessageBegin] = null;
  parser[kOnExecute] = null;
  parser[kOnTimeout] = null;
  parser.onIncoming = null;
}

在解析過程中會執(zhí)行多個鉤子函數(shù)。

// 解析 header 時
const kOnHeaders = HTTPParser.kOnHeaders | 0;
// 解析 header 完成時
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
// 解析 HTTP body 時
const kOnBody = HTTPParser.kOnBody | 0;
// 解析完一個 HTTP 報文時
const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;

接著看 Node.js 在處理 HTTP 響應時,這些鉤子函數(shù)的邏輯。

解析到 header。

function parserOnHeaders(headers, url) {
  // Once we exceeded headers limit - stop collecting them
  if (this.maxHeaderPairs <= 0 ||
      this._headers.length < this.maxHeaderPairs) {
    this._headers.push(...headers);
  }
  this._url += url;
}

解析完 header。

function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
                                 url, statusCode, statusMessage, upgrade,
                                 shouldKeepAlive) {
  const parser = this;
  const { socket } = parser;
  const incoming = parser.incoming = new IncomingMessage(socket);
  return parser.onIncoming(incoming, shouldKeepAlive);
}

接著回調(diào) onIncoming 函數(shù)。

function parserOnIncomingClient(res, shouldKeepAlive) {
const socket = this.socket;
const req = socket._httpMessage;

if (req.res) {
    // 收到了多個響應
    socket.destroy();
    return0;
  }
// 觸發(fā) response 事件
if (req.aborted || !req.emit('response', res)) {
    // ...
  }
return0;  // No special treatment.
}

解析 HTTP 響應 body。

function parserOnBody(b) {
const stream = this.incoming;

// If the stream has already been removed, then drop it.
if (stream === null)
    return;

// 把 body push 到響應對象中
if (!stream._dumped) {
    const ret = stream.push(b);
    if (!ret)
      readStop(this.socket);
  }
}

解析完 HTTP 響應。

function parserOnMessageComplete() {
  const parser = this;
  const stream = parser.incoming;
  // stream 就是上面的 IncomingMessage 對象
  if (stream !== null) {
    // 標記響應對象解析完成
    stream.complete = true;
    // 標記流結(jié)束
    stream.push(null);
  }
}

分析問題

了解了大概的流程后看一下為啥會出現(xiàn)內(nèi)存泄露問題,當通過 parser.execute(data) 解析響應時,因為服務器返回了兩個響應,第一次解析完 HTTP 響應 header 時執(zhí)行以下代碼。

const incoming = parser.incoming = new IncomingMessage(socket);
return parser.onIncoming(incoming, shouldKeepAlive);

onIncoming 會觸發(fā) response 事件,這是正常的流程,緊接著又解析完第二個響應的 header 時問題就來了,這時同樣會執(zhí)行上面的代碼,并且注意 parser.incoming 指向了新的 IncomingMessage 對象,接著看這時 onIncoming 的邏輯。

// 已經(jīng)收到了一個響應了,忽略并銷毀 socket
if (req.res) {
  socket.destroy();
  return 0;
}

Node.js 這里做了判斷,直接銷毀 socket 并返回,最終 parser.execute(data) 執(zhí)行結(jié)束,相關代碼如下。

// 解析 HTTP 響應
const ret = parser.execute(d);
// 響應是否解析完成
if (parser.incoming?.complete) {
  // 做一些清除操作,釋放相關對象內(nèi)存
  freeParser(parser, req, socket);
}

因為 parser.incoming 這時候指向的是第二個響應,其 complete 字段的值是 false,所以導致沒有執(zhí)行清除操作,引起內(nèi)存泄露。

修復方案

修復方案有兩個,一是在解析到第二個響應時,以下代碼返回 -1 表示解析出錯。

if (req.res) {
  socket.destroy();
  return -1;
}

但是這種方式有一個問題是,因為解析完第一個響應時已經(jīng)觸發(fā)了 response 事件,然后這里如果又觸發(fā) error 事件會比較奇怪,讓用戶側(cè)不好處理。第二種方案是忽略第二個響應。最終選擇的是第二種方案,改動如下。

if (req.res) {
  socket.destroy();
  if (socket.parser) {
      // Now, parser.incoming is pointed to the new IncomingMessage,
      // we need to rewrite it to the first one and skip all the pending IncomingMessage
      socket.parser.incoming = req.res;
      socket.parser.incoming[kSkipPendingData] = true;
    }
  return 0;
}

首先讓 parser.incoming 執(zhí)行第一個響應,并且設置丟棄后續(xù)所有數(shù)據(jù)標記,然后在后續(xù)解析過程中忽略收到的數(shù)據(jù),否則后續(xù)的數(shù)據(jù)會干擾第一個響應。

function parserOnBody(b) {
const stream = this.incoming;

if (stream === null || stream[kSkipPendingData])
    return;

if (!stream._dumped) {
    const ret = stream.push(b);
    if (!ret)
      readStop(this.socket);
  }
}

function parserOnMessageComplete() {
const parser = this;
const stream = parser.incoming;

if (stream !== null && !stream[kSkipPendingData]) {
    stream.complete = true;
    stream.push(null);
  }
}

1. issue:https://github.com/nodejs/node/issues/60025

2. PR:https://github.com/nodejs/node/pull/60062

責任編輯:武曉燕 來源: 編程雜技
相關推薦

2023-06-30 23:25:46

HTTP模塊內(nèi)存

2025-01-08 08:47:44

Node.js內(nèi)存泄露定時器

2017-03-19 16:40:28

漏洞Node.js內(nèi)存泄漏

2017-03-20 13:43:51

Node.js內(nèi)存泄漏

2014-09-12 10:35:09

Node.jsHTTP 206

2013-11-01 09:34:56

Node.js技術

2015-03-10 10:59:18

Node.js開發(fā)指南基礎介紹

2022-01-02 06:55:08

Node.js ObjectWrapAddon

2020-01-03 16:04:10

Node.js內(nèi)存泄漏

2021-10-03 15:02:50

HTTPNodejs

2017-04-24 08:31:26

Node.jsExpress.jsHTTP

2022-06-23 06:34:56

Node.js子線程

2011-09-02 14:47:48

Node

2011-09-09 14:23:13

Node.js

2011-09-08 13:46:14

node.js

2011-11-01 10:30:36

Node.js

2012-10-24 14:56:30

IBMdw

2011-11-10 08:55:00

Node.js

2021-10-21 08:59:17

技術HTTP攻擊

2021-12-25 22:29:57

Node.js 微任務處理事件循環(huán)
點贊
收藏

51CTO技術棧公眾號