使用node.js 進行伺服器端JavaScript程式設計

developerworks發表於2013-01-14

  node.js 是一個可以使用 JavaScript 開發伺服器端應用的平臺。它依託於 Google V8 JavaScript 引擎,並採用事件 I/O 的架構,可以用來建立高效能伺服器。本文詳細介紹了 node.js 的基本知識、模組化的結構、事件驅動的機制以及常用的模組。

  隨著 Web 2.0 概念和 Ajax 技術的流行,JavaScript 作為 Ajax 應用開發中必不可少的一部分,已經得到了廣泛的流行。開發人員也開始逐步的熟悉和掌握 JavaScript,並積累了相關的開發經驗。雖然 JavaScript 目前主要用在 Web 應用中,以瀏覽器作為執行平臺,但是已經有相關的嘗試把 JavaScript 遷移到伺服器端,這其中包括 Aptana 的 Jaxer 等。這種做法與 Google GWT 是異曲同工的。Google GWT 允許開發人員使用 Java 語言來編寫 Web 前端程式碼。這兩種做法的目的都是為了複用開發人員已經掌握的知識和積累的經驗。在這點上,node.js 類似於 Jaxer。

  簡單的來說,node.js 是一個允許開發人員使用 JavaScript 語言編寫伺服器端程式碼的框架。也就是說編寫的 JavaScript 程式碼可以直接執行在本地機器上,而不僅限於瀏覽器。從實現的角度來說,Jaxer 和 node.js 都使用了已有的 JavaScript 執行引擎。Jaxer 用的是 Mozilla Firefox 中使用的 JavaScript 引擎,而 node.js 用的則是 Google Chrome 中用的 V8 引擎。

 node.js 入門

  node.js 可以執行在 Linux、Windows 和 Macintosh 等主流的作業系統上。在 Windows 平臺上執行 node.js 的話,需要 Cygwin 或是 MinGW 的支援。下面以常用的 Windows 平臺為例來說明。首先需要安裝 Cygwin。安裝的時候需要選擇 gcc-g++ 、make 、openssl 和 python 等包。gcc 的版本必須是最新的。接著從參考資料中給出的地址下載 node.js 0.4.0 版本的原始碼。下載解壓之後,依次在 Cygwin 中執行 ./configure 、make 和 make install 等命令進行編譯和安裝。安裝完成之後,直接執行 node 命令就可以啟動 node.js 提供的命令列。在命令列中可以直接輸入 JavaScript 程式碼並執行。也可以通過 node server.js 的方式來執行一個 JavaScript 檔案 server.js 。

  程式碼清單 1中給出了一個簡單的“Hello World”程式的示例。通過 node helloworld.js 來執行該 JavaScript 檔案之後,會在控制檯輸出“Hello World”。

  清單 1. 使用 node.js 的“Hello World”程式

 process.stdout.write("Hello World");

  程式碼清單 1中的 process 表示的是當前執行的 node.js 程式,其屬性 stdout 表示的是程式的標準輸出流。通過 write() 方法向給流中寫入一個字串。從程式碼清單 1 可以看到,使用 JavaScript 就可以訪問標準輸出流等本地系統上的資源。這從一個側面反映出來了 node.js 的強大。

  在node.js 可以執行的 JavaScript 程式碼中,可以使用一些全域性的物件:包括 程式碼清單 1中用到的 process 、下面會介紹的用來載入模組的 require() 方法、表示當前正在執行的 JavaScript 檔名的 __filename 、表示當前正在執行的 JavaScript 檔案的目錄的 __dirname 和與瀏覽器中相似的用來執行定時任務的 setTimeout() 和 setInterval() 方法等。

  在介紹了 node.js 的基本知識之後,下面介紹 node.js 的模組化結構。


 模組化結構

  node.js 使用了 CommonJS 定義的模組系統。不同的功能元件被劃分成不同的模組。應用可以根據自己的需要來選擇使用合適的模組。每個模組都會暴露一些公共的方法或屬性。模組使用者直接使用這些方法或屬性即可,不需要關係模組內部的實現細節。除了系統預置的多個模組之外,應用開發團隊也可以利用這個機制來將應用拆分成多個模組,以提高程式碼的可複用性。

  使用模組

  在node.js 中使用一個模組的方式是非常簡單的。使用某個模組之前需要首先宣告對它的依賴。在 JavaScript 程式碼中可以直接使用全域性函式 require() 來載入一個模組。如 require("http") 可以載入系統預置的 http 模組。而 require("./myModule.js") 用來載入與當前 JavaScript 檔案同一目錄下的 myModule.js 模組。如果使用 require() 的路徑以“/”開頭的話,則認為是模組 JavaScript 檔案在作業系統上的絕對路徑。如果不是這兩種情況的話,node.js 就會嘗試在當前 JavaScript 檔案的父目錄及其祖先目錄下的 node_modules 目錄下查詢。比如目錄 /usr/home/my.js 中呼叫了 require("other.js") 的話,node.js 會依次嘗試查詢下列檔案:/usr/home/node_modules/other.js 、/usr/node_modules/other.js 和 /node_modules/other.js 。

  require() 方法的返回值是該模組所暴露出來的公開 JavaScript 物件,包含了可供使用的方法和屬性。程式碼清單 2 給出了模組的基本使用方式。

  清單 2. 模組的基本使用方式

 var greetings = require("./greetings.js"); 
 var msg = greetings.sayHello("Alex", "zh_CN"); 
 process.stdout.write(msg);

  如程式碼清單 2所示,一般是直接把 require() 方法的返回值賦值給一個變數,在 JavaScript 程式碼中直接使用此變數即可。greetings.js 模組暴露了一個 sayHello() 方法,當前 JavaScript 程式碼直接使用了該方法。

  開發自己的模組

  開發自己的模組的基本工作是在模組對應的 JavaScript 檔案中編寫模組相關的程式碼。這其中封裝了模組的內部處理邏輯。一般來說,一個模組通常會暴露一些公開的方法或屬性給其使用者。模組的內部程式碼需要把這些方法或屬性給暴露出來。程式碼清單 3 給出了程式碼清單 2中所使用的 greetings.js 檔案的內容。

  清單 3. greetings.js 模組的內容

 var languages = { 
    "zh_CN" : "你好,", 
    "en" : "Hello, "
 };      
 exports.sayHello = function(name, language) { 
    return languages[language] || languages["en"] + name; 
 };

  如程式碼清單 3所示,exports 物件的內容就是模組的使用者呼叫 require() 方法的返回值中所包含的內容。模組通過這種方式來宣告其所暴露出來的公開方法和屬性。在模組中定義的變數,如 languages ,是隻對模組內部的程式碼可見的。

  如果一個模組所包含的內容比較多,也可以用資料夾的方式來組織。可以在資料夾的根目錄下面建立一個 package.json 檔案,其內容中包含了模組的名稱和入口 JavaScript 檔案的路徑。如果沒有提供這個 package.json 檔案的話,node.js 會預設在資料夾中查詢 index.js 檔案作為模組的啟動 JavaScript 檔案。

  在介紹完 node.js 的模組化結構之後,下面介紹其事件驅動機制。


 事件驅動

  開發過 Web 應用的人都熟悉瀏覽器中的事件處理機制。當對某個 DOM 元素上的某類事件感興趣的時候,只需要在該 DOM 元素上面註冊一個事件監聽器即可。如 ele.addEventListener("click", function() {}) 就新增了一個對 click 事件的監聽器。當事件發生的時候,事件監聽器的 JavaScript 方法就會被呼叫。事件的處理方法是非同步執行的。這種非同步執行的方式非常適合於開發高效能併發網路應用。實際上,目前的高效能併發應用開發一般有兩種做法:第一種是使用多執行緒的機制,另外一種就是採用基於事件驅動的方式。多執行緒的問題在於應用開發起來難度較高,很容易出現執行緒飢餓或是死鎖等問題,對開發人員提出了更高的要求。而事件驅動的方式則更加靈活,很容易為 Web 開發人員所理解和使用,也不存線上程死鎖等問題。依託於效能強大的 Google V8 引擎和先進的事件 I/O 架構,node.js 可以成為建立高效能伺服器端應用的良好基礎。

  基於 node.js 開發應用與開發 Web 應用有相似的程式設計模型。很多模組都會暴露出一些事件,使用這些模組的程式碼通過註冊事件監聽器的方式來新增相應的處理邏輯。程式碼清單 4中給出了一個簡單的 HTTP 代理伺服器的實現程式碼。

  清單 4. HTTP 代理伺服器

 var http = require("http"); 
 var url = require("url"); 

 http.createServer(function (req, res) { 
    var urlObj = url.parse(req.url, true); // 獲取被代理的 URL 
    var urlToProxy = urlObj.query.url; 
    if (!urlToProxy) { 
        res.statusCode = 400; 
        res.end("URL 是必須的。"); 
    } 
    else { 
        console.log("處理代理請求:" + urlToProxy); 
        var parsedUrl = url.parse(urlToProxy); 
        var opt = { 
            host : parsedUrl.hostname, 
            port : parsedUrl.port || 80, 
            path : (parsedUrl.pathname || "") + (parsedUrl.search || "") 
                + (parsedUrl.hash || "") 
        }; 
        http.get(opt, function(pres) { // 請求被代理 URL 的內容
            res.statusCode = pres.statusCode; 
            var headers = pres.headers; 
            for (var key in headers) { 
                res.setHeader(key, headers[key]); 
            } 
            pres.on("data", function(chunk) { 
                res.write(chunk); // 寫回資料
            }); 
            pres.on("end", function() { 
                res.end(); 
            }); 
        }); 
    } 
 }).listen(8088, "127.0.0.1"); 

 console.log("代理伺服器已經在 8088 埠啟動。");

  整個代理伺服器的實現比較簡單。首先通過 http 模組中的 createServer() 方法用來建立一個 HTTP 伺服器,再通過 listen() 方法就可以讓該 HTTP 伺服器在特定埠監聽。在 createServer() 方法中傳入的引數是 HTTP 請求的響應方法。實際上,每個 HTTP 請求都是對應於 HTTP 伺服器上的一個 request 事件。程式碼清單 4中的 HTTP 伺服器建立部分實際上等價於程式碼清單 5中給出的實現方式。

  清單 5. 使用事件機制的 HTTP 伺服器建立方式

 var server = http.createServer(); 
 server.on("request", function(req, res) { 
 });

  在請求的處理方法裡面,通過 http.get() 方法來獲取被代理 URL 的內容。這裡同樣採用了基於事件的處理方式。pres.on("data", function(chunk) {}) 在 pres 的 data 事件上新增了一個處理方法。該方法的作用是當獲取到被代理 URL 的內容的時候,就把獲取到的內容寫回到原始 HTTP 請求的響應中。對於 end 事件的處理也是同樣的。在使用 node.js 進行開發的時候,會經常遇到這種使用事件處理方法和回撥方法的場景。

  在介紹了 node.js 的事件驅動機制之後,下面介紹一些常用的模組。


 常用模組

  node.js 預設提供了很多與網路與檔案系統操作相關的模組。這些模組是構建伺服器端應用的基礎。下面對其中一些常見的模組進行具體說明。

  事件模組

  前面提到過,node.js 採用的是事件驅動的架構,其中的很多模組都會產生各種不同的事件,可以由模組使用者來新增事件處理方法。所有能夠產生事件的物件都是事件模組中的 EventEmitter 類的例項。

  EventEmitter 類中的方法都與事件的產生和處理相關,如下所示:

  • addListener(event, listener) 和 on(event, listener) :這兩個方法的作用都是用來為某個事件 event 新增事件處理方法 listener 。
  • once(event, listener) :這個方法為某個事件 event 新增僅執行一次的處理方法 listener 。處理方法在執行一次之後就會被刪除。
  • removeListener(event, listener) :該方法用來刪除某個事件 event 上的處理方法 listener 。
  • emit(event, [arg1], [arg2], [...]) :該方法用來產生某個事件 event 。事件名稱 event 之後的引數被傳遞給對應的事件處理方法。

  程式碼清單 6給出了事件模組的使用示例。

  清單 6. 事件模組的使用示例

 var events = require("events"); 
 var emitter = new events.EventEmitter(); 
 emitter.on("myEvent", function(msg) { 
    console.log(msg); 
 }); 
 emitter.emit("myEvent", "Hello World.");

  在事件模組中有一個特殊的事件 error 。當出現錯誤的時候,EventEmitter 會產生此事件。如果此事件沒有對應的處理方法的話,預設的行為是輸出錯誤資訊後,程式自動終止。因此,需要注意總是新增一個對 error 事件的處理方法。

  node.js 中存在各式各樣不同的資料流,包括檔案系統、HTTP 請求和響應、以及 TCP/UDP 連線等。這些流都是 EventEmitter 的例項,因此可以產生各種不同的事件。流可以分成只讀、只寫和讀寫流三種。

  可讀流主要會產生 4 個事件:

  • data :當讀取到流中的資料時,產生此事件。
  • end :當流中沒有資料可讀時,產生此事件。
  • error :當讀取資料出現錯誤時,產生此事件。
  • close :當流被關閉時,產生此事件。

  除了上面的事件之外,還有一個 pipe() 方法可以用來把當前的可讀流和另外一個可寫流連線起來。可讀流中的資料會被自動寫入到可寫流中。

  可寫流中最常用的是 write() 和 end() 兩個方法。write() 方法用來向流中寫入資料,而 end() 則用來結束寫入操作。

  為了表示二進位制資料,node.js 使用了類 Buffer 來表示資料緩衝區,以對二進位制資料進行操作。Buffer 類內部是以陣列的方式來儲存資料的。一旦建立出來之後,Buffer 的大小是不能被修改的。Buffer 類的例項是可以與 JavaScript 中的字串型別互相轉換的。在轉換的時候需要指定編碼格式。通過 Buffer 類的 toString(encoding, start, end) 方法可以把 Buffer 中的從 start 到 end 的內容轉換成以 encoding 編碼的字串。可以支援的編碼格式有:ascii 、utf8 和 base64 。通過 new Buffer(str, encoding) 可以用一個字串 str 來初始化一個緩衝區。write(string, offset, encoding) 用來把一個字串 string 以編碼格式 encoding 寫入到緩衝區中以 offset 開始的位置上。

  網路操作

  node.js 提供了一些與網路操作相關的模組,包括 TCP、UDP 和 HTTP 等,可以實現網路伺服器和客戶端。

  與 TCP 協議相關的實現在 net 模組中。通過該模組的 createServer(connectionListener) 方法可以建立一個 TCP 伺服器。引數 connectionListener 是當有客戶端連線到該伺服器上時的處理方法,等價於對 connect 事件的處理。一個 TCP 伺服器是類 Server 的例項。通過 listen 方法可以讓伺服器在指定埠監聽。

  如果想連線到一個已有的 TCP 伺服器的話,可以使用 createConnection(port, host) 方法來連線到指定主機 host 的埠 port 上。該方法的返回值是 Socket 類的例項,表示一個套接字連線。得到一個 Socket 類的例項之後,就可以通過 write() 方法來向該連線中寫入資料。如果想從該連線上獲取資料的話,可以新增 data 事件的處理方法。

  程式碼清單7 中給出了一個簡單的用來進行表示式計算的 TCP 伺服器,可以通過 telnet 命令連線到此伺服器來進行測試。

  清單 7. 簡單的表示式計算伺服器

 var net = require("net"); 
 var server = net.createServer(function(socket) { 
    socket.setEncoding("utf8"); 
    var buffer = [], len = 0; 
    socket.on("data", function(data) { // 接收到客戶端資料
        if (data.charCodeAt(0) == 13) { 
            var expr = buffer.join(""); 
            try { 
                var result = eval(expr); // 進行計算
                socket.write(result.toString()); // 寫回結果
            } catch (e) { 
                socket.write("Wrong expression."); 
            } finally { 
                socket.write("\r\n"); 
                buffer = []; 
            } 
        } 
        else { 
            buffer.push(data); 
        } 
    }); 
 }); 
 server.listen(8180, "127.0.0.1"); 
 console.log("伺服器已經在埠 8180 啟動。");

  除了 TCP 伺服器外,模組 http 和 https 可以分別實現 HTTP 和 HTTPS 伺服器,模組 dgram 可以實現 UDP/Datagram 套接字連線,模組 tls 可以實現安全的套接字連線(SSL)。這些模組的使用方式都類似於模組 tcp 。

  檔案系統

  node.js 中的 fs 模組用來對本地檔案系統進行操作。fs 模組中提供的方法可以用來執行基本的檔案操作,包括讀、寫、重新命名、建立和刪除目錄以及獲取檔案後設資料等。每個操作檔案的方法都有同步和非同步兩個版本。非同步操作的版本都會使用一個回撥方法作為最後一個引數。當操作完成的時候,該回撥方法會被呼叫。而回撥方法的第一個引數總是保留為操作時可能出現的異常。如果操作正確成功,則第一個引數的值是 null 或 undefined 。而同步操作的版本的方法名稱則是在對應的非同步方法之後加上一個 Sync 作為字尾。比如非同步的 rename() 方法的同步版本是 renameSync() 。下面列出來了 fs 模組中的一些常用方法,都只介紹非同步操作的版本。

  • rename(path1, path2) :將路徑 path1 表示的目錄或檔案重新命名成路徑 path2 。
  • truncate(fd, len) :將檔案描述符 fd 對應的檔案的長度截斷為 len 。
  • chmod(path, mode) :將路徑 path 表示的目錄或檔案的許可權修改為 mode 。
  • stat(path) :獲取路徑 path 表示的目錄或檔案的後設資料。後設資料用 Stats 類來表示。
  • open(path, flags, mode) :開啟一個路徑 path 表示的檔案。回撥方法中可以得到該檔案的描述符。
  • read(fd, buffer, offset, length, position) :讀取給定檔案描述符 fd 所表示的檔案中從 position 位置開始的長度為 length 位元組的資料,並存放到緩衝區 buffer 中從 offset 起始的位置上。回撥方法中可以得到實際讀取的位元組數。
  • write(fd, buffer, offset, length, position) :將緩衝區 buffer 中的資料寫入到檔案描述符 fd 所表示的檔案中。引數的含義與 read() 方法一樣。回撥方法中可以得到實際寫入的位元組數。
  • readFile(filename, encoding) :以編碼格式 encoding 來讀取一個檔案 filename 中的內容。回撥方法中可以得到檔案的內容。
  • writeFile(filename, data, encoding) :將資料 data 以編碼格式 encoding 寫入到檔案 filename 中。

  除了上面列出來的直接操作檔案本身的方法外,還可以把檔案轉換成流。createReadStream(path, options) 和 createWriteStream(path, options) 分別用來從檔案中建立可讀和可寫流。引數 path 表示的是檔案的路徑,options 是一個表示讀取或寫入檔案時的選項的 JavaScript 物件。

  程式碼清單 8中給出了一個簡單的 HTTP 靜態檔案伺服器的實現。

  清單 8. HTTP 靜態檔案伺服器

 var http = require("http"), 
    fs = require("fs"), 
    path = require("path"), 
    url = require("url"); 

 var server = http.createServer(function(req, res) { 
    var pathname = url.parse(req.url).pathname; 
    var filepath = path.join("/tmp", "wwwroot", pathname); 
    var stream = fs.createReadStream(filepath, {flags : "r", encoding : null}); 
    stream.on("error", function() { 
        res.writeHead(404); 
        res.end(); 
    }); 
    stream.pipe(res); 
 }); 
 server.on("error", function(error) { 
    console.log(error); 
 }); 
 server.listen(8088, "127.0.0.1");

  如程式碼清單 8所示,首先把 HTTP 請求的路徑轉換成伺服器上檔案路徑,再從檔案中建立可讀流,最後通過 pipe() 方法把檔案的資料流傳遞到 HTTP 請求的響應中。

  輔助模組

  除了上面介紹的這些常見模組之外,node.js 還提供了一些輔助的模組。

  模組 path 用來處理檔案系統上的路徑。這個模組中的 join() 用來把多個路徑連線起來,形成一個完整的路徑。如 join("/usr", "home", "test/index.html") 的結果是路徑 /usr/home/test/index.html 。normalize() 用來對路徑進行歸一化操作,去掉其中多餘的“/”以及處理“..”和“.”。resolve([from ...], to) 方法用來獲取給定路徑 to 的絕對路徑。如果 to 不是絕對路徑,就把它之前的引數按從右到左的順序新增上去,直到得到了一個絕對路徑。如果到最後還是無法得到絕對路徑,就把當前的工作目錄加上。假設當前的工作目錄是 /usr/home ,那麼 resolve("test", "index.html") 的返回結果是 /usr/home/test/index.html 。dirname() 方法用來獲取路徑的目錄部分。如 dirname("/usr/home/index.html") 的返回結果是 /usr/home 。basename() 用來獲取路徑的最後一個部分。如 basename("/usr/home/index.html") 的返回結果是 index.html 。extname() 用來獲取一個路徑的副檔名部分。如 extname("/usr/home/index.html") 的返回結果是 .html 。

  模組 url 用來對 URL 進行解析。parse(urlStr, parseQueryString) 方法用來把一個 URL 字串 urlStr 解析成主機名、埠和路徑等幾個部分。該方法的返回值是一個包含了 protocol 、hostname 、port 、pathname 和 query 等屬性的 JavaScript 物件。如果引數 parseQueryString 的值為 true 的話,URL 中包含的查詢字串部分也會被解析。format(urlObj) 方法的作用與 parse() 方法正好相反,用來從一個 JavaScript 物件中構建出 URL 字串。

  模組 querystring 用來處理 URL 中的查詢字串。stringify(obj) 方法用來把一個 JavaScript 物件 obj 轉換成查詢字串的格式。如 stringify({a : 1, b : "good"}) 的返回結果是 a=1&b=good 。parse(str) 用來把一個查詢字串解析成 JavaScript 物件。

  模組 vm 可以用來執行 JavaScript 程式碼。方法 runInThisContext(code) 用來執行一段 JavaScript 程式碼 code 並返回其結果。通過該方法執行的 JavaScript 程式碼不能訪問當前程式碼的作用域。runInNewContext(code, [sandbox]) 方法也是用來執行 JavaScript 程式碼的,與 runInThisContext() 不同的是通過該方法執行的 JavaScript 程式碼使用 sandbox 物件作為全域性物件。如 runInNewContext("a + 3", {a : 4}) 的返回結果是 7。createScript(code) 方法用來預先編譯一段 JavaScript 程式碼,但是並不立即執行。該方法的返回值是一個 Script 物件。該物件同樣有 runInThisContext() 和 runInNewContext([sandbox]) 兩個方法,含義與上面提到的兩個方法類似。

  模組 os 提供了與底層作業系統相關的一些資訊。包括 hostname() 用來獲取作業系統的主機名;type() 用來獲取作業系統的型別;release() 用來獲取作業系統的發行版本號;uptime() 用來獲取以秒計算的系統執行時間;cpus() 用來獲取 CPU 的相關資訊。freemem() 和 totalmem() 分別用來獲取系統的記憶體總數和可用記憶體數。

  模組 util 提供了一些常用的輔助方法。debug(string) 方法用來輸出資訊到標準錯誤流。log(string) 方法用來輸出附帶時間戳的資訊到標準輸出流。inspect(object, showHidden, depth) 方法用來輸出一個物件的內部結構,引數 object 是要檢視的物件,showHidden 表示是否檢視物件的隱藏屬性,depth 表示檢視的物件層次結構的深度,預設值是 2。inherits(constructor, superConstructor) 方法用來實現 JavaScript 中基於原型的繼承機制。

  在介紹完 node.js 提供的常用模組之後,下面通過一個完整的示例來說明 node.js 的用法。


 例項分析

  這個例項實現的功能是動態監測伺服器的記憶體使用狀態,即記憶體的佔用率。獲取伺服器上的記憶體佔用率比較簡單,只需要使用 os 模組提供的方法即可,即 freemem()/totalmem() 。為了能夠實時的監測記憶體佔有率,伺服器需要實時的把資料傳輸給瀏覽器端。這裡最好的實現方式是 HTML 5 中引入的 WebSocket 規範。該規範在 Firefox 4 和 Google Chrome 等新瀏覽器上得到了支援。同時伺服器端也需要支援此規範。Socket.IO 在 node.js 上提供了對 WebSocket 規範的支援,包括伺服器端和瀏覽器端程式碼。程式碼清單 9給出了使用 Socket.IO 的伺服器端程式碼。

  清單 9. 監測記憶體佔用率的伺服器端程式碼

 var io = require('./socket.io'); 
 var io = io.listen(server); 
 io.on("connection", function(client){ 
    setInterval(function() { 
        client.send(os.freemem() / os.totalmem()); 
    }, 500); 
 });

  在程式碼清單 9中,server 是 node.js 中的一個 HTTP 伺服器物件,用來響應一般的 HTTP 請求。Socket.IO 可以對 node.js 的 HTTP 伺服器的請求進行攔截,將部分請求交給 Socket.IO 來處理。這裡的處理邏輯是當有客戶端連線上的時候,就每隔 500 毫秒把伺服器的記憶體佔用率傳送給客戶端。程式碼清單 10給出了瀏覽器端的 HTML 和 JavaScript 程式碼。

  清單 10. 監測記憶體佔用率的瀏覽器端程式碼

 <!doctype html> 
 <html> 
  <head> 
    <title> 伺服器記憶體使用情況 </title> 
    <script src="/socket.io/socket.io.js"></script> 
    <style> 
        #usage {border : 1px dashed green;} 
    </style> 
    <script> 
        var canvas, width = 200, height = 200, buffer = [], max = 200; 
        function updateChart(data) { 
            if (buffer.length >= max) { 
                buffer.unshift(); 
            } 
            buffer.push(data); 
            var ctx = canvas.getContext("2d"); 
            ctx.clearRect(0, 0, width, height); 
            for (var i = 1, n = buffer.length; i < n; i++) { 
                ctx.strokeStyle = "red"; 
                ctx.beginPath(); 
                ctx.moveTo(i - 1 , buffer[i - 1] * height); 
                ctx.lineTo(i, buffer[i] * height); 
                ctx.closePath(); 
                ctx.stroke(); 
            } 
        } 
    
        function onLoad() { 
            canvas = document.getElementById("usage"); 
            var socket = new io.Socket(null, {port: 8088}); 
            socket.connect(); // 連線到伺服器
            socket.on("message", function(obj){ // 接收到訊息時的處理方法
                updateChart(obj); 
            }); 
        } 
    </script> 
  </head> 
  <body onload="onLoad();"> 
    <h1> 記憶體使用情況 </h1>   
    <canvas id="usage" width="200" height="200"></canvas> 
  </body> 
 </html>

  如程式碼清單 10所示,首先建立一個與伺服器之間的 WebSocket 連線。通過 message 事件定義了當接收到伺服器端的訊息時,更新瀏覽器端的顯示。瀏覽器端通過一個 HTML 5 提供的 canvas 來繪製記憶體佔用率的曲線圖,如圖1所示。

  圖 1. 記憶體佔用率的曲線圖
記憶體佔用率的曲線圖


 總結

  一提到伺服器端開發,開發人員一般想到的就是 Java 和 C/C++ 等語言。但是通過 node.js 提供的強大能力,熟悉 JavaScript 的 Web 開發人員也可以開發出伺服器端的應用。本文詳細介紹了 node.js 的事件驅動機制和模組化結構,並對其中的常用模組做了詳細說明,最後通過一個完整的例項展示了 node.js 的實用性。

 下載

  本文中用到的 node.js 上開發的 JavaScript 程式碼 sample.zip

  學習

  獲得產品和技術

  討論

相關文章