9.玩轉進程
10.測試
11.產品化
node的單線程只不過是js層面的單線程,是基於V8引擎的單線程,由於,V8的緣故,先後端的js執行模型基本上是相似的,可是node的內核機制依然是經過libuv調用epoll或者IOCP的多線程機制。換句話說,node從嚴格意義上講,並不是是真正的單線程架構,node內核自身有必定的IO線程和IO線程池,經過libuv的調度,直接使用了操做系統層面的多線程。node的開發者,能夠經過擴展c/c++模塊來直接操縱多線程來提升效率。不過,單線程帶來的好處是程序狀態單一,沒有鎖、線程同步、線程上下文切換等問題。可是單線程的程序,並不是是完美的。如今的服務器不少都是多cpu,多cpu核心的,一個node實例只能利用一個cpu核心,那麼其餘的cpu核心不就浪費了嗎?而且,單線程的容錯也很弱,一旦拋出了沒有捕獲的異常,必將引發整個程序的崩潰,那這樣的程序必然是很是脆弱的,這樣的服務器端語言又有什麼價值呢?node
兩個問題:c++
經歷了同步(qps爲1/n)、複製進程(預先賦值必定數量的進程,prefork,可是,一旦用超了,仍是跟同步的服務器同樣,qps爲m/n)、多線程(qps爲M*L/N,這種模型,當併發上萬後,內存耗用的問題將會暴露出來也就是C10k問題,apache就是採用了這樣的多線程、多進程架構)和事件驅動等幾個不一樣的模型。apache
面對單進程單線程對多核使用不足的問題,前人的經驗是啓動多個進程,理想狀態下,每一個進程各自利用一個cpu,以此實現多核cpu的利用。node提供了child_process模塊,並提供了child_process.fork()函數來實現進程的複製。後端
//node worker.js var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).listen(Math.round((1 + Math.random()) * 1000), '127.0.0.1'); //node master.js var fork = require('child_process').fork; var cpus = require('os').cpus(); for (var i = 0; i < cpus.length; i++) { fork('./worker.js'); }
這兩段代碼會根據當前機器上的cpu數量,複製出對應node進程數,在*nix下,能夠經過ps aux | grep worker.js查看到進程的數量。
這就是主從架構了,在這裏存在兩個進程,master是主進程、worker是工做進程。這是典型的分佈式架構用於並行業務處理的模式,具備較好的可伸縮性和穩定性。主進程不負責具體業務處理,只負責調度和管理工做進程,所以主進程是相對於穩定和簡單的,工做進程負責具體的業務處理,由於,業務多種多樣,因此,工做進程的穩定性,是咱們須要考慮的。api
經過fork複製的進程都是獨立的,每一個進程都有着獨立而全新的v8實例,所以,須要至少30毫秒的啓動時間和10mb左右的內存,可是,咱們要記得fork進程是昂貴的,好在node在事件驅動的方式上,實現了單線程解決大併發的問題,這裏啓動多個進程只是爲了充分將cpu資源利用起來,而不是爲了解決併發的問題。服務器
1).建立子進程網絡
child_process模塊給予了node隨意建立子進程(child_process)的能力,它提供了4個方法用於建立子進程。多線程
spawn()與exec()、execFile()不一樣的是,後二者建立時可指定timeout屬性,設置超時時間,一旦建立的進程運行超過設定的時間進程將會被殺死。
exec()與execFile()不一樣的是,exec()適合執行已有的命令,execFile()適合執行文件。這裏咱們一node worker.js爲例,來分別實現上述的4中方法架構
var cp = require('child_process'); cp.spawn('node', ['worker.js']); cp.exec('node worker.js', function (err, stdout, stderr) { // some code }); cp.execFile('worker.js', function (err, stdout, stderr) { // some code }); cp.fork('./worker.js');
以上四個方法在建立子進程後,均會返回子進程對象,他們的差異以下:併發
這裏的可執行文件是指直接能夠執行的,也就是*.exe或者.sh,若是是js文件,經過execFile()運行,那麼這個文件的首行必須添加環境變量:#!/usr/bin/env node,儘管4種建立子進程的方式存在差異,可是事實上後面3種方法都是spawn()的延伸應用。
2)進程間通訊
主線程與工做線程之間經過onmessage()和postMessage()進程通訊,子進程對象則由send()方法實現主進程向子進程發送數據,message事件實現收聽子進程發來的數據,與api在必定程度上類似。經過消息傳遞,而不是共享或直接操縱相關資源,這是較爲輕量和無依賴的作法。
// parent.js var cp = require('child_process'); var n = cp.fork(__dirname + '/sub.js'); n.on('message', function (m) { console.log('PARENT got message:', m); }); n.send({ hello: 'world' }); // sub.js process.on('message', function (m) { console.log('CHILD got message:', m); }); process.send({ foo: 'bar' });
經過fork()或其餘api建立子進程後,爲了實現父子進程之間的通訊,父進程與子進程之間將會建立IPC通道,經過IPC通道,父子進程之間才能經過message和send()傳遞消息。
進程間通訊原理
PC的全稱是Inter-Process Communication,即進程間通訊。進程間通訊的目的是爲了讓不一樣的進程可以互相訪問資源,並進程協調工做。實現進程間通訊的技術有不少,如命名管道、匿名管道、socket、信號量、共享內存、消息隊列、Domain Socket等,node中實現IPC通道的是管道技術(pipe)。
在node中管道是個抽象層面的稱呼,具體細節實現由libuv提供,在win下是命名管道(named pipe)實現,在*nix下,採用unix Domain Socket來實現。
可是,具體在應用層面只是簡單的message事件和send()方法,接口十分簡潔和消息化。
父進程在實際建立子進程前,會建立IPC通道並監聽它,而後才真正建立出子進程,並經過環境變量(NODE_CHANNEL_FD)告訴子進程這個IPC通訊的文件描述符。子進程在啓動的過程當中,根據文件描述符去鏈接這個已存在的IPC通道,從而完成父子進程之間的鏈接。
創建鏈接以後的父子進程就能夠自由的通訊了,因爲IPC通道是用命名管道或者Domain Socket建立的,他們與網絡socket的行爲比較相似,屬於雙向通道。不一樣的是他們在系統內核中就完了進程間的通訊,而不通過實際的網絡層,很是高效。在node中,IPC通道被抽象爲stream對象,在調用send()時發送數據(相似於write()),接收到的消息會經過message事件(相似於data)觸發給應用層。
注意:只有啓動的子進程是node進程是,子進程纔會根據環境變量去鏈接IPC通道,對於其餘類型的子進程則沒法自動實現進程間通訊,須要讓其餘進程也按照約定去鏈接這個已經建立好的IPC通道才行。
3)句柄傳遞
進程間發送句柄的功能,send()方法除了可以經過IPC發送數據外還能發送句柄,第二個可選參數就是句柄:
child.send(message, [sendHandle])
句柄是一種能夠用來標識資源的引用,它的內部包含了指向對象的文件描述符。所以,句柄能夠用來標識一個服務端的socket對象、一個客戶端的socket對象、一個udp套接字、一個管道等。
這個句柄就解決了一個問題,咱們能夠去掉代理方案,在主進程接收到socket請求後,將這個socket直接發送給工做進程,而不從新與工做進程之間創建新的socket鏈接轉發數據。咱們來看一下代碼實現:
主進程發送完句柄,並關閉監聽以後,就變成了以下結構:
這樣,就能夠實現多個子進程能夠同時監聽相同端口,再沒有EADDRINUSE的異常發生。
1.句柄發送與還原
子進程對象send()方法能夠發送的句柄類型包括以下幾種:
send()方法在將消息發送到IPC管道前,將消息組裝成兩個對象,一個參數是handle,另外一個是message
//message參數 { cmd: 'NODE_HANDLE', type: 'net.Server', msg: message }
發送到IPC管道中的其實是咱們要發送的句柄文件描述符,文件描述符其實是一個整數值,這個message對象在寫入到IPC通道時,也會經過JSON.stringify()進行序列化,因此最終發送到IPC通道中的信息都是字符串,send()方法能發送消息和句柄並不意味着它能發送任意對象。
鏈接了IPC通道的子進程能夠讀取到父進程發來的消息,將字符串經過JSON.parse()解析還原爲對象後,纔出發message事件將消息體傳遞給應用層使用,在這個過程當中,消息對象還要被進行過濾處理,message.cmd的值若是以NODE_爲前綴,它將響應一個內部事件internalMessage
若是message.cmd值爲NODE_HANDLE,它將取出message.type的值和獲得的文件描述符一塊兒還原出一個對應的對象。這個過程的示意圖以下:
2.端口共同監聽
1)進程事件
2)自動重啓
3)負載均衡
4)狀態共享
1)Cluster工做原理
2)Cluster事件