Node.js 的原理總結

一. nodejs背景

先來講說nodejs最常被提到的幾個關鍵詞,「單線程」,「非阻塞異步IO」,「事件循環」。接下來主要來經過這幾個關鍵字總結一下nodejs的內在原理,以及引伸出的一些問題。javascript

二. nodejs是單線程嗎?

若是說nodejs是單線程語言,能夠想象一下,一個單實例的nodejs的服務器同時接受100個用戶請求時,第100個用戶的請求要等前面99的用戶處理完成才能獲得處理,若是每一個用戶的請求要0.3秒,第100個用戶須要30秒的等待,這顯然和咱們的實際狀況並不符合,因此說,nodejs並非單純的單線程。java

那爲何說nodejs是單線程語言呢?而是由於nodejs中javascript代碼的執行是單線程,怎麼理解這句話,看下面代碼。node

console.log('javascript start');
setTimeout(()=>{
  console.log('javascript setTimeout');
}, 2000);

const now = Date.now();
while(Date.now() < now + 4000) {}
console.log('javascript end');
複製代碼

執行結果:git

$ node index.js 
javascript start
javascript end
javascript setTimeout
複製代碼

上面的代碼中,setTimeout的回調代碼在while執行4秒期間,計時器已是過了兩秒的,而'javascript setTimeout'這一句打印卻在'javascript end'以後,即便計時器在兩秒後回調代碼應該被執行時,由於javascript的線程處於非空閒狀態,而不能輸出'javascript setTimeout',javascript代碼是單線程這樣理解。github

三. nodejs的異步IO

再拿上面的例子來看,當100個用戶請求同時被接受到時,當須要IO(網絡IO/文件IO)操做時,單線程的javascript並不會停下來等待IO操做完成,而是「事件驅動」開始介入,javascript執行線程繼續執行未完的javascript代碼,當執行完成後該線程處於空閒狀態,能夠看下面這一段代碼示例。shell

// http.js

const http = require('http');
const fs = require('fs');

let num = 0;

http.createServer((req, res) => {
  console.log('request id: %d, time:', num++, Date.now());
  fs.readFile('./test.txt', ()=> {
    res.end('response');
  });
}).listen(9007, ()=>{
  console.log('server start, 127.0.0.1:9007');
});
複製代碼
// req.js
const http = require('http');

for(let i=0; i<100; i++) {
  http.get('http://127.0.0.1:9007', (res)=>{
    res.on("data",(data)=>{
      console.log('response time:', Date.now())
      // console.log('data', data.toString())
    })
  }).on('error', (err)=>{
    console.log('error', err);
  })
}
複製代碼
node http.js     // 啓動服務器
複製代碼

node req.js    // 發起100個請求
複製代碼

能夠看出100個請求均是在請求返回以前很是短的時間都被獲得了處理,而返回則均在請求以後,並不是請求按接收順序依次等待各個IO獲得處理後依次返回。bash

四. 事件循環

說到事件循環,在上面的請求中,100個請求的都在很是短的時間獲得了處理,然後請求又各自獲得了回覆,能夠思考一下,javascript已經執行到了第100個請求,而第1個請求才獲得回覆,而第一個請求的棧信息沒有丟失,說明第一個請求的請求棧信息被記錄了,這一過程即是註冊IO事件。服務器

從上面註冊事件後,事件循環獲得激活,對於上面代碼中fs.readFile這個讀文件IO則開始真正執行,而這時候IO的執行跟javascript代碼的執行便沒有關係了,由nodejs底層libuv提供的線程池接收該文件IO執行工做,該線程池默認大小爲4,能夠經過環境變量process.env.UV_THREADPOOL_SIZE在啓動的時候進行調整,可是最大不能超過1024個,有興趣的能夠查看線程池源碼;由上能夠看出nodejs內部實際是多進程並行工做的,而是利用事件循環作了封口處理。網絡

nodesys.png

再來講說事件循環,上面示例中fs.readFile讀文件時,如何知道這個讀操做完成了呢?能夠思考一下,讀操做是線程池來控制執行的,在該線程執行前,先在註冊事件的內存中初始化一個狀態是「執行中」,而且事件循環也已經被激活,開始輪詢等待執行結果,當執行IO的線程在執行完以後,再經過底層的異步IO接口(epoll_wait/IOCP)進行通知到初始註冊的任務隊列內存進行變動狀態,事件循環輪詢到狀態變成「已完成」,這時候在IO事件註冊時注入的回調函數獲得執行權,javascript線程開始工做,整個異步過程完畢。 異步

能夠看看事件循環裏面都要通過哪些步驟,如何稱爲事件循環:

能夠看一下英文原版的解釋,事件循環解釋

翻譯過來:

**階段概覽**
timers:這個階段執行setTimeout() 和 setInterval()中到期的回調函數
I/O callbacks:執行全部除了setTimeout() ,setInterval(),close事件,setImmediate的其餘回調函數
idle, prepare:僅內部使用
poll:獲取新的I/O 事件,在適當的條件下nodejs會阻塞在這個階段
check:setImmediate的回調函數在這裏被調用
close callbacks:像socket.on("close",func)這一類執行close事件的回調

複製代碼

如上內容均爲本身總結,不免會有錯誤或者認識誤差,若有問題,但願你們留言指正,以避免誤人,如有什麼問題請留言,會盡力回答之。若是對你有幫助不要忘了分享給你的朋友哦!也能夠關注做者,查看歷史文章而且關注最新動態,助你早日成爲一名全棧工程師!

相關文章
相關標籤/搜索