亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

本文為來(lái)自 字節(jié)跳動(dòng)-國(guó)際化電商-S 項(xiàng)目團(tuán)隊(duì) 成員的文章,已授權(quán) ELab 發(fā)布。

一個(gè) TCP 連接的案例?TCP 服務(wù)端?
const.NET = require('net');

const server = new net.Server();

server.listen(9999, '127.0.0.1', () => {
   console.log(`server is listening on ${server.address().address}:${server.address().port}`);
});

server.on('connection', (socket) => {
   server.getConnections((err, connections) => {
    console.log('current clients is: ', connections);
   });

    socket.on('data', (data) => {
        console.log(`received data: ${data.toString()}`);
    });
});

?TCP 客戶端?
const net = require('net');

const client = new net.Socket();

client.connect({
    port: 9999,
    address: '127.0.0.1',
});

client.on('connect', () => {
   console.log('connect success');

    client.write(`Hello Server!, I'm ${Math.round(Math.random() * 100)}`);
});

疑問(wèn)?

NodeJS 代碼是如何跑起來(lái)的

TCP 連接在 NodeJS 中是如何保持一直監(jiān)聽(tīng)而進(jìn)程不中斷的

NodeJS 是如何處理并發(fā)連接的,當(dāng)遇到阻塞型調(diào)用時(shí)如何不阻塞主線程的

核心架構(gòu)

?NodeJS 架構(gòu)?

圖片

NodeJS 源碼分為三層:JS、C++ 以及 C。

JS 層

JS 層提供面向用戶的調(diào)用底層能力的接口,即各種 NodeJS 原生模塊,如 net、http、fs、DNS 以及 path 等

C++ 層

C++ 層主要通過(guò) V8 為 JS 層提供與底層交互的能力,起到類似橋梁的作用,通過(guò) V8 不僅實(shí)現(xiàn) JS 的解釋執(zhí)行,還擴(kuò)展的 JS 的能力邊界

C 層

C 層主要包括 Libuv 這一跨平臺(tái)的異步 IO 庫(kù)以及其他第三方 C 庫(kù)

啟動(dòng)過(guò)程

圖片

image.png

?分析?

注冊(cè) C++ 模塊

RegisterBuiltinModules 函數(shù)的作用是注冊(cè)一系列 C++ 模塊,通過(guò)宏定義展開(kāi),最終變成如下邏輯:

void RegisterBuiltinModules() {  
      _register_async_wrap();  
      _register_buffer();
      _register_fs();
      _register_url();
      // ...
    }

通過(guò)注冊(cè)函數(shù),將各個(gè) C++ 模塊維護(hù)在 modlist_internal 這一鏈表中,后續(xù)在原生 JS 模塊中調(diào)用 C++ 模塊時(shí)就可以根據(jù)模塊名找到對(duì)應(yīng)的模塊。

創(chuàng)建 Environment 對(duì)象

Environment 在 NodeJS 中是一個(gè)運(yùn)行時(shí)的環(huán)境對(duì)象,很多全局變量都托管在該類上,創(chuàng)建完 environment 后,就將其和 Context 進(jìn)行綁定,后續(xù) V8 可通過(guò) context 獲取 env 對(duì)象。

下面簡(jiǎn)單介紹一下 V8 的 isolate 、 context、scope、handle 等對(duì)象。

isolate 是一個(gè)獨(dú)立隔離實(shí)例的環(huán)境,同一時(shí)刻只能被一個(gè)線程進(jìn)入;

context 可以理解為執(zhí)行上下文對(duì)象,可以導(dǎo)入不同的環(huán)境變量和函數(shù);

Scope 指的是作用域,可看成是句柄的容器,一個(gè)作用域里面可以有很多個(gè)句柄;

HandleScope 是用來(lái)管理 Handle 的,而 Context::Scope 僅僅用來(lái)管理 Context 對(duì)象。

Handle 是 V8 引用對(duì)象的技術(shù)手段,Handle 分為 Local 和 Persistent 兩種。Local 是局部的,它同時(shí)被 HandleScope 進(jìn)行管理。 persistent,類似于全局的,不受 HandleScope 的管理,其作用域可以延伸到不同的函數(shù)。

圖片

初始化 loader 和執(zhí)行上下文

RunBootstrApping 主要調(diào)用了 BootstrapInternalLoaders 和 BootstrapNode 函數(shù)。

BootstrapInternalLoaders 用于編譯執(zhí)行 /lib/internal/bootstrap/loader.js,它的具體邏輯是為了NodeJS 能在JS層 通過(guò) binding 函數(shù)加載C++模塊,以便在原生 JS 模塊中調(diào)用 C++ 模塊。

BootstrapNode 用于初始化執(zhí)行上下文,暴露 global 對(duì)象在全局上下文中,編譯執(zhí)行 /lib/internal/bootstrap/node,從而設(shè)置一些全局變量或方法到 global 或者 process

// lib/internal/bootstrap/node.js

// proces 掛載一系列屬性方法
{
  process.dlopen = rawMethods.dlopen;
  process.uptime = rawMethods.uptime;

  // TODO(joyeecheung): either remove them or make them public
  process._getActiveRequests = rawMethods._getActiveRequests;
  process._getActiveHandles = rawMethods._getActiveHandles;

  // TODO(joyeecheung): remove these
  process.reallyExit = rawMethods.reallyExit;
  process._kill = rawMethods._kill;

  const wrapped = perThreadSetup.wrapProcessMethods(rawMethods);
  process._rawDebug = wrapped._rawDebug;
  process.hrtime = wrapped.hrtime;
  process.hrtime.bigint = wrapped.hrtimeBigInt;
  process.cpuUsage = wrapped.cpuUsage;
  process.resourceUsage = wrapped.resourceUsage;
  process.memoryUsage = wrapped.memoryUsage;
  process.kill = wrapped.kill;
  process.exit = wrapped.exit;

  process.openStdin = function() {
    process.stdin.resume();
    return process.stdin;
  };
}
// global 掛載一系列屬性和方法
if (!config.noBrowserGlobals) {
  // Override global console from the one provided by the VM
  // to the one implemented by Node.js
  // https://console.spec.whatwg.org/#console-namespace
  exposeNamespace(global, 'console', createGlobalConsole(global.console));

  const { URL, URLSearchParams } = require('internal/url');
  // https://url.spec.whatwg.org/#url
  exposeInterface(global, 'URL', URL);
  // https://url.spec.whatwg.org/#urlsearchparams
  exposeInterface(global, 'URLSearchParams', URLSearchParams);

  const {
    TextEncoder, TextDecoder
  } = require('internal/encoding');
  // https://encoding.spec.whatwg.org/#textencoder
  exposeInterface(global, 'TextEncoder', TextEncoder);
  // https://encoding.spec.whatwg.org/#textdecoder
  exposeInterface(global, 'TextDecoder', TextDecoder);

  // https://html.spec.whatwg.org/multipage/webappapis.html#windoworworkerglobalscope
  const timers = require('timers');
  defineOperation(global, 'clearInterval', timers.clearInterval);
  defineOperation(global, 'clearTimeout', timers.clearTimeout);
  defineOperation(global, 'setInterval', timers.setInterval);
  defineOperation(global, 'setTimeout', timers.setTimeout);

  defineOperation(global, 'queueMicrotask', queueMicrotask);

  // Non-standard extensions:
  defineOperation(global, 'clearImmediate', timers.clearImmediate);
  defineOperation(global, 'setImmediate', timers.setImmediate);
}
// ...

初始化 Libuv

這里對(duì)事件循環(huán)的部分階段做一些初始化的操作,創(chuàng)建一個(gè)默認(rèn)的 event_loop 結(jié)構(gòu)體用于管理后續(xù)各個(gè)階段產(chǎn)生的任務(wù)

void Environment::InitializeLibuv(bool start_profiler_idle_notifier) {
  HandleScope handle_scope(isolate());
  Context::Scope context_scope(context());

  CHECK_EQ(0, uv_timer_init(event_loop(), timer_handle()));
  uv_unref(reinterpret_cast<uv_handle_t*>(timer_handle()));

  uv_check_init(event_loop(), immediate_check_handle());
  uv_unref(reinterpret_cast<uv_handle_t*>(immediate_check_handle()));

  uv_idle_init(event_loop(), immediate_idle_handle());

  uv_check_start(immediate_check_handle(), CheckImmediate);
  uv_prepare_init(event_loop(), &idle_prepare_handle_);
  uv_check_init(event_loop(), &idle_check_handle_);
  uv_async_init(
      event_loop(),
      &task_queues_async_,
      [](uv_async_t* async) {
        Environment* env = ContainerOf(
            &Environment::task_queues_async_, async);
        env->CleanupFinalizationGroups();
        env->RunAndClearNativeImmediates();
      });
  uv_unref(reinterpret_cast<uv_handle_t*>(&idle_prepare_handle_));
  uv_unref(reinterpret_cast<uv_handle_t*>(&idle_check_handle_));
  uv_unref(reinterpret_cast<uv_handle_t*>(&task_queues_async_));
  // ...
}

執(zhí)行用戶 JS 代碼

StartExecution 用于加載用戶 JS 代碼并執(zhí)行

// src/node.cc
MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
  // ...
  if (!first_argv.empty() && first_argv != "-") {
    return StartExecution(env, "internal/main/run_main_module");
  }
  // ...
}
 
// lib/internal/main/run_main_module.js
require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);

進(jìn)入 Libuv 事件循環(huán)

執(zhí)行完用戶 JS 代碼,用戶代碼就會(huì)往 Libuv 中注冊(cè)一些任務(wù),然后進(jìn)入整個(gè)事件循環(huán),直到?jīng)]有待處理的任務(wù),Libuv 則會(huì)退出事件循環(huán),進(jìn)而退出 NodeJS 進(jìn)程。

// src/node_main_instance.cc
  do {
    uv_run(env->event_loop(), UV_RUN_DEFAULT);

    per_process::v8_platform.DrainVMTasks(isolate_);

    more = uv_loop_alive(env->event_loop());
    if (more && !env->is_stopping()) continue;

    if (!uv_loop_alive(env->event_loop())) {
      EmitBeforeExit(env.get());
    }

    // 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 && !env->is_stopping());

?源代碼概覽?

// src/node_main.cc
int main(int argc, char* argv[]) {
    return node::Start(argc, argv);
}
 
// src/node.cc
namespace node {
    int Start(int argc, char** argv) {
        
        InitializationResult result = InitializeOncePerProcess(argc, argv);
        // ...
        
        NodeMainInstance main_instance(&params,
                               uv_default_loop(),
                               per_process::v8_platform.Platform(),
                               result.args,
                               result.exec_args,
                               indexes);
        result.exit_code = main_instance.Run();
    }
    
    InitializationResult InitializeOncePerProcess(int argc, char** argv) {
        // ...
      {
        result.exit_code =
            InitializeNodeWithArgs(&(result.args), &(result.exec_args), &errors);
            //...
      }
    
      V8::Initialize();
      return result;
    }
    
    int InitializeNodeWithArgs(std::vector<std::string>* argv,
                               std::vector<std::string>* exec_argv,
                               std::vector<std::string>* errors) {
      
      // ...
      // Register built-in modules
      binding::RegisterBuiltinModules();
      // ...
    }
    
    MaybeLocal<Value> Environment::RunBootstrapping() {
      EscapableHandleScope scope(isolate_);
    
      //...
      if (BootstrapInternalLoaders().IsEmpty()) {
        return MaybeLocal<Value>();
      }
    
      Local<Value> result;
      if (!BootstrapNode().ToLocal(&result)) {
        return MaybeLocal<Value>();
      }
      
      //...
    
      return scope.Escape(result);
    }
}
 
// src/node_main_instance.cc
namespace node {
    int NodeMainInstance::Run() {
      // ...
      DeleteFnPtr<Environment, FreeEnvironment> env =
          CreateMainEnvironment(&exit_code);
    
      if (exit_code == 0) {
        LoadEnvironment(env.get());
        // ...
        {
          // ...
          do {
            uv_run(env->event_loop(), UV_RUN_DEFAULT);
    
            per_process::v8_platform.DrainVMTasks(isolate_);
    
            more = uv_loop_alive(env->event_loop());
            if (more && !env->is_stopping()) continue;
    
            if (!uv_loop_alive(env->event_loop())) {
              EmitBeforeExit(env.get());
            }
    
            // 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 && !env->is_stopping());
        }
      }
      // ...
    }
    
    NodeMainInstance::CreateMainEnvironment(int* exit_code) {

      // ...
      context = NewContext(isolate_);
      Context::Scope context_scope(context);
    
      DeleteFnPtr<Environment, FreeEnvironment> env { CreateEnvironment(
          isolate_data_.get(),
          context,
          args_,
          exec_args_,
          EnvironmentFlags::kDefaultFlags) };
    
      return env;
    }
}
 
// src/environment.cc
namespace node {
    void LoadEnvironment(Environment* env) {
      USE(LoadEnvironment(env,
                          StartExecutionCallback{},
                          {}));
    }
    
    MaybeLocal<Value> LoadEnvironment(
        Environment* env,
        StartExecutionCallback cb,
        std::unique_ptr<InspectorParentHandle> removeme) {
      env->InitializeLibuv(per_process::v8_is_profiling);
      env->InitializeDiagnostics();
    
      return StartExecution(env, cb);
    }
    
    Environment* CreateEnvironment(IsolateData* isolate_data,
                               Local<Context> context,
                               int argc,
                               const char* const* argv,
                               int exec_argc,
                               const char* const* exec_argv) {
      return CreateEnvironment(
          isolate_data, context,
          std::vector<std::string>(argv, argv + argc),
          std::vector<std::string>(exec_argv, exec_argv + exec_argc));
    }
    
    Environment* CreateEnvironment(
        IsolateData* isolate_data,
        Local<Context> context,
        const std::vector<std::string>& args,
        const std::vector<std::string>& exec_args,
        EnvironmentFlags::Flags flags,
        ThreadId thread_id,
        std::unique_ptr<InspectorParentHandle> inspector_parent_handle) {
      Isolate* isolate = context->GetIsolate();
      HandleScope handle_scope(isolate);
      Context::Scope context_scope(context);
      // TODO(addaleax): This is a much better place for parsing per-Environment
      // options than the global parse call.
      Environment* env = new Environment(
          isolate_data,
          context,
          args,
          exec_args,
          flags,
          thread_id);
      // ... 
      if (env->RunBootstrapping().IsEmpty()) {
        FreeEnvironment(env);
        return nullptr;
      }
    
      return env;
    }
}

Libuv 架構(gòu)

Libuv 是 NodeJS 的核心組件,是一個(gè)跨平臺(tái)的處理異步 I/O 請(qǐng)求的 C 庫(kù),從架構(gòu)來(lái)看,它把各類請(qǐng)求主要分為兩大類:網(wǎng)絡(luò) I/O 相關(guān)請(qǐng)求,以及文件 I/O、DNS Ops 以及 User code 組成的請(qǐng)求。

對(duì)于網(wǎng)絡(luò) I/O 相關(guān)請(qǐng)求,根據(jù) OS 平臺(tái)的不同,分別采用了 linux 的 epoll、OSX 和 BSD 類 OS 的 kqueue、SunOS 的 event ports 以及 windows 的 IOCP 等 I/O 讀寫機(jī)制。

對(duì)于 File I/O 為代表的請(qǐng)求,則使用線程池實(shí)現(xiàn)異步請(qǐng)求處理,具有更好的跨平臺(tái)特性。

事件循環(huán) event loop

在 Node 應(yīng)用啟動(dòng)后,就會(huì)進(jìn)入 Libuv 事件循環(huán)中,每一輪循環(huán) Libuv 都會(huì)處理維護(hù)在各個(gè)階段的任務(wù)隊(duì)列的回調(diào)節(jié)點(diǎn),在回調(diào)節(jié)點(diǎn)中可能會(huì)產(chǎn)生新的任務(wù),任務(wù)可能在當(dāng)前循環(huán)或是下個(gè)循環(huán)繼續(xù)被處理。

以下是 Libuv 的執(zhí)行流程圖:

下面簡(jiǎn)述一下各個(gè)階段代表的含義:

  1. 首先判斷當(dāng)前事件循環(huán)是否處于 alive 狀態(tài),否則退出整個(gè)事件循環(huán)。alive 狀態(tài)表示是否有 active 狀態(tài)的 handle 和 request,closing 狀態(tài)的 handle
  2. 基于系統(tǒng)時(shí)間更新時(shí)間戳
  3. 判斷由定時(shí)器組成的小頂堆中那個(gè)節(jié)點(diǎn)超時(shí),超時(shí)則執(zhí)行定時(shí)器回調(diào)
  4. 執(zhí)行 pending 回調(diào)任務(wù),一般 I/O 回調(diào)添加的錯(cuò)誤或?qū)憯?shù)據(jù)成功的任務(wù)都會(huì)在下一個(gè)事件循環(huán)的 pending 階段執(zhí)行
  5. 執(zhí)行 idle 階段的回調(diào)任務(wù)
  6. 執(zhí)行 prepare 階段的回調(diào)任務(wù)
  7. 調(diào)用各平臺(tái)的 I/O 讀寫接口,最多等待 timeout 時(shí)間(定時(shí)器最快過(guò)期時(shí)間),期間如果有數(shù)據(jù)返回,則執(zhí)行 I/O 對(duì)應(yīng)的回調(diào)
  8. 執(zhí)行 check 階段的回調(diào)任務(wù)
  9. 執(zhí)行 closing 階段的回調(diào)任務(wù)
  10. 重新回到流程 1

?源碼概覽?

// src/unix/core.c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int can_sleep;

  r = uv__loop_alive(loop);
  if (!r)
    uv__update_time(loop);

  while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);

    can_sleep =
        QUEUE_EMPTY(&loop->pending_queue) && QUEUE_EMPTY(&loop->idle_handles);

    uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
      timeout = uv__backend_timeout(loop);

    uv__metrics_inc_loop_count(loop);
    // 動(dòng)態(tài)設(shè)置 epoll_wait 的超時(shí)時(shí)間
    uv__io_poll(loop, timeout);

    for (r = 0; r < 8 && !QUEUE_EMPTY(&loop->pending_queue); r++)
      uv__run_pending(loop);

    uv__metrics_update_idle_time(loop);

    uv__run_check(loop);
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

任務(wù)調(diào)度

了解了 Libuv 的事件循環(huán)流程,接下來(lái)結(jié)合 JS 代碼具體看看 NodeJS 是如何進(jìn)行任務(wù)調(diào)度的。

image.png

目前,主要有五種主要類型的隊(duì)列被 Libuv 的事件循環(huán)所處理:

  • 過(guò)期或是定期的時(shí)間隊(duì)列,由 setTimeout 或 setInterval 函數(shù)所添加的任務(wù)
  • Pending 隊(duì)列,主要存放讀寫成功或是錯(cuò)誤的回調(diào)
  • I/O 事件隊(duì)列,主要存放完成 I/O 事件時(shí)的回調(diào)
  • Immediates 隊(duì)列,采用 setImmediate 函數(shù)添加的任務(wù),對(duì)應(yīng) libuv 的 check 階段
  • Close 任務(wù)隊(duì)列,主要存放 close 事件回調(diào)

除了以上五種主要的任務(wù)列表,還有額外兩種不屬于 libuv 而是作為 NodeJS 一部分的任務(wù)隊(duì)列:

  • Next Ticks 隊(duì)列,采用 process.nextTick 添加的任務(wù)
  • 其他微任務(wù)隊(duì)列,例如 promise callback 等

nextTicks 隊(duì)列和其他微任務(wù)隊(duì)列會(huì)在事件循環(huán)每一階段穿插調(diào)用,nextTicks 優(yōu)先級(jí)會(huì)比其他微任務(wù)隊(duì)列更高。

?示例?

// timer -> pending -> idle -> prepare -> poll io -> check -> close

// timer phase
setTimeout(() => {
    Promise.resolve().then(() => {
        console.log('promise resolve in timeout');
        process.nextTick(() => {
            console.log('tick task in timeout promise');
        });
    })
    process.nextTick(() => {
        console.log('tick task in timeout');
        process.nextTick(() => {
            console.log("tick task in timeout->tick");
        })
    });
    console.log('timer task');

}, 0);

// check phase
setImmediate(() => {
    process.nextTick(() => {
        console.log('imeediate->tick task')
    });
    console.log('immediate task');
});

Promise.resolve().then(() => {
    console.log('promise resolve');
});

process.nextTick(() => {
    console.log("tick task");
});

console.log('run main thread');
 
// result
run main thread
tick task
promise resolve
timer task
tick task in timeout
tick task in timeout->tick
promise resolve in timeout
tick task in timeout promise
immediate task
imeediate->tick task

現(xiàn)在解讀一下以上的執(zhí)行流程:

NodeJS 在經(jīng)過(guò)一系列初始化工作后,開(kāi)始執(zhí)行用戶 JS 代碼,解釋執(zhí)行過(guò)程中,分別把 setTimeout、setImmediate、Promise、nextTick 函數(shù)的回調(diào)插入 timer、immediate、microtask 和 nexttick 隊(duì)列。

  1. 執(zhí)行主線程 console.log("run main thread"),打印 "run main thread"
  2. 進(jìn)入 timer 階段前,發(fā)現(xiàn) nextTick 、promise 隊(duì)列有任務(wù)如下:
nextTicks = [() => {
    console.log("tick task");
}];

microtasks = [() => {
    console.log('promise resolve');
}];

分別打印 "tick task" 以及 "promise resolve"

  1. 進(jìn)入 timer 階段,執(zhí)行定時(shí)器回調(diào),定時(shí)器回調(diào)中再次往 microtask 和 nextTick 插入新的任務(wù)如下:
nextTicks = [() => {
        console.log('tick task in timeout');
        process.nextTick(() => {
            console.log("tick task in timeout->tick");
        })
    }];

microtasks = [() => {
        console.log('promise resolve in timeout');
        process.nextTick(() => {
            console.log('tick task in timeout promise');
        });
    }];

打印主線程任務(wù)中的 "timer task",再進(jìn)入下一階段,發(fā)現(xiàn) nextTicks 和 microtasks 隊(duì)列為非空,執(zhí)行微任務(wù)。由于 nextTicks 優(yōu)先級(jí)更高,先打印 "tick task in timeout",然后又往 nextTicks 插入 () => {console.log("tick task in timeout->tick")} ,繼續(xù)執(zhí)行 nextTicks 任務(wù)打印 "tick task in timeout->tick"。

此時(shí) nextTicks 隊(duì)列已空,執(zhí)行 miacrotasks 隊(duì)列,打印 "promise resolve in timeout",此時(shí)又往 nextTicks 插入任務(wù) () => {console.log('tick task in timeout promise')}?,繼續(xù)執(zhí)行 nextTick 任務(wù),打印 "tick task in timeout promise"。

進(jìn)入 check 階段(Immediate),為 nextTicks 添加 () => {console.log('imeediate->tick task') }?,主線程打印 "immediate task",進(jìn)入下一階段前先執(zhí)行 nextTicks 任務(wù),打印 'imeediate->tick task'。

?拓展?

setImmediate(() => console.log('this is set immediate 1'));
setImmediate(() => console.log('this is set immediate 2'));
setImmediate(() => console.log('this is set immediate 3'));

setTimeout(() => console.log('this is set timeout 1'), 0);
setTimeout(() => {
    console.log('this is set timeout 2');
    process.nextTick(() => console.log('this is process.nextTick added inside setTimeout'));
}, 0);
setTimeout(() => console.log('this is set timeout 3'), 0);
setTimeout(() => console.log('this is set timeout 4'), 0);
setTimeout(() => console.log('this is set timeout 5'), 0);

process.nextTick(() => console.log('this is process.nextTick 1'));
process.nextTick(() => {
    process.nextTick(console.log.bind(console, 'this is the inner next tick inside next tick'));
});
process.nextTick(() => console.log('this is process.nextTick 2'));
process.nextTick(() => console.log('this is process.nextTick 3'));
process.nextTick(() => console.log('this is process.nextTick 4'));

I/O 模型

得益于 Libuv 這一跨平臺(tái)的高性能異步 I/O 庫(kù),使得 NodeJS 在處理 I/O 密集型任務(wù)上十分彰顯優(yōu)勢(shì)。下面結(jié)合不同的 I/O 模型,對(duì)比分析一下 NodeJS 目前工程實(shí)踐所采用的 I/O 模型的優(yōu)越性。

首先理清一下阻塞和非阻塞、異步和同步的概念:

  • 阻塞和非阻塞

在應(yīng)用程序通過(guò) I/O 函數(shù)申請(qǐng)讀寫數(shù)據(jù)時(shí),如果在數(shù)據(jù)就緒前進(jìn)程一直在等待的,就是阻塞 I/O,即發(fā)起 I/O 請(qǐng)求時(shí)是阻塞的

  • 異步和同步

數(shù)據(jù)從內(nèi)核緩沖區(qū)到到用戶內(nèi)存復(fù)制過(guò)程中,需要用戶進(jìn)程等待,就是同步 I/O,即實(shí)際的 I/O 讀寫是同步的

?同步阻塞?

圖片來(lái)源:https://www.51cto.com/article/693213.html

在網(wǎng)絡(luò)編程中,當(dāng)調(diào)用 recvfrom 獲取客戶端數(shù)據(jù)時(shí),首先會(huì)阻塞進(jìn)程,等待數(shù)據(jù)通過(guò)網(wǎng)卡到內(nèi)核緩沖區(qū);當(dāng)數(shù)據(jù)就緒后,再同步等待指代數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶空間,此時(shí)用戶進(jìn)程再進(jìn)行數(shù)據(jù)處理。

同步阻塞 I/O 模型是最簡(jiǎn)單的 I/O 模型,好處是使用簡(jiǎn)單,通常在 fd 較少、數(shù)據(jù)就緒很快的場(chǎng)景,缺點(diǎn)是如果內(nèi)核數(shù)據(jù)一直沒(méi)準(zhǔn)備好,則用戶進(jìn)程將會(huì)一直阻塞無(wú)法執(zhí)行后續(xù)任務(wù)。

以網(wǎng)絡(luò)編程為例,默認(rèn)情況下socket 是 blocking 的,即函數(shù) accept , recvfrom 等,需等待函數(shù)執(zhí)行結(jié)束之后才能夠返回(此時(shí)操作系統(tǒng)切換到其他進(jìn)程執(zhí)行)。accpet 等待到有 client 連接請(qǐng)求并接受成功之后,recvfrom 需要讀取完client 發(fā)送的數(shù)據(jù)之后才能夠返回

// 創(chuàng)建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// IP 和端口配置 ...
// ...
// 綁定 IP 和端口
bind(serv_sock, ...);
// 監(jiān)聽(tīng)來(lái)自監(jiān)聽(tīng)型套接字的請(qǐng)求
listen(serv_sock, ...);

int clnt_sock;
while (1) {
    // 創(chuàng)建新的通信型套接字用于接收來(lái)自客戶端的請(qǐng)求,此時(shí)會(huì)阻塞程序執(zhí)行,直到有請(qǐng)求到來(lái)
    clnt_sock = accept(serv_sock, ...);
    // 接收客戶端的數(shù)據(jù),同步阻塞 I/O,等待數(shù)據(jù)就緒
    recvfrom(clnt_sock, ...);
    // 處理數(shù)據(jù)
    handle(data);
}

?同步非阻塞?

圖片來(lái)源:https://www.51cto.com/article/693213.html

同步非阻塞 I/O 的特點(diǎn)是當(dāng)用戶進(jìn)程發(fā)起網(wǎng)絡(luò)讀請(qǐng)求時(shí),如果內(nèi)核緩沖區(qū)還沒(méi)接收到客戶端數(shù)據(jù),會(huì)立即返回 EWOULDBLOCK 錯(cuò)誤碼,而不會(huì)阻塞用戶進(jìn)程,用戶進(jìn)程可結(jié)合輪詢調(diào)用方式繼續(xù)發(fā)起 recvfrom 調(diào)用,直到數(shù)據(jù)就緒,然后同步等待數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶空間,然后用戶進(jìn)程進(jìn)行數(shù)據(jù)處理。

同步非阻塞 I/O 的優(yōu)勢(shì)在于當(dāng)發(fā)起 I/O 請(qǐng)求時(shí)不會(huì)阻塞用戶進(jìn)程,一定程度上提升了程序的性能,但是為了及時(shí)獲取數(shù)據(jù)的就緒狀態(tài),需要頻繁輪詢,這樣也會(huì)消耗不小的 CPU 資源。

以網(wǎng)絡(luò)編程為例,可設(shè)置 socket 為 non-blocking 模式,使用 socket()創(chuàng)建的 socket 默認(rèn)是阻塞的;可使用函數(shù) fcntl 可設(shè)置創(chuàng)建的 socket 為非阻塞的,這樣使用原本 blocking 的各種函數(shù)(accept、recvfrom),可以立即獲得返回結(jié)果。通過(guò)判斷返回的errno了解狀態(tài):

這樣就實(shí)現(xiàn)同步非阻塞 I/O 請(qǐng)求:

// 創(chuàng)建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 設(shè)置 socket 為非阻塞 I/O
int flags = fcntl(serv_sock, F_GETFL, 0);
fcntl(serv_sock, F_SETFL, flags | O_NONBLOCK);


// IP 和端口配置 ...
// ...
// 綁定 IP 和端口
bind(serv_sock, ...);
// 監(jiān)聽(tīng)來(lái)自監(jiān)聽(tīng)型套接字的請(qǐng)求
listen(serv_sock, ...);


// 創(chuàng)建新的通信型套接字用于接收來(lái)自客戶端的請(qǐng)求
int clnt_sock;

while (1) {
    // 在non-blocking模式下,如果返回值為-1,且 errno == EAGAIN 或errno == EWOULDBLOCK 表示no connections 沒(méi)有新連接請(qǐng)求;
    clnt_sock = accept(serv_sock, ...);
    if (clnt_sock == -1 && errno == EAGAIN) {
        fprintf(stderr, "no client connectionsn");
        continue;
    } else if (clnt_sock == -1) {
        perror("accept failed");
    }
    
    // 接收客戶端的數(shù)據(jù),同步非阻塞 I/O,在non-blocking模式下,如果返回值為-1,且 errno == EAGAIN表示沒(méi)有可接受的數(shù)據(jù)或正在接受尚未完成;
    while (1) {
        int ret = recvfrom(clnt_sock, ...);
        if (ret == -1 && errno == EAGAIN) {
        fprintf(stderr, "no data readyn");
            continue;
        } else if (ret == -1) {
            perror("read failed");
        }
        // 處理數(shù)據(jù)
        handle(data);
    }
}

?I/O 多路復(fù)用?

圖片來(lái)源:https://www.51cto.com/article/693213.html

上述兩種 I/O 模型均是面向單個(gè)客戶端連接的,同一時(shí)間只能處理一個(gè) client 請(qǐng)求,雖然可以通過(guò)多進(jìn)程/多線程的方法解決,但是多進(jìn)程/多線程需要考慮額外的資源消耗以及同步互斥的相關(guān)問(wèn)題。

為了高效解決多個(gè) fd 的狀態(tài)監(jiān)聽(tīng),I/O 多路復(fù)用技術(shù)應(yīng)運(yùn)而生。

I/O 多路復(fù)用的核心思想是可以同時(shí)監(jiān)聽(tīng)多個(gè)不同的 fd(網(wǎng)絡(luò)環(huán)境下即是網(wǎng)絡(luò)套接字),當(dāng)套接字中的任何一個(gè)數(shù)據(jù)就緒了,就可以通知用戶進(jìn)程,此時(shí)用戶進(jìn)程再發(fā)起 recvfrom 請(qǐng)求去讀取數(shù)據(jù)。

以網(wǎng)絡(luò)編程為例,可通過(guò)維護(hù)一個(gè)需要監(jiān)聽(tīng)的所有 socket 的 fd 列表,然后調(diào)用 select/epoll 等監(jiān)聽(tīng)函數(shù),如果 fd 列表中所有 socket 都沒(méi)有數(shù)據(jù)就緒,則 select/epoll 會(huì)阻塞,直到有一個(gè) socket 接收到數(shù)據(jù),然后喚醒進(jìn)程。

int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 設(shè)置 socket 為非阻塞 I/O
int flags = fcntl(serv_sock, F_GETFL, 0);
fcntl(serv_sock, F_SETFL, flags | O_NONBLOCK);

// IP 和端口配置 ...
// ...
// 綁定 IP 和端口
bind(serv_sock, ...);
// 監(jiān)聽(tīng)來(lái)自監(jiān)聽(tīng)型套接字的請(qǐng)求
listen(serv_sock, ...);

// 存放需要監(jiān)聽(tīng)的 socket 列表
fd_set readfds;
// 添加 需要監(jiān)聽(tīng)的 socket 到 readfds
FD_SET(serv_sock, readfds);

// 創(chuàng)建新的通信型套接字用于接收來(lái)自客戶端的請(qǐng)求
int clnt_sock;
// 調(diào)用 select 返回的結(jié)果值
int res;
// 預(yù)計(jì)可接受的最大連接數(shù)量,select 最大支持 1024 個(gè) fd
int maxfd = 1000;
while (1) {
    // 調(diào)用 select 阻塞監(jiān)聽(tīng) fd 列表,直到有一個(gè) socket 接收到請(qǐng)求,喚醒進(jìn)程
    res = select(maxfd + 1, &readfds, ...);
    if (res == -1) {
        perror("select failed");
        exit(EXIT_FAILURE);
    } else if (res == 0) {
        fprintf(stderr, "no socket ready for readn");
    }
    // 遍歷每個(gè) socket,如果是 serv_sock 則 accept,否則進(jìn)行讀操作
    for (int i = 0; i <= maxfd; i++) {
        // 是否 socket 是否在 監(jiān)聽(tīng)的 fd 列表中
        if (!FD_ISSET(i, &readfds)) {
            continue;
        }
        
        if (i == serv_sock) {
            // 當(dāng)前請(qǐng)求是 server sock,則建立 accept 連接
            clnt_sock = accpet(serv_sock, ...);
            // 將新建立的客戶端連接添加進(jìn)行 readfds 監(jiān)聽(tīng)列表中
            FD_SET(clnt_sock, &readfds);
        } else {
            // 當(dāng)請(qǐng)求是客戶端的 socket,接收客戶端的數(shù)據(jù),此時(shí)數(shù)據(jù)已經(jīng)就緒,將數(shù)據(jù)從內(nèi)核空間復(fù)制到用戶空間
            int ret = recvfrom(i, ...);
            if (ret == -1 && errno == EAGAIN) {
            fprintf(stderr, "no data readyn");
                continue;
            } else if (ret == -1) {
                perror("read failed");
            }
            // 處理數(shù)據(jù)
            handle(data);
        }
    }
}

上面是使用 select 函數(shù)實(shí)現(xiàn)的 I/O 多路復(fù)用,實(shí)際在 Libuv 采用的是 epoll 函數(shù),epoll 函數(shù)是為了解決 select 的以下缺點(diǎn)而誕生的:

  • 監(jiān)聽(tīng)的 I/O 最大連接數(shù)量有限,Libux 系統(tǒng)下一般為 1024
  • 一方面,監(jiān)聽(tīng)的 fd 列表需要從用戶空間傳遞到內(nèi)核空間進(jìn)行 socket 列表的監(jiān)聽(tīng);另一方面,當(dāng)數(shù)據(jù)就緒后,又需要從內(nèi)核空間復(fù)制到用戶空間,隨著監(jiān)聽(tīng)的 fd 數(shù)量增長(zhǎng),效率也會(huì)下降。
  • 此外,select 函數(shù)返回后,每次都需要遍歷一遍監(jiān)聽(tīng)的 fd 列表,找到數(shù)據(jù)就緒的 fd。

圖片來(lái)源:https://www.51cto.com/article/693213.html

epoll 的優(yōu)勢(shì)在于:

  • 基于事件驅(qū)動(dòng),每次只返回就緒的 fd,避免所有 fd 的遍歷操作
  • epoll 的 fd 數(shù)量上限是操作系統(tǒng)最大的文件句柄數(shù)目,一般與內(nèi)存相關(guān)
  • 底層使用紅黑樹(shù)管理監(jiān)聽(tīng)的 fd 列表,紅黑樹(shù)在增刪、查詢等操作上時(shí)間復(fù)雜度均是 logN,效率較高
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 設(shè)置 socket 為非阻塞 I/O
int flags = fcntl(serv_sock, F_GETFL, 0);
fcntl(serv_sock, F_SETFL, flags | O_NONBLOCK);

// IP 和端口配置 ...
// ...
// 綁定 IP 和端口
bind(serv_sock, ...);
// 監(jiān)聽(tīng)來(lái)自監(jiān)聽(tīng)型套接字的請(qǐng)求
listen(serv_sock, ...);

// 創(chuàng)建 epoll 對(duì)象
int MAX_EVENTS = 5; // 告訴內(nèi)核可能需要監(jiān)聽(tīng)的 fd 數(shù)量,如果使用時(shí)大于該數(shù),則內(nèi)核會(huì)申請(qǐng)動(dòng)態(tài)申請(qǐng)工多空間
int epoll_fd = epoll_create(MAX_EVENTS);

if (epoll_fd == -1) {
    printf("epoll_create error!n");
    return -1;
}

// 注冊(cè) serv_sock 所監(jiān)聽(tīng)的事件
struct epoll_event ev;
struct epoll_event events[MAX_EVENTS];

ev.data.fd = serv_sock; // 設(shè)置該事件的 fd 為 serv_sock
ev.events = EPOLLIN; // 設(shè)置監(jiān)聽(tīng) serv_sock 的可讀事件


// 添加 serv_sock 到 epoll 的可讀事件監(jiān)聽(tīng)隊(duì)列中
int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, serv_sock, &ev);

if (ret == -1) {
    printf("epoll_ctl error!n");
    return -1;
}

int connfd = 0; // 與客戶端連接成功后的通信型 fd
while (1) {
    // int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
    // 等待 epoll_fd 中的事件,如果 serv_sock 有可讀事件發(fā)生,則函數(shù)返回就緒后的 fd 數(shù)量
    // 最后一個(gè) timeout 參數(shù)可用來(lái)控制 epoll_wait 的等待事件發(fā)生的時(shí)間,-1 為阻塞等待,0 為非阻塞立即返回
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    
    for (int i = 0; i < nfds; i++) {
        // 客戶端發(fā)起請(qǐng)求
        if (events[i].data.fd == serv_sock) {
            connfd = accept(serv_sock, ...);
            if (connfd == -1) {
                printf("accept error!n");
            }
            
            ev.data.fd = connfd; // 設(shè)置該事件的 fd 為當(dāng)前的 connfd
            ev.events = EPOLLIN; // 設(shè)置當(dāng)前的 connfd 的可讀事件
             
             // 添加當(dāng)前 connfd 到 epoll 的可讀事件監(jiān)聽(tīng)隊(duì)列中
            if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, connfd, &ev) == -1) {
                printf("epoll_ctl add error!n");
                return -1;
            }
        } else {
            // 某個(gè)來(lái)自客戶端的請(qǐng)求的數(shù)據(jù)已就緒
            int ret = recvfrom(i, ...);
            if (ret == -1 && errno == EAGAIN) {
            fprintf(stderr, "no data readyn");
                continue;
            } else if (ret == -1) {
                perror("read failed");
            }
            // 處理數(shù)據(jù)
            handle(data);
        }
    }
}

以上就是基于 epoll 機(jī)制的事件驅(qū)動(dòng)型的 I/O 多路復(fù)用模型,服務(wù)器通過(guò)注冊(cè)文件描述符及其對(duì)應(yīng)監(jiān)聽(tīng)的事件到 epoll(epoll_ctl),epoll 開(kāi)始阻塞監(jiān)聽(tīng)事件直到有某個(gè) fd 的監(jiān)聽(tīng)事件觸發(fā)(epoll_wait),然后就遍歷就緒事件,根據(jù) fd 類型的不同執(zhí)行不同的任務(wù)。

服務(wù)器架構(gòu)

?單進(jìn)程單線程·串行模型?

int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// IP 和端口配置 ...
// ...
// 綁定 IP 和端口
bind(serv_sock, ...);
// 監(jiān)聽(tīng)來(lái)自監(jiān)聽(tīng)型套接字的請(qǐng)求
listen(serv_sock, ...);

int clnt_sock;
while (1) {
    // 創(chuàng)建新的通信型套接字用于接收來(lái)自客戶端的請(qǐng)求,此時(shí)會(huì)阻塞程序執(zhí)行,直到有請(qǐng)求到來(lái)
    clnt_sock = accept(serv_sock, ...);
    // 接收客戶端的數(shù)據(jù),同步阻塞 I/O,等待數(shù)據(jù)就緒
    recvfrom(clnt_sock, ...);
    // 處理數(shù)據(jù)
    handle(data);
}

單進(jìn)程單線程·串行處理請(qǐng)求是最簡(jiǎn)單的服務(wù)器架構(gòu),先從經(jīng)過(guò)三次握手,然后從連接隊(duì)列中獲取客戶端連接節(jié)點(diǎn)(accept 返回的套接字),然后從客戶端的套接字獲取數(shù)據(jù)進(jìn)行處理,接下來(lái)再進(jìn)行下個(gè)連接節(jié)點(diǎn)處理。

在并發(fā)連接數(shù)較大的情況下,并且采用的是阻塞式 I/O 模型,那么處理客戶端連接的效率就會(huì)非常低。

?多進(jìn)程/多線程?

單進(jìn)程串行處理請(qǐng)求因?yàn)樽枞?I/O 導(dǎo)致連接隊(duì)列中的節(jié)點(diǎn)被阻塞導(dǎo)致處理效率低下,通過(guò)把請(qǐng)求分給多個(gè)進(jìn)程處理從而提升效率,人多力量大。在多進(jìn)程/多線程架構(gòu)下,如果一個(gè)請(qǐng)求發(fā)送阻塞 I/O,那么操作系統(tǒng)會(huì)掛起該進(jìn)程,接著調(diào)度其他進(jìn)程,實(shí)現(xiàn)并發(fā)處理能力的提高。

但這種架構(gòu)模式下的性能瓶頸在于系統(tǒng)的進(jìn)程數(shù)、線程數(shù)是有限的,開(kāi)辟進(jìn)程和線程的開(kāi)銷也是需要考慮的問(wèn)題,系統(tǒng)資源消耗高。

int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// IP 和端口配置 ...
// ...
// 綁定 IP 和端口
bind(serv_sock, ...);
// 監(jiān)聽(tīng)來(lái)自監(jiān)聽(tīng)型套接字的請(qǐng)求
listen(serv_sock, ...);

int clnt_sock;
int pid;
while (1) {
    // 創(chuàng)建新的通信型套接字用于接收來(lái)自客戶端的請(qǐng)求,此時(shí)會(huì)阻塞程序執(zhí)行,直到有請(qǐng)求到來(lái)
    clnt_sock = accept(serv_sock, ...);
    pid = fork();
    
    if (pid < 0) {
        perror("fork failed");
        return -1;
    }
    if (fork() > 0) {
        // 父進(jìn)程
        continue;
    } else {
        // 子進(jìn)程
        // 接收客戶端的數(shù)據(jù),同步阻塞 I/O,等待數(shù)據(jù)就緒
        recvfrom(clnt_sock, ...);
        // 處理數(shù)據(jù)
        handle(data);
    }
}

?單進(jìn)程單線程·事件驅(qū)動(dòng)?

除了通過(guò)多進(jìn)程/多線程方式去應(yīng)對(duì)并發(fā)量大的場(chǎng)景,基于 I/O 多路復(fù)用模型的單進(jìn)程單線程·事件驅(qū)動(dòng)架構(gòu)也是較好的解決方案,同時(shí)由于是單線程,所以不會(huì)因開(kāi)辟大量進(jìn)場(chǎng)/線程所帶來(lái)的資源開(kāi)銷以及同步互斥的問(wèn)題。

單線程不適合執(zhí)行 CPU 密集型任務(wù),因?yàn)槿绻蝿?wù)一直占用 CPU 時(shí)間,則后續(xù)任務(wù)無(wú)法執(zhí)行,因此針對(duì)大量 CPU 計(jì)算、引起進(jìn)程阻塞的任務(wù),可引入線程池技術(shù)去解決。

目前 NodeJS 就是采用這種設(shè)計(jì)架構(gòu),所有 JS 代碼跑在主線程中(單線程),基于 I/O 多路復(fù)用的模型去實(shí)現(xiàn)事件驅(qū)動(dòng)的多讀寫請(qǐng)求的管理,配合線程池,將 CPU 密集型任務(wù)從主線程分離出來(lái),以保證主線程的高效響應(yīng)。

解答

  • NodeJS 代碼是如何跑起來(lái)的
  • 當(dāng)我們執(zhí)行 node server.js 時(shí),NodeJS 首先會(huì)進(jìn)行一系列的初始化操作,包括:

注冊(cè) C++ 系列的模塊和 V8 的初始化操作

創(chuàng)建 environment 對(duì)象用于存放一些全局的公共變量

初始化模塊加載器,以便在用戶 JS 代碼層調(diào)用原生 JS 模塊以及原生 JS 模塊調(diào)用 C++ 模塊能夠成功加載

初始化執(zhí)行上下文,暴露 global 在全局上下文中,并設(shè)置一些全局變量和方法在 global 或 process 對(duì)象

初始化 libuv,創(chuàng)建一個(gè)默認(rèn)的 event_loop 結(jié)構(gòu)體用于管理后續(xù)各個(gè)階段產(chǎn)生的任務(wù)

緊接著 NodeJS 執(zhí)行用戶 JS 代碼,用戶 JS 代碼執(zhí)行一些初始化的邏輯以及往事件循環(huán)注冊(cè)任務(wù),然后進(jìn)程就進(jìn)入事件循環(huán)的階段。

整個(gè)事件循環(huán)分為 7 個(gè)階段,timer 處理定時(shí)器任務(wù),pending 處理 poll io 階段的成功或錯(cuò)誤回調(diào),idle、prepare、check 是自定義階段,poll io 主要處理網(wǎng)絡(luò) I/O,文件 I/O等任務(wù),close 處理關(guān)閉的回調(diào)任務(wù),同時(shí)在各個(gè)事件階段還會(huì)穿插微任務(wù)隊(duì)列。

以開(kāi)篇的 TCP 服務(wù)為例,當(dāng)創(chuàng)建 TCP 服務(wù)器調(diào)用原生 JS 的 net 模塊的 server.listen 方法后, net 模塊就會(huì)引用 C++ 的 TCP 模塊實(shí)例化一個(gè) TCP 服務(wù)器,內(nèi)部調(diào)用了 Libuv 的 uv_tcp_init 方法,該方法封裝了 C 中用于創(chuàng)建套接字的 socket 函數(shù);接著就是調(diào)用 C++ 的 TCP 模塊的 Bind 方法,該方法封裝了 Libuv 的 uv_ip_addr 以及 uv_tcp_bind,分別用于設(shè)置 TCP 的 IP 地址和端口信息以及調(diào)用 C 中的 bind 方法用于綁定地址信息。

然后 net 模塊注冊(cè) onconnect 回調(diào)函數(shù),該函數(shù)將在客戶端請(qǐng)求到來(lái)后,在 Libuv 的 poll io 階段執(zhí)行,onconnect 函數(shù)調(diào)用了 C++ 的 ConnectionWrap::OnConnection 方法,內(nèi)部調(diào)用了 Libuv 的 uv_accpet 去接收來(lái)自客戶端的連接。最后調(diào)用 TCP 實(shí)例的 listen 方法使得服務(wù)器進(jìn)入被動(dòng)監(jiān)聽(tīng)狀態(tài),listen 使用了 C++ 的 TCPWrap::Listen 方法,該方法是對(duì) uv_listen 的封裝,最終調(diào)用的 C 的 listen 方法。

當(dāng)客戶端請(qǐng)求通過(guò)網(wǎng)卡傳遞過(guò)來(lái),對(duì)應(yīng)的監(jiān)聽(tīng)型 socket 發(fā)生狀態(tài)變更,事件循環(huán)模塊根據(jù)命中之前設(shè)置的可讀事件,將 onconnection 回調(diào)插入 poll io 階段的任務(wù)隊(duì)列,當(dāng)新一輪的事件循環(huán)到達(dá) poll io 時(shí)執(zhí)行回調(diào),調(diào)用 accept 方法創(chuàng)建與客戶端的通信型 socket,此時(shí)進(jìn)入進(jìn)程阻塞,經(jīng)過(guò)三次握手后,建立與客戶端的連接,將用戶 JS 的回調(diào)插入 poll io 的任務(wù)隊(duì)列,在新一輪的事件循環(huán)中進(jìn)行數(shù)據(jù)的處理。

圖片

image.png

  • TCP 連接在 NodeJS 中是如何保持一直監(jiān)聽(tīng)而進(jìn)程不中斷的

TCP 服務(wù)器在啟動(dòng)之后,就往 NodeJS 的事件循環(huán)系統(tǒng)插入 listen 的監(jiān)聽(tīng)任務(wù),該任務(wù)會(huì)一直阻塞監(jiān)聽(tīng)(不超過(guò) timeout)來(lái)自客戶端的請(qǐng)求,當(dāng)發(fā)生請(qǐng)求后,建立連接然后進(jìn)行數(shù)據(jù)處理后,再會(huì)進(jìn)入監(jiān)聽(tīng)請(qǐng)求的阻塞狀態(tài),新一輪的事件循環(huán)發(fā)現(xiàn) poll io 隊(duì)列還有任務(wù)所以不會(huì)退出事件循環(huán),從而驅(qū)動(dòng)進(jìn)程一直運(yùn)行。

  • NodeJS 是如何處理并發(fā)連接的,當(dāng)遇到阻塞型調(diào)用時(shí)如何不阻塞主線程的

NodeJS 采用的是單線程+事件驅(qū)動(dòng)的服務(wù)端架構(gòu),首先對(duì)于事件循環(huán)以外的代碼會(huì)在初始化時(shí)執(zhí)行完,然后進(jìn)程就進(jìn)入事件循環(huán),針對(duì)網(wǎng)絡(luò) I/O NodeJS 底層采用的是 I/O 多路復(fù)用模型,通過(guò)監(jiān)聽(tīng)就緒的連接做到從容應(yīng)對(duì)大并發(fā)連接。對(duì)于網(wǎng)絡(luò)數(shù)據(jù)而言,當(dāng)調(diào)用阻塞的 recvfrom 處理來(lái)自的網(wǎng)絡(luò)的數(shù)據(jù),此時(shí)數(shù)據(jù)已經(jīng)就緒,所以數(shù)據(jù)處理起來(lái)很快,如果是大文件,則需要業(yè)務(wù)代碼自行開(kāi)辟線程去處理;對(duì)于文件 I/O,NodeJS 底層采用線程池的機(jī)制,在主線程外開(kāi)辟工作線程去處理本地大文件,在處理完后通過(guò)事件通知機(jī)制告訴上層 JS 代碼。

參考資料

  • https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810
  • https://zhuanlan.zhihu.com/p/115912936?utm_source=pocket_reader
  • https://www.cnblogs.com/JAVAlyy/p/8882066.html?utm_source=pocket_reader
  • https://github.com/theanarkh/understand-nodejs/blob/master/docs/chapter01-Node.js%E7%BB%84%E6%88%90%E5%92%8C%E5%8E%9F%E7%90%86.md

分享到:
標(biāo)簽:Node js
用戶無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定