Node.js 初體驗

zt80 12年前發布 | 979 次閱讀 5.2.1版本發布

服務器端 JS 情緣

在校期間我學會了JavaScript和Java,當時我就在考慮JS有沒有類似JSP一樣的服務器端程序,名字應該是JSSP(JavaScript Server Page),可以在 HTML 中嵌入 JS。Google了一圈發現IIS支持用JScript代替VBScript做ASP開發,另外SourceForge上真有個叫JSSP的項目,以及今天的主角Node.js。當時的Node.js剛起步,首頁背景還是黑乎乎的(不曉得其他童鞋是否也有印象)。經過一圈比較,我最終選擇使用Rhino——一個純Java實現的JS引擎,它吸引我的地方是能直接調用Java類庫。

Node.js

最近關注Node.js人變多了。在長期與一堆厚重的Java框架、類庫為伍之后,我也想看看外面的世界。Node.js最為人所津津樂道的就是異步加回調機制以及良好的性能。我想知道它和我熟悉的Java有何不同。

Node.js 要解決的問題

在使用Java開發的過程里,經常會有與下面類似的代碼:

// block A
// do something

// block B // on Database ResultSet rs = dbo.executeQuery("Query Statement");

while (rs.next()) { // block C // parse the result }

// block D // do something </pre>

代碼塊A先處理一些任務;代碼塊B發送查詢語句到數據庫,等待返回數據集;代碼塊C處理返回結果;代碼塊D繼續做其他事情。執行時序圖如下:

[[1.png]]

容易看出,在等待代碼塊B時,整個程序都暫停了,中間有一大段空閑時間沒有處理任何任務。從依賴關系上說,代碼塊C必須在代碼塊B成功執行后才能執行;但代碼塊D對前面的B、C并沒有依賴關系。因此,如果在等待期間先執行代碼塊D,直到代碼塊B執行完畢再觸發代碼塊C。如下圖所示:

[[2.png]]

假設每個代碼塊所需的執行時間是5秒,那第一種方案需要20秒,而第二種只需要15秒。Node.js要做的事情就是使用第二種方案取代第一種方案以獲得性能的提升。

回調函數隊列

情理之中意料之外,Node.js實現的方式是單進程且單線程。它內部維護著一個回調函數隊列,遵循先到先處理的原則逐個執行。讓我聯想到[[http://zh.wikipedia.org/wiki/%E6%89%B9%E5%A4%84%E7%90%86%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F][批處理操作系統]] ,任務一個接一個地執行,沒有搶占并享有所有系統資源。Node.js回調機制所做的事情就是把相應的代碼塊塞到隊尾。

比如上一節的例子中的方法二,執行過程就變成:代碼塊A被塞到隊列中;數據庫查詢語句注冊了一個事件并綁定代碼塊C為回調函數;代碼塊D被塞到隊尾;此時代碼塊B執行完成并觸發事件,把代碼塊C塞到隊尾。因此,依次執行的是A、D、C(注:B是數據庫服務器上的查詢操作,并不是Node.js中執行的代碼),期間并無間歇。

考慮下面的代碼,就遵循上述的代碼模式:

  1. 代碼塊A:請求計數以及記錄請求開始處理的時間(不是到達時間)。
  2. 代碼塊B:此處用setTimeout做了5秒延遲,模擬外部程序處理五秒鐘。
  3. 代碼塊C:記錄回調函數被調用的時間,睡5秒來模擬服務器運算,并記錄結束時間。
  4. 代碼塊D:記錄響應的時間(即用戶收到回饋的時間)。
var http = require('http');
var count = 0;

http.createServer(function(request, response) {
    // block A
    count++;
    var id = count;

    var start = new Date();
    var reply;

    // block B
    setTimeout(function() {
        // block C
        var called = new Date();
        var end;

        do {
            end = new Date();
        } while (end.getTime() - called.getTime() < 5000);

        console.log(id + ' start @ ' + start);
        console.log(id + ' reply @ ' + reply);
        console.log(id + ' called @ ' + called);
        console.log(id + ' end @ ' + end);
    }, 5000);

    // block D
    response.writeHead(200, {'Content-Type': 'text/html'});
    response.end();
    reply = new Date();
}).listen(80);

在我的機器上執行結果如下:

λ sudo node main.js 
1 start @ Fri Sep 21 2012 19:12:35 GMT+0800 (CST)
1 reply @ Fri Sep 21 2012 19:12:35 GMT+0800 (CST)
1 called @ Fri Sep 21 2012 19:12:40 GMT+0800 (CST)
1 end @ Fri Sep 21 2012 19:12:45 GMT+0800 (CST)

和預期的一樣:請求在19:12:35時開始處理,并且沒有被阻塞,而是在同一時間就返回了;5秒后回調函數開始被處理;又過了5秒回調函數執行完畢,整個過程結束。

我們來數數上面這段代碼總共有幾個顯眼的回調函數:

  1. 整段代碼/文件是一個回調函數,在程序啟動時被塞到隊列中并立即執行了;
  2. createServer 中注冊的回調函數,在收到用戶請求后被觸發(塞到隊列,不一定馬上執行);
  3. setTimeout 中注冊的回調函數,在延遲5后才被塞到隊列。

單線程的問題

Node.js采用單進程+單線程的其中一個原因是避免系統頻繁開辟線程帶來的開銷。網絡上說開啟一個線程要使用2M的內存(有這么多?),我沒去驗證過具體數值,但至少是有一些開銷的,當請求量大是的確會成為瓶頸。上節提到Node.js的處理任務的方式類似批處理操作系統,因此它在規避線程開銷的同時也完全繼承了批處理方式的缺陷——交互不友好。我指的交互是后面的請求會被回調函數隊列中前面的任務阻塞住,從接受到回饋耗費很長的時間等待。

用下面代碼分別在第0秒、第1秒、第7秒發送一個請求,并記錄每個請求從發出到收到回饋的時間:

#!/bin/bash

date
time curl http://localhost
sleep 1
date
time curl http://localhost
sleep 6
date
time curl http://localhost
date

執行結果如下:

  1. 19:16:36 發送第一個請求,幾乎馬上收到回饋;
  2. 等待1秒后發送第二個請求,同樣馬上收到回饋;
  3. 繼續等待6秒發送第三個請求,耗時8秒后猜收到回饋!
  4. </ol>
    $ ./submit.sh 
    2012年 09月 21日 星期五 19:16:36 CST

    real 0m0.018s user 0m0.004s sys 0m0.008s 2012年 09月 21日 星期五 19:16:37 CST

    real 0m0.015s user 0m0.012s sys 0m0.000s 2012年 09月 21日 星期五 19:16:43 CST

    real 0m7.982s user 0m0.000s sys 0m0.004s 2012年 09月 21日 星期五 19:16:51 CST</pre>

    相信第三次請求的用戶會極其不滿,來看看Node.js執行時的快照:

    1 start @ Fri Sep 21 2012 19:16:36 GMT+0800 (CST)
    1 reply @ Fri Sep 21 2012 19:16:36 GMT+0800 (CST)
    1 called @ Fri Sep 21 2012 19:16:41 GMT+0800 (CST)
    1 end @ Fri Sep 21 2012 19:16:46 GMT+0800 (CST)
    2 start @ Fri Sep 21 2012 19:16:37 GMT+0800 (CST)
    2 reply @ Fri Sep 21 2012 19:16:37 GMT+0800 (CST)
    2 called @ Fri Sep 21 2012 19:16:46 GMT+0800 (CST)
    2 end @ Fri Sep 21 2012 19:16:51 GMT+0800 (CST)
    3 start @ Fri Sep 21 2012 19:16:51 GMT+0800 (CST)
    3 reply @ Fri Sep 21 2012 19:16:51 GMT+0800 (CST)
    3 called @ Fri Sep 21 2012 19:16:56 GMT+0800 (CST)
    3 end @ Fri Sep 21 2012 19:17:01 GMT+0800 (CST)

    下表中A1表示第一次請求中的代碼塊A,其他以此類推。假定代碼塊A、B、D都是瞬間完成,只有C耗時5秒。從表中可知,第三次請求是在第15鐘才開始被處理。根據測試腳本的輸出可知,第三次請求其實在第7秒就已經發出了,但由于是那時隊列中還有C1、C2在處理,因此等待了8秒鐘!假設隊列的平均長度是100,那每個請求平均的等待時間就是 (100 / 4 - 1) * (A+B+C+D),即要等前面24個請求處理完。這還僅僅是請求得到回饋的時間,該請求對應回調函數被執行的時間還要更久。

    </tr>

    </tbody>

    </tr>

    </tbody> </table>

    適用場景

    通過上面的研究,我覺得Node.js并不適合需要與用戶實時交互的系統;它適合集中處理用戶發來的大規模“指令”,即不需要及時看到結果的請求。比如微博系統,用戶發表一條微博,可能需要在服務器上排隊1分鐘才能最終保存到數據庫。在這一分鐘里,用戶更多地是看看別人發表的微博,并不十分迫切地想看到自己那條微博。如果希望有更好的體驗,其實可以用DOM直接把用戶發表的微博先更新到當前頁面,同時使用Ajax異步請求保存這條數據。

     本文由用戶 zt80 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
    寫博客

    博客分類

    推薦閱讀

    Node.js 初體驗

    服務器端 JS 情緣 在校期間我學會了JavaScript和Java,當時我就在考慮JS有沒有類似JSP一樣的服務器端程序,名字應該是JSSP(JavaScript Server Page),可...

    VmWare初體驗

    看一個新進同事,演示 VmWare的集群環境的搭建,覺的,這個東西,不錯 從公司服務器上,下載VMwareworkstation_7.1.3.exe, 安裝很簡單,傻瓜式的 裝完后,? 再從公司...

    SockJS+CoffeeScript+ Node.js組合使用指南

    I recently did a blog post on Socket.IO . Today I’ll go over an alternative called SockJS . Accor...

    node.js 后端框架設計構想

    我打算把我的后端的框架定位為建站框架,本文是我的一些思路與初步實踐。如果園子里有做過后端框架的高手(不限語言),也請指教一下。以下是大概的流程。 后端的核心文件mass.js包含批量創建與刪除文...
    當前時間 當前隊列
    0 A1 B1 D1
    1 A2 B2 D1
    5 C1
    6 C1 C2
    7 C1 C2 A3 B3 D3
    10 C2 A3 B3 D3
    15 A3 B3 D3
    20 C3
    25
sesese色