前言
前端工程師因為需要操縱Ajax(Ajax的A就是Asynchronous的意思)叠纹,因此寿谴,是最了解異步IO的人群之一腻惠,另外了解異步IO人群就是操作系統(tǒng)開發(fā)工程師了(在操作系統(tǒng)層面衷恭,異步是通過信號量腾夯、消息等方式進行的)。
但是從另外一個層面來說价脾,異步編程在高級語言中非常少見牧抵,這是因為,程序員不是很適應異步編程的開發(fā)方式和代碼書寫習慣侨把。以PHP來說犀变,PHP是一種同步阻塞式的編程語言,甚至連多線程都不提供秋柄。這種特點在小型網(wǎng)站中获枝,基本上不會構成任何問題,但是骇笔,在復雜的網(wǎng)絡應用中省店,阻塞導致了無法更好的并發(fā)。
node是第一個以異步編程為特點的高級語言笨触,同時懦傍,還包括了單線程和事件驅動的編程方式和特性。因此芦劣,Node是一種非常適合開發(fā)IO密集型的程序的語言粗俱。(這一點跟Nginx很像,但是Nginx是個服務器虚吟,它還是要受制于同步語言的牽制)
異步IO的好處
用戶體驗
異步IO降低了用戶獲取資源的響應時間寸认。例如有兩個任務签财,分別耗時t1和t2,那么同步程序消耗的總時間將會大于等于這兩個任務的時間總和偏塞,也就是t>=t1+t2唱蒸。而異步IO,因此烛愧,兩個任務是同時進行的油宜,因此掂碱,t<=t1+t2怜姿,或者說t=max(t1,t2)。我們來看看代碼:
//同步程序
//消耗時間t1
getData('from_db');
//消耗時間t2
getData('from_remote_api');
//異步程序
getData('from_db', function (result) {
// 消耗時間t1
});
getData('from_remote_api', function (result) {
// 消耗時間t2
});
因為疼燥,當下的網(wǎng)絡環(huán)境分布式是一種常態(tài)沧卢,因此,異步IO的優(yōu)勢非常明顯醉者。此處附上書中給的不同IO的消耗cpu時鐘但狭。
因為,后端的響應速度提升了撬即,因此立磁,前端的用戶體驗也會更好。
資源分配
計算機組件分為IO設備和計算設備剥槐。因此唱歧,之前解決業(yè)務需求的普遍做法是單線程和多線程,我們來比較一下:
線程 | 說明 |
---|---|
單線程 | 串行執(zhí)行程序粒竖,one by one颅崩,雖然可以增加進程來提升效率,但是蕊苗,這個進程的提升是通過增加機器來實現(xiàn)的沿后,從經濟角度來考慮的話,并不實惠 |
多線程 | 多線程的代價在于創(chuàng)建線程和執(zhí)行期線程上下文切換的開銷朽砰。一般來說尖滚,多線程效率會優(yōu)于單線程(因此可以利用多核CPU并有效的提升CPU使用率),但是多線程會面臨鎖瞧柔、狀態(tài)同步等問題 |
因此熔掺,IO設備和計算設備是可以并行進行的,因此非剃,就有了node的異步IO和事件驅動置逻。
node的解決方案
node利用單線程,多進程的方式(類似于前端瀏覽器的Web Workers子進程的方式)备绽,遠離了多線程死鎖券坞、狀態(tài)同步等問題万牺,利用異步IO和事件驅動拾枣,讓單線程也可以遠離阻塞,更好的使用CPU資源。
異步IO和非阻塞IO
操作系統(tǒng)層面击奶,操作系統(tǒng)內核對應IO只有兩種方式:阻塞和非阻塞。
阻塞IO舉例:讀取磁盤文件秩贰,系統(tǒng)內核在完成磁盤尋道螟深、讀取數(shù)據(jù)、復制數(shù)據(jù)到內存他挎,這個調用才結束筝尾。
注意:操作系統(tǒng)對計算機進行了抽象,所以IO設備都被抽象為了文件办桨,內核在文件IO操作時筹淫,通過文件描述符進行管理,而文件描述符類似于應用程序與系統(tǒng)內核之間的憑證呢撞。應用程序如果需要進行IO調用损姜,需要先打開文件描述符,然后再根據(jù)文件描述符去實現(xiàn)文件的數(shù)據(jù)讀寫殊霞。
那么非阻塞IO呢摧阅,他會在調用后,馬上完成回調绷蹲。但是任務仍然在后臺運行棒卷,直到任務完成,才會返回最終的信號瘸右,或者娇跟,再次讀取文件。
此處太颤,非阻塞IO與阻塞IO的區(qū)別在于阻塞IO完成整個獲取數(shù)據(jù)的過程苞俘,而非阻塞IO則不帶數(shù)據(jù)直接返回,需要獲取數(shù)據(jù)龄章,還需要通過文件描述符再次讀取吃谣。但是,由于不知道任務何時真正完成做裙,因此岗憋,需要輪詢訪問任務,查看是否已經完成锚贱,這無形當中增加了系統(tǒng)資源的占用仔戈。
操作系統(tǒng)的輪詢種類
read
read通過了一種反復查看IO狀態(tài)的形式來完成輪詢,并讀取數(shù)據(jù)。cpu一直耗用在無謂的輪詢上监徘,資源浪費明顯晋修。
select
通過select對文件描述符上的事件狀態(tài)進行判斷,一旦讀取完成凰盔,則再次調用read完成真正的讀取墓卦。這個方式也有資源浪費。并且户敬,select采用了1024長度的數(shù)組來存儲狀態(tài)落剪,也就是說,select最多只能同時檢查1024個文件描述符尿庐。
poll
poll是采用了鏈表的select忠怖,避免了數(shù)組的長度限制。但是屁倔,還是要不斷的檢查狀態(tài)脑又,還是存在cpu資源的浪費暮胧。并且锐借,書中說,當文件描述符較多時往衷,它的性能比較低下钞翔。
epoll
epoll在進入輪詢后,如果沒有檢查到IO事件席舍,將會進行休眠布轿,直到事件將其喚醒,這個方案利用了事件通知来颤、執(zhí)行回調的方式汰扭,避免了無謂的遍歷查詢,減少了cpu資源的浪費福铅。另外萝毛,還有FreeBSD下的kqueue,這個跟epoll類似滑黔,書中沒有詳細介紹笆包。
輪詢小結
從本質上來講,輪詢技術還是一種同步執(zhí)行的程序略荡,要么不斷的遍歷庵佣,要么進行休眠。這使得程序依舊需要花費時間進行等待汛兜。
理想的非阻塞異步IO
我們期待的理想非阻塞異步IO是這樣的:
應用程序發(fā)起非阻塞調用巴粪,無須通過遍歷或者事件喚醒等方式的輪詢,可以直接處理下一個任務,只需要在IO完成后通過信號或者回調將數(shù)據(jù)傳遞給應用程序即可肛根。我們來看一下理想的非阻塞異步IO的示意圖:
這種方式使用了信號或者回調來傳遞數(shù)據(jù)衡创,在linux下用原生的這種IO,我們稱之為AIO晶通。但是璃氢,這種方式只在linux下存在,而且狮辽,AIO僅支持內核IO的o_direct方式讀取一也,導致無法利用系統(tǒng)緩存。
(O_DIRECT和O_SYNC是系統(tǒng)調用open的flag參數(shù)喉脖。通過指定open的flag參數(shù)椰苟,以特定的文件描述符打開某一文件。這兩個flag會對寫盤的性能有很大的影響树叽。)
/* Open new or existing file for reading and wrting,
sync io and no buffer io; file permissions read+
write for owner, nothing for all others */
fd = open("myfile", O_RDWR | O_CREAT | O_SYNC | O_DIRECT, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open");
//O_DIRECT: 無緩沖的輸入舆蝴、輸出。
//O_SYNC:以同步IO方式打開文件题诵。
為了解決這種問題洁仗,我們通過增加線程來實現(xiàn):
通過讓部分線程進行阻塞IO或者非阻塞IO輪詢來獲取數(shù)據(jù),讓一個線程進行計算處理性锭,通過線程之間的通信將IO得到的數(shù)據(jù)進行傳遞赠潦,這就實現(xiàn)了異步IO。glibc的AIO就是典型的線程池模擬異步IO的程序草冈,但是她奥,存在bug不推薦使用。libev的作者推出了libeio異步IO庫怎棱,這個庫采用了線程池和阻塞IO來模擬異步IO哩俭,node在*nix平臺下采用了libeio配合libev來實現(xiàn)異步IO,并在v0.9.3后拳恋,自行實現(xiàn)了線程池來完成這種異步IO凡资。
在win平臺上,win使用了IOCP來實現(xiàn)異步IO诅岩,調用異步IO讳苦,等待IO完成后通知并執(zhí)行回調,用戶無需考慮輪詢吩谦,但是他的內部仍然是線程池的原理鸳谜,這些線程池由系統(tǒng)接手管理。
IOCP的異步IO模型式廷,與node的異步調用模型十分近似咐扭。為了平衡差異,node提供了libuv作為抽象封裝層,對于平臺進行了兼容蝗肪。在node編譯期間會判斷平臺條件袜爪,選擇性的編譯unix目錄或win目錄下的源文件到目標程序:
注意:我們時常提到的node是單線程的,這里的單線程僅僅只是js執(zhí)行在單線程中薛闪,在node里辛馆,無論哪個平臺,內部完成IO任務的都另有線程池豁延。
node的異步IO
完成node整個異步IO環(huán)節(jié)的有事件循環(huán)昙篙、觀察者、請求對象
事件循環(huán)
事件循環(huán)是node的自身執(zhí)行模型诱咏,正是事件循環(huán)使得回調函數(shù)得以在node中大量的使用苔可。在進程啟動時node會創(chuàng)建一個while(true)循環(huán),這個和Netty也是一樣的袋狞,每次執(zhí)行循環(huán)體焚辅,都會完成一次Tick。每個Tick的過程就是查看是否有事件等待被處理苟鸯。如果有同蜻,就取出事件及相關的回調函數(shù),并執(zhí)行關聯(lián)的回調函數(shù)倔毙。如果不再有事件處理就退出進程埃仪。
觀察者
觀察者模式又可以稱為“生產者-消費者模式”乙濒,在node中陕赃,每個事件循環(huán)中會有一個或者多個觀察者,這些觀察者都注冊了相關的事件颁股,等待事件的完成么库,并調用回調函數(shù)。(例如node中的網(wǎng)絡IO觀察者甘有、文件IO觀察者等)事件循環(huán)會不斷的從觀察者那里取出事件并處理诉儒,最終返回回調函數(shù)。
在win下亏掀,這個循環(huán)基于IOCP忱反,在*nix下,則基于多線程創(chuàng)建滤愕。
請求對象
js層面發(fā)起異步調用
請求對象這個概念比較重要温算,因此書中給了一個機遇win下異步IO(利用IOCP)實現(xiàn)的簡單例子來探尋從JS代碼到系統(tǒng)內核之間都發(fā)生了什么。
對于一般的非異步回調函數(shù)间影,函數(shù)由我們自行調用注竿,如下所示:
var forEach = function (list, callback) {
for (var i = 0; i < list.length; i++) {
callback(list[i], i, list);
}
};
對于node中的異步IO調用而言,回調函數(shù)卻不由開發(fā)者調用,這種調用巩割,從js發(fā)起調用到內核執(zhí)行完IO操作的過程中裙顽,存在一種中間產物:請求對象。以fs.open()來說明宣谈,通過這個例子我們將要探討Node與底層之間是如何執(zhí)行異步IO調用以及回調函數(shù)究竟是如何被調用執(zhí)行的:
fs.open = function(path, flags, mode, callback) {
// ...
binding.open(pathModule._makeLong(path),
stringToFlags(flags),
mode,
callback);
};
fs.open()是根據(jù)指定路徑和參數(shù)打開一個文件愈犹,并得到文件描述符,這是后續(xù)所有IO操作的初始操作闻丑。JS層面的代碼通過調用C++核心模塊進行下層操作甘萧。
我們可以看出,第一步是js調用node核心模塊梆掸,第二步是核心模塊調用c++內建模塊扬卷,第三步是內建模塊通過libuv進行系統(tǒng)調用,調用fs.c酸钦,實質上是調用uv_fs_open()怪得。在這個調用過程中,創(chuàng)建了FSReqWrap請求對象卑硫。從js層傳入的參數(shù)和當前方法都被封裝在這個請求對象中徒恋,回調函數(shù)則被設置在這個對象的oncomplete_sym屬性上:
req_wrap->object->Set(oncomplete_sym,callback);
對象包裝完畢后,在win下欢伏,則調用QueueUserWorkItem()方法將這個FSReqWrap對象推入線程池中入挣,等待執(zhí)行:
QueueUserWorkItem(&uv_fs_thread_proc, /*執(zhí)行方法的句柄,這個句柄就是uv_fs_thread_proc*/
req, /*uv_fs_thread_proc方法的運行時參數(shù)*/
WT_EXECUTEDEFAULT) /*執(zhí)行標志*/
當線程池中有可用線程時硝拧,我們會調用uv_fs_thread_proc()方法径筏,該方法根據(jù)傳入?yún)?shù)的類型調用相應的底層函數(shù),也就是uv_fs_open()調用的是fs_open()方法障陶。然后滋恬,js的調用就立即返回了,也就是由js層面發(fā)起的異步調用的第一階段就結束了抱究,js線程就可以繼續(xù)執(zhí)行當前任務的后續(xù)操作了恢氯。當前的IO操作在線程池中等待執(zhí)行,不管它是否阻塞IO鼓寺,都不會影響js線程的后續(xù)執(zhí)行勋拟,如此,就達到了異步的目的妈候。
執(zhí)行回調
請求對象發(fā)生在js層面調用異步IO的第一階段敢靡,那么組裝好請求對象、送入IO線程池等待執(zhí)行就全部是在第一階段完成的州丹,完成這一步醋安,js就會繼續(xù)執(zhí)行后續(xù)的代碼杂彭,其他的工作都由內核負責,這后續(xù)的步驟被稱為回調通知吓揪。
線程池中的IO操作調用完畢之后亲怠,會將獲取的結果存儲在req->result屬性上,然后調用PostQueuedCompletionStatus()通知IOCP柠辞,告知當前對象操作已經完成:
PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped))
PostQueuedCompletionStatus()的作用就是向IOCP提交執(zhí)行狀態(tài)团秽,并將線程歸還線程池,通過PostQueuedCompletionStatus()方法提交的狀態(tài)叭首,可以通過GetQueuedCompletionStatus()提取习勤。這個提取動作是通過事件循環(huán)IO觀察者,在每次執(zhí)行Tick的過程中調用IOCP的GetQueuedCompletionStatus()方法檢測線程池中是否有執(zhí)行完的請求焙格,如果存在图毕,會將請求對象加入到IO觀察者的隊列中,然后將其當做事件處理眷唉。
這個IO觀察者回調函數(shù)的行為就是取出請求對象的result屬性作為參數(shù)予颤,并取出oncomplete_sym屬性作為方法,然后調用執(zhí)行冬阳,以此達到調用js中傳入的回調函數(shù)的目的蛤虐。
至此,整個異步IO的流程就完全結束了肝陪。
小結
事件循環(huán)驳庭、觀察者、請求對象氯窍、IO線程池這四部分共同構成了node異步IO模型的基本要素饲常,通過node調用異步IO,然后node內核通過libuv判斷平臺荞驴,并調用不同的組件不皆,這個組件前邊介紹的是win下的IOCP,那么在linux下是epoll熊楼,在FreeBSD下kqueue,Solaris是Event ports能犯,然后這個組件向系統(tǒng)內核發(fā)送IO調用鲫骗,最后再從內核獲取已經完成的IO操作,并配以事件循環(huán)踩晶,以此完成異步IO的全過程执泰。
不同的是,線程池在win下用內核IOCP直接提供渡蜻,*inx則由libuv自行實現(xiàn)术吝。
非IO的異步API
在node中存在一些與IO無關的異步API计济,他們主要包括:設置超時定時器setTimeout()、設置間隔定時器setInterval()排苍、設置馬上執(zhí)行間隔setImmediate()和process.nextTick()
定時器
setTimeout()和setInterval()與瀏覽器中的API是一致的沦寂,setTimeout()設置的是單次執(zhí)行任務,setInterval()設置的是多次執(zhí)行任務淘衙。這些方法的原理與異步IO類似传藏,只不過不需要線程池的參與,調用setTimeout()和setInterval()創(chuàng)建的定時器會被插入到定時器觀察者內部的一個紅黑樹中彤守,每次執(zhí)行Tick毯侦,會從該紅黑樹中迭代取出定時器對象,檢查是否超過定時時間具垫,如果超出侈离,就形成一個事件,他的回調函數(shù)將立即執(zhí)行筝蚕。我們來看一下setTimeout()的行為:
setTimeout()是一次的霍狰,setInterval()是多次的,會重復檢測和執(zhí)行setTimeout()的這些行為饰及。
通過上邊看出蔗坯,定時器在于cpu時鐘層面是不精確的,雖然人類感覺不到燎含,但是宾濒,還是會有誤差,這個誤差就來源于每次的Tick循環(huán)屏箍,比如一個還有1毫秒就要到時的回調任務绘梦,剛好循環(huán)體執(zhí)行過去了,那么再等待下一次循環(huán)體調用的時候赴魁,這個定時器其實已經超時許久了卸奉。因此,對于毫米級精確的任務颖御,定時器并不好用榄棵。
process.nextTick()
我記得有一次面試中,面試官問過一個問題潘拱,為什么會存在如下程序:
setTimeout(function () {
// TODO
}, 0);
這段程序的目的是實現(xiàn)立即異步執(zhí)行一個任務疹鳄。但是,前邊已經說了芦岂,由于事件循環(huán)自身的特點瘪弓,以及定時器的精度不夠,另外禽最,調用定時器會動用紅黑樹腺怯,并通過紅黑樹創(chuàng)建定時器對象和迭代操作袱饭,因此,即便是setTimeout(fn, 0) 也很浪費性能呛占,我們之前講了從read到epoll的演變就是為了降低性能的浪費虑乖,但是,如果使用了這樣的定時器栓票,依然會存在性能浪費决左,非常得不償失。因此走贪,采用process.nextTick()的方法來操作就較為輕量了佛猛,經濟劃算,代碼如下:
process.nextTick = function(callback) {
// on the way out, don't bother.
// it won't get fired anyway
if (process._exiting) return;
if (tickDepth >= process.maxTickDepth)
maxTickWarn();
var tock = { callback: callback };
if (process.domain) tock.domain = process.domain;
nextTickQueue.push(tock);
if (nextTickQueue.length) {
process._needTickCallback();
}
};
每次調用process.nextTick()坠狡,只會將回調函數(shù)放入隊列中继找,在下一輪Tick時取出執(zhí)行,定時器采用紅黑樹的操作時間復雜度是o(lg(n))逃沿,nextTick()的時間復雜度僅為o(1)婴渡,效率的提高可想而知。
setImmediate()
setImmediate()與process.nextTick()方法十分類似凯亮,都是將回調函數(shù)延遲執(zhí)行边臼,在node v0.9.1之前,setImmediate()還沒有實現(xiàn)假消,因此柠并,我們可以比較一下兩個功能的用法和效率
process.nextTick(function () {
console.log('延遲執(zhí)行');
});
console.log('正常執(zhí)行');
打印出的結果是
正常執(zhí)行
延遲執(zhí)行
setImmediate(function () {
console.log('延遲執(zhí)行');
});
console.log('正常執(zhí)行');
打印出的結果也是
正常執(zhí)行
延遲執(zhí)行
但是,兩者存在細微差別富拗,process.nextTick()的優(yōu)先級會高一些臼予,我們看一下示例代碼:
process.nextTick(function () {
console.log('nextTick延遲執(zhí)行');
});
setImmediate(function () {
console.log('setImmediate延遲執(zhí)行');
});
console.log('正常執(zhí)行');
輸出結果如下
正常執(zhí)行
nextTick延遲執(zhí)行
setImmediate延遲執(zhí)行
process.nextTick()的優(yōu)先級高的原因在于事件循環(huán)對于觀察者檢查的先后順序,process.nextTick()屬于idle觀察者啃沪,setImmediate()屬于check觀察者粘拾,在每一輪循檢查中,idle觀察者會優(yōu)先于IO觀察者创千,IO觀察者又會優(yōu)先于check觀察者缰雇。
在具體實現(xiàn)上,process.nextTick()的回調函數(shù)保存在一個數(shù)組中签餐,setImmediate()的結果則保存在鏈表中寓涨,在行為上,process.nextTick()在每輪循環(huán)中會將數(shù)組中的回調函數(shù)全部執(zhí)行完氯檐,setImmediate()則在每輪循環(huán)中執(zhí)行鏈表中的一個回調函數(shù),我們來看一個例子加以佐證:
// 加入兩個 nextTick()的回調函數(shù)
process.nextTick(function () {
console.log('nextTick延遲執(zhí)行1');
});
process.nextTick(function () {
console.log('nextTick延遲執(zhí)行2');
});
// 加入兩個setImmediate()的回調函數(shù)
setImmediate(function () {
console.log('setImmediate延遲執(zhí)行1');
// 進入下次循環(huán)
process.nextTick(function () {
console.log('插入一個nextTick證明前邊的推論');
});
});
setImmediate(function () {
console.log('setImmediate延遲執(zhí)行2');
});
console.log('正常執(zhí)行');
執(zhí)行結果如下:
正常執(zhí)行
nextTick延遲執(zhí)行1
nextTick延遲執(zhí)行2
setImmediate延遲執(zhí)行1
插入一個nextTick證明前邊的推論
setImmediate延遲執(zhí)行2
從執(zhí)行結果可以看出体捏,這個優(yōu)先級的設置了冠摄。之所以這樣設計糯崎,是為了保證每輪循環(huán)能夠較快的執(zhí)行結束,防止cpu占用過多而阻塞后續(xù)IO調用河泳。
事件驅動與高性能服務器
通過前邊的介紹沃呢,我們了解了異步實現(xiàn)的原理,同時也了解了事件驅動的實質拆挥,這就是通過主循環(huán)加事件觸發(fā)的方式來運行程序薄霜。
異步IO可以用在方方面面,因為纸兔,計算機操作系統(tǒng)將設備都抽象為了文件惰瓜,因此,異步IO可以操作基礎文件汉矿、標準文件崎坊、網(wǎng)絡套接字等等,只不過使用的監(jiān)聽不一樣洲拇,例如網(wǎng)絡套接字奈揍,會有網(wǎng)絡套接字的監(jiān)聽,然后將監(jiān)聽到的請求事件交給IO觀察者赋续。利用node構建web服務器男翰,正是在這樣一個基礎上實現(xiàn)的,我們看一下這個流程:
經典web模型對比
模型 | 說明 |
---|---|
同步式 | 一次處理一個請求纽乱,其他請求處于等待狀態(tài) |
每進程/每請求 | 通過為每個請求開啟一個進程的方式處理多個請求蛾绎,但是不具備擴展性,因為系統(tǒng)資源只有那么多迫淹。 |
每線程/每請求 | 通過為每個請求開啟一個線程的方式處理多個請求秘通,線程會占有更多的內存,當大并發(fā)的請求到來時敛熬,會導致服務器緩慢 |
每線程/每請求的方式目前還被Apache所采用肺稀,node通過事件驅動的方式處理請求,無須為每個請求創(chuàng)建額外的對應線程应民,可以省掉創(chuàng)建線程和銷毀線程的開銷话原,同時操作系統(tǒng)在調度任務是因為線程較少,上下文切換的代價很低诲锹,這使得服務器可以有條不紊的處理請求繁仁,即使在大量連接的情況下,也不受線程上下文切換開銷的影響归园,這個就是node高性能的一個原因黄虱。
事件驅動帶來的高效已經漸漸開始為業(yè)界所重視,Nginx也摒棄了多線程的方式庸诱,采用于node相同的事件驅動捻浦,如今晤揣,nginx大有取代apache之勢,nginx用于反向代理和負載均衡朱灿,將nginx與node結合昧识,必然會寫出高性能高并發(fā)的好程序的。
其實盗扒,在node之前跪楞,ruby的event machine、perl的anyevent侣灶、python的twisted都采用了事件驅動的方式進行異步IO甸祭,但是由于這些語言都是以同步阻塞IO的形式制定的,因此炫隶,沒有獲得成功淋叶。另外,由于node的成功伪阶,Lua也受到了啟發(fā)煞檩,做了一個新項目叫作luavit。
總結
這一章主要講解了各種異步IO的原理和node異步IO的實現(xiàn)栅贴,并且還介紹了4中非IO的異步API斟湃。可以看出檐薯,事件循環(huán)是異步實現(xiàn)的核心凝赛,它與瀏覽器中的執(zhí)行模型基本保持了一致,使得node在構建高性能服務器方面取得了長足發(fā)展坛缕。