NodeJS源碼分析

簡要

Node已經如今發展很快,已經相對穩定和成熟,在某些時候有必要知道其內部運行原理以及運行處理過程。
種一棵樹最好的時間是十年前 其次是現在。希望能堅持下去。

Nodejs當前最新版本 8.9.4

NodeJS官方網站下載源碼
這裏寫圖片描述

Node.js主要分爲四大部分,Node Standard Library,Node Bindings,V8,Libuv

大體流程是這樣的:

  1. 初始化 V8 、LibUV , OpenSSL

  2. 創建 Environment 環境

  3. 設置 Process 進程對象

  4. 執行 node.js 文件

解壓包後代碼結構如下:

├── AUTHORS
├── BSDmakefile   # bsd平臺makefile文件
├── BUILDING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── CPP_STYLE_GUIDE.md
├── GOVERNANCE.md
├── LICENSE
├── Makefile     # Linux平臺makefile文件
├── README.md
├── android-configure
├── benchmark
├── common.gypi
├── configure
├── deps          # Node底層核心依賴; 最核心的兩塊V8 Engine和libuv事件驅動的異步I/O模型庫
├── doc           
├── lib           # Node後端核心庫
├── node.gyp      # Node編譯任務配置文件 
├── node.gypi
├── src           # C++內建模塊
├── test          # 測試代碼
├── tools         # 編譯時用到的工具
└── vcbuild.bat   # Windows跨平臺makefile文件
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

Hello World 底層運行過程

官方Hello world代碼

#app.js
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

一個簡單的Helloworld涉及到多個模塊:

  • global
  • module
  • http
  • event
  • net

1.0 從main執行到js

入口 src/node_main.cc 106行 通過 src/node.cc 調用 node::Start(argc, argv);
node_main.cc

namespace node {
  extern bool linux_at_secure;
}  // namespace node

int main(int argc, char *argv[]) {
#if defined(__linux__)
  char** envp = environ;
  while (*envp++ != nullptr) {}
  Elf_auxv_t* auxv = reinterpret_cast<Elf_auxv_t*>(envp);
  for (; auxv->a_type != AT_NULL; auxv++) {
    if (auxv->a_type == AT_SECURE) {
      node::linux_at_secure = auxv->a_un.a_val;
      break;
    }   
  }
#endif
  // Disable stdio buffering, it interacts poorly with printf()
  // calls elsewhere in the program (e.g., any logging from V8.)
  setvbuf(stdout, nullptr, _IONBF, 0); 
  setvbuf(stderr, nullptr, _IONBF, 0); 
  // main作爲入口調用node::Start
  return node::Start(argc, argv);
}
#endif
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

1.1 node::Start 加載js

調用順序:

Start() -> LoadEnviroment() -> ExecuteString()

最終在LoadEnvrioment()裏面加載node.js文件,調用ExecuteString() 解析執行node.js文件,返回值是一個f_value

並且在ExecuteString()調用V8的 Script::Compile()  Script::Run()兩個接口去解析執行js代碼。

node.cc

# Nodejs啓動入口, 
inline int Start(Isolate* isolate, IsolateData* isolate_data,
                 int argc, const char* const* argv,
                 int exec_argc, const char* const* exec_argv) {
  HandleScope handle_scope(isolate);
  Local<Context> context = Context::New(isolate);
  Context::Scope context_scope(context);
  Environment env(isolate_data, context);
  CHECK_EQ(0, uv_key_create(&thread_local_env));
  uv_key_set(&thread_local_env, &env);
  env.Start(argc, argv, exec_argc, exec_argv, v8_is_profiling);

  const char* path = argc > 1 ? argv[1] : nullptr;
  StartInspector(&env, path, debug_options);

  if (debug_options.inspector_enabled() && !v8_platform.InspectorStarted(&env))
    return 12;  // Signal internal error.

  env.set_abort_on_uncaught_exception(abort_on_uncaught_exception);

  if (force_async_hooks_checks) {
    env.async_hooks()->force_checks();
  }

  {
    Environment::AsyncCallbackScope callback_scope(&env);
    env.async_hooks()->push_async_ids(1, 0);

    //加載nodejs文件後調用ExecuteString()
    LoadEnvironment(&env); 
    env.async_hooks()->pop_async_id(1);
  }

  env.set_trace_sync_io(trace_sync_io);
  //事件循環池
  {
    SealHandleScope seal(isolate);
    bool more;
    PERFORMANCE_MARK(&env, LOOP_START);
    do {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);

      v8_platform.DrainVMTasks();

      more = uv_loop_alive(env.event_loop());
      if (more)
        continue;

      EmitBeforeExit(&env);

      // Emit `beforeExit` if the loop became alive either after emitting
      // event, or after running some callbacks.
      more = uv_loop_alive(env.event_loop());
    } while (more == true);
    PERFORMANCE_MARK(&env, LOOP_EXIT);
  }

  env.set_trace_sync_io(false);

  const int exit_code = EmitExit(&env);
  RunAtExit(&env);
  uv_key_delete(&thread_local_env);

  v8_platform.DrainVMTasks();
  WaitForInspectorDisconnect(&env);
#if defined(LEAK_SANITIZER)
  __lsan_do_leak_check();
#endif

  return exit_code;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

核心運行流程

整體運行流程圖
這裏寫圖片描述

  1. 核心數據結構 default_loop_struct 結構體爲struct uv_loop_s
    當加載js文件時,如果代碼有io操作,調用lib模塊->底層C++模塊->LibUV(deps uv)->拿到系統返回的一個fd(文件描述符),和 js代碼傳進來的回調函數callback,封裝成一個io觀察者(一個uv__io_s類型的對象),保存到default_loop_struct.

  2. 進入事件池, default_loop_struct保存對應io觀察着,V8 Engine處理js代碼, main函數調用libuv進入uv_run(), node進入事件循環 ,判斷是否有存活的觀察者

    • 如果也沒有io, Node進程退出
    • 如果有io觀察者, 執行uv_run()進入epoll_wait()線程掛起,io觀察者檢測是否有數據返回callback, 沒有數據則會一直在epoll_wait()等待執行 server.listen(3000)會掛起一直等待。

Module對象

根據CommonJS規範,每一個文件就是一個模塊,在每個模塊中,都會有一個module對象,這個對象就指向當前的模塊。

module對象具有以下屬性:

  • id:當前模塊的bi
  • exports:表示當前模塊暴露給外部的值
  • parent: 是一個對象,表示調用當前模塊的模塊
  • children:是一個對象,表示當前模塊調用的模塊
  • filename:模塊的絕對路徑
  • paths:從當前文件目錄開始查找node_modules目錄;然後依次進入父目錄,查找父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄
  • loaded:一個布爾值,表示當前模塊是否已經被完全加載

示例:

module.exports = { 
    name: 'fzxa',
    getAge: function(age){
            console.log(age)
    }   
}
console.log(module)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

執行node module.js 返回如下

Module {
  id: '.',
  exports: { name: 'fzxa', getAge: [Function: getAge] },
  parent: null,
  filename: '/Users/fzxa/Documents/study/module.js',
  loaded: false,
  children: [],
  paths: 
   [ '/Users/fzxa/Documents/study/node_modules',
     '/Users/fzxa/Documents/node_modules',
     '/Users/fzxa/node_modules',
     '/Users/node_modules',
     '/node_modules' ] }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

module對象具有一個exports屬性,該屬性就是用來對外暴露變量、方法或整個模塊的。當其他的文件require進來該模塊的時候,實際上就是讀取了該模塊module對象的exports屬性

exports對象

exportsmodule.exports都是引用類型的變量,而且這兩個對象指向同一塊內存地址

exports = module.exports = {};
  • 1

例子:

var module = {
    exports: {}
}

var exports = module.exports

function change(exports) {
    //爲形參添加屬性,是會同步到外部的module.exports對象的
    exports.name = "fzxa"
    //在這裏修改了exports的引用,並不會影響到module.exports
    exports = {
        age: 24
    }
    console.log(exports) //{ age: 24 }
}

change(exports)
console.log(module.exports) //{exports: {name: "fzxa"}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

直接給exports賦值,會改變當前模塊內部的形參exports對象的引用,也就是說當前的exports已經跟外部的module.exports對象沒有任何關係了,所以這個改變是不會影響到module.exports的

module.exports就是爲了解決上述exports直接賦值,會導致拋出不成功的問題而產生的。有了它,我們就可以這樣來拋出一個模塊了.

require方法

Node中引入模塊的機制步驟

  1. 路徑分析
  2. 文件定位
  3. 編譯執行
    Node對引入過的模塊也會進行緩存。不同的地方是,node緩存的是編譯執行之後的對象而不是靜態文件

Module._load的源碼:

Module._load = function(request, parent, isMain) {

  //  計算絕對路徑
  var filename = Module._resolveFilename(request, parent);

  //  第一步:如果有緩存,取出緩存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // 第二步:是否爲內置模塊
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 第三步:生成模塊實例,存入緩存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 第四步:加載模塊
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第五步:輸出模塊的exports屬性
  return module.exports;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

Module._load方法的內部調用了Module._findPath這個方法,這個方法是用來返回模塊的絕對路徑的,源碼如下:

Module._findPath = function(request, paths) {

  // 列出所有可能的後綴名:.js,.json, .node
  var exts = Object.keys(Module._extensions);

  // 如果是絕對路徑,就不再搜索
  if (request.charAt(0) === '/') {
    paths = [''];
  }

  // 是否有後綴的目錄斜槓
  var trailingSlash = (request.slice(-1) === '/');

  // 第一步:如果當前路徑已在緩存中,就直接返回緩存
  var cacheKey = JSON.stringify({request: request, paths: paths});
  if (Module._pathCache[cacheKey]) {
    return Module._pathCache[cacheKey];
  }

  // 第二步:依次遍歷所有路徑
  for (var i = 0, PL = paths.length; i < PL; i++) {
    var basePath = path.resolve(paths[i], request);
    var filename;

    if (!trailingSlash) {
      // 第三步:是否存在該模塊文件
      filename = tryFile(basePath);

      if (!filename && !trailingSlash) {
        // 第四步:該模塊文件加上後綴名,是否存在
        filename = tryExtensions(basePath, exts);
      }
    }

    // 第五步:目錄中是否存在 package.json 
    if (!filename) {
      filename = tryPackage(basePath, exts);
    }

    if (!filename) {
      // 第六步:是否存在目錄名 + index + 後綴名 
      filename = tryExtensions(path.resolve(basePath, 'index'), exts);
    }

    // 第七步:將找到的文件路徑存入返回緩存,然後返回
    if (filename) {
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
 }

  // 第八步:沒有找到文件,返回false 
  return false;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

當我們第一次引入一個模塊的時候,require的緩存機制會將我們引入的模塊加入到內存中,以提升二次加載的性能。但是,如果我們修改了被引入模塊的代碼之後,當再次引入該模塊的時候,就會發現那並不是我們最新的代碼,這是一個麻煩的事情。如何解決呢

require有如下方法:
require(): 加載外部模塊
require.resolve():將模塊名解析到一個絕對路徑
require.main:指向主模塊
require.cache:指向所有緩存的模塊
require.extensions:根據文件的後綴名,調用不同的執行函數

//刪除指定模塊的緩存
delete require.cache[require.resolve('/*被緩存的模塊名稱*/')]

// 刪除所有模塊的緩存
Object.keys(require.cache).forEach(function(key) {
     delete require.cache[key];
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

HTTP_Server

首先需要創建一個 http.Server 類的實例,然後監聽它的 request 事件

requestListener 回調函數作爲觀察者,監聽了 request 事件, 默認超時時間爲2分

lib/_http_server.js

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

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

  // Similar option to this. Too lazy to write my own docs.
  // http://www.squid-cache.org/Doc/config/half_closed_clients/
  // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
  this.httpAllowHalfOpen = false;

  this.on('connection', connectionListener);

  this.timeout = 2 * 60 * 1000;
  this.keepAliveTimeout = 5000;
  this._pendingResponseData = 0;
  this.maxHeadersCount = null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

觀察者 connectionListener 處理 connection 事件。
這時,則需要一個 HTTP parser 來解析通過 TCP 傳輸過來的數據:

lib/_http_server.js

function connectionListener(socket) {
  debug('SERVER new http connection');

  httpSocketSetup(socket);

  // Ensure that the server property of the socket is correctly set.
  // See https://github.com/nodejs/node/issues/13435
  if (socket.server === null)
    socket.server = this;

  // If the user has added a listener to the server,
  // request, or response, then it's their responsibility.
  // otherwise, destroy on timeout by default
  if (this.timeout)
    socket.setTimeout(this.timeout);
  socket.on('timeout', socketOnTimeout);

  var parser = parsers.alloc();
  parser.reinitialize(HTTPParser.REQUEST);
  parser.socket = socket;
  socket.parser = parser;
  parser.incoming = null;

  // Propagate headers limit from server instance to parser
  if (typeof this.maxHeadersCount === 'number') {
    parser.maxHeaderPairs = this.maxHeadersCount << 1;
  } else {
    // Set default value because parser may be reused from FreeList
    parser.maxHeaderPairs = 2000;
  }

  var state = { 
    onData: null,
    onEnd: null,
    onClose: null,
    onDrain: null,
    outgoing: [], 
    incoming: [], 
    // `outgoingData` is an approximate amount of bytes queued through all
    // inactive responses. If more data than the high watermark is queued - we
    // need to pause TCP socket/HTTP parser, and wait until the data will be
    // sent to the client.
    outgoingData: 0,
    keepAliveTimeoutSet: false
  };  
  state.onData = socketOnData.bind(undefined, this, socket, parser, state);
  state.onEnd = socketOnEnd.bind(undefined, this, socket, parser, state);
  state.onClose = socketOnClose.bind(undefined, socket, state);
  state.onDrain = socketOnDrain.bind(undefined, socket, state);
  socket.on('data', state.onData);
  socket.on('error', socketOnError);
  socket.on('end', state.onEnd);
  socket.on('close', state.onClose);
  socket.on('drain', state.onDrain);
  parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);

  // We are consuming socket, so it won't get any actual data
  socket.on('resume', onSocketResume);
  socket.on('pause', onSocketPause);

  // Override on to unconsume on `data`, `readable` listeners
  socket.on = socketOnWrap;

  // We only consume the socket if it has never been consumed before.
  var external = socket._handle._externalStream;
  if (!socket._handle._consumed && external) {
    parser._consumed = true;
    socket._handle._consumed = true;
    parser.consume(external);
  }
  parser[kOnExecute] =
    onParserExecute.bind(undefined, this, socket, parser, state);

  socket._paused = false;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章