使用Node.js作為完整的云環(huán)境開發(fā)堆棧
隨著技術(shù)創(chuàng)新表面上繼續(xù)以指數(shù)級速度發(fā)展,新思想層出不窮。服務(wù)器端的 JavaScript 就是這些新思想之一。 Node.js 是一種事件驅(qū)動的 I/O 框架,用于 UNIX 類平臺上的 V8 JavaScript 引擎,適合于編寫可伸縮的網(wǎng)絡(luò)程序,如 Web 服務(wù)器。 Node.js 正是這種新思想的實(shí)現(xiàn)。
51CTO推薦專題:Node.js專區(qū)
Node.js 并非與 JavaScript 抗衡,而是使用它作為完整的開發(fā)堆棧,從服務(wù)器端代碼一直延伸到瀏覽器。Node.js 還充分利用了另一種創(chuàng)新思想:通過回調(diào)利用異步 I/O 的并發(fā)性模型。
Node.js 云計(jì)算平臺
在云計(jì)算環(huán)境中使用 Node.js 框架時,能顯示出它的一個巨大優(yōu)點(diǎn)。對于應(yīng)用程序開發(fā)人員,這往往歸結(jié)使用平臺即服務(wù) (PaaS) 或基礎(chǔ)架構(gòu)即服務(wù) (IaaS) 模型。對于開發(fā)人員而言,最抽象和公認(rèn)最方便的方法是使用 PaaS 提供程序。圖 1 十分簡單地說明了 PaaS 和IaaS 模型的結(jié)構(gòu)。
圖 1. PaaS 與 IaaS 結(jié)構(gòu)

最近,一個激動人心的開源項(xiàng)目 Cloud Foundry 公布了代碼以創(chuàng)建一個能夠運(yùn)行 Node.js的私有 PaaS。同樣的主機(jī)引擎也可用在公共云和商業(yè)云中,而且它們接受軟件補(bǔ)丁。
基礎(chǔ)架構(gòu)管理是一大痛點(diǎn),如果能夠?qū)⑦@項(xiàng)工作外包(永遠(yuǎn)!)給規(guī)模經(jīng)營的提供商,且無論是源代碼,還是物理硬件資源,對于開發(fā)人員確實(shí)是一個激動人心的時刻。
使用 Node.js shell
在我們著手編寫一個完整的 Node.js 例子之前,讓我們先開始介紹如何使用交互式 shell。如果尚未安裝 Node.js,您可以參考資源部分,然后按照說明安裝它,或者使用在線的交互式 Node.js 站點(diǎn)之一,它允許您直接在瀏覽器中輸入代碼。
要在 Node.js 中以交互方式編寫 JavaScript 函數(shù),在命令行提示中輸入node,如下所示:
- lion% node
- > var foo = {bar: ‘baz’};
- > console.log(foo);
- { bar: ‘baz’ }
- >
在這個例子中,創(chuàng)建了對象foo,然后調(diào)用console.log 將它輸出到控制臺。這十分有效而且有趣,不過當(dāng)您使用 tab 完成功能來探討 foo 時,如下面的例子所示,真正的樂趣才剛剛開始。如果輸入 foo.bar.,然后按下 tab 鍵,您將看到對象上的可用方法。
- > foo.bar.
- [...output suppressed for space...]
- foo.bar.toUpperCase foo.bar.trim
- foo.bar.trimLeft foo.bar.trimRight
試用 toUpperCase 方法似乎很有趣,下面顯示了它的用法:
- > foo.bar.toUpperCase();
- ‘BAZ’
您可以看到,該方法將字符串轉(zhuǎn)換為大寫字母。這類交互式開發(fā)非常適合于使用像 Node.js這樣的事件驅(qū)動型框架進(jìn)行開發(fā)。
在完成簡單介紹之后,我們開始真正地構(gòu)建一些東西。
用 Node.js 構(gòu)建聊天服務(wù)器
Node.js 讓編寫基于事件的網(wǎng)絡(luò)服務(wù)器變得十分簡單。例如,讓我們創(chuàng)建一些聊天服務(wù)器。***個服務(wù)器十分簡單,幾乎沒有什么功能,也沒有任何異常處理。
一個聊天服務(wù)器允許多個客戶端連接到它。每個客戶端都可以編寫消息,然后廣播給所有其他用戶。下面給出了最簡單的聊天服務(wù)器的代碼。
- net = require(‘net’);
- var sockets = [];
- var s = net.Server(function(socket) {
- sockets.push(socket);
- socket.on(‘data’, function(d) {
- for (var i=0; i < sockets.length; i++ ) {
- sockets[i].write(d);
- }
- });
- });
- s.listen(8001);
在不到 20 行代碼中(實(shí)際上,真正實(shí)現(xiàn)功能的代碼只有 8 行),您已經(jīng)構(gòu)建了一個能夠使用的聊天服務(wù)器。下面是這個簡單程序的流程:
◆ 當(dāng)一個套接字進(jìn)行連接時,將該套接字對象附加到一個數(shù)組。
◆ 當(dāng)客戶端寫入它們的連接時,將該數(shù)據(jù)寫到所有的套接字。
現(xiàn)在,讓我們檢查所有代碼,并解釋這個例子如何實(shí)現(xiàn)聊天服務(wù)器預(yù)定功能。***行允許訪問 net 模塊的內(nèi)容:
- net = require(‘net’);
讓我們使用這個模塊中的 Server。
您將需要一個位置來保存所有客戶端連接,以便在寫入數(shù)據(jù)時可以寫到它們中去。下面是用于保存所有客戶端套接字連接的變量:
- var sockets = [];
下一行開始一個代碼塊,規(guī)定當(dāng)每個客戶端連接時要做的事情。
- var s = net.Server(function(socket) {
傳遞到 Server 中的惟一參數(shù)是將針對每個客戶端連接進(jìn)行調(diào)用的一個函數(shù)。在這個函數(shù)中,將客戶端連接添加到所有客戶端連接的列表中:
- sockets.push(socket);
下一部分代碼建立了一個事件處理器,規(guī)定了當(dāng)一個客戶端發(fā)送數(shù)據(jù)時要做的事情:
- socket.on(‘data’, function(d) {
- for (var i=0; i < sockets.length; i++ ) {
- sockets[i].write(d);
- }
- });
socket.on() 方法調(diào)用為節(jié)點(diǎn)注冊一個事件處理器,以便當(dāng)某些事件發(fā)生時它知道如何處理。當(dāng)接收到來自客戶端的數(shù)據(jù)時,Node.js 會調(diào)用這個特殊的事件處理器。其他的事件處理器包括 connect、end、timeout、drain、error 和 close。
socket.on() 方法調(diào)用的結(jié)構(gòu)類似于前面提過的 Server() 調(diào)用。您傳入一個函數(shù)給這兩者,當(dāng)有事發(fā)生時調(diào)用此函數(shù)。這種回調(diào)方法在異步網(wǎng)絡(luò)框架中很常見。這是當(dāng)開始使用像 Node.js 這樣的異步框架時,擁有過程編程經(jīng)驗(yàn)的人會遇到的主要問題。
在這種情況下,當(dāng)任意客戶端發(fā)送數(shù)據(jù)給服務(wù)器時,就會調(diào)用這個匿名函數(shù)并將數(shù)據(jù)傳入函數(shù)中。它基于您已經(jīng)積累的套接字對象列表進(jìn)行迭代,并給它們?nèi)堪l(fā)送相同的數(shù)據(jù)。每個客戶端連接都將接收到這些數(shù)據(jù)。
這個聊天服務(wù)器十分簡單,它缺少一些非常基礎(chǔ)的功能,比如識別是誰發(fā)送哪條消息,或者處理某個客戶端斷開的情況。(如果一個客戶端從這臺聊天服務(wù)器斷開,任何人發(fā)送消息,服務(wù)器都會崩潰。)
下面的源代碼(在下載示例文件中叫做 chat2.js )是一個經(jīng)過改進(jìn)的套接字服務(wù)器,其功能有所增強(qiáng),能夠處理“糟糕的情況“(比如客戶端斷開)。
- net = require(‘net’);
- var sockets = [];
- var name_map = new Array();
- var chuck_quotes = [
- "There used to be a street named after Chuck Norris, but it was changed because
- nobody crosses Chuck Norris and lives.",
- "Chuck Norris died 20 years ago, Death just hasn't built up the courage to tell
- him yet.",
- "Chuck Norris has already been to Mars; that's why there are no signs of life.",
- "Some magicians can walk on water, Chuck Norris can swim through land.",
- "Chuck Norris and Superman once fought each other on a bet. The loser had to start
- wearing his underwear on the outside of his pants."
- ]
- function get_username(socket) {
- var name = socket.remoteAddress;
- for (var k in name_map) {
- if (name_map[k] == socket) {
- name = k;
- }
- }
- return name;
- }
- function delete_user(socket) {
- var old_name = get_username(socket);
- if (old_name != null) {
- delete(name_map[old_name]);
- }
- }
- function send_to_all(message, from_socket, ignore_header) {
- username = get_username(from_socket);
- for (var i=0; i < sockets.length; i++ ) {
- if (from_socket != sockets[i]) {
- if (ignore_header) {
- send_to_socket(sockets[i], message);
- }
- else {
- send_to_socket(sockets[i], username + ‘: ‘ + message);
- }
- }
- }
- }
- function send_to_socket(socket, message) {
- socket.write(message + ‘\n’);
- }
- function execute_command(socket, command, args) {
- if (command == ‘identify’) {
- delete_user(socket);
- name = args.split(‘ ‘, 1)[0];
- name_map[name] = socket;
- }
- if (command == ‘me’) {
- name = get_username(socket);
- send_to_all(‘**’ + name + ‘** ‘ + args, socket, true);
- }
- if (command == ‘chuck’) {
- var i = Math.floor(Math.random() * chuck_quotes.length);
- send_to_all(chuck_quotes[i], socket, true);
- }
- if (command == ‘who’) {
- send_to_socket(socket, ‘Identified users:’);
- for (var name in name_map) {
- send_to_socket(socket, ‘- ‘ + name);
- }
- }
- }
- function send_private_message(socket, recipient_name, message) {
- to_socket = name_map[recipient_name];
- if (! to_socket) {
- send_to_socket(socket, recipient_name + ‘ is not a valid user’);
- return;
- }
- send_to_socket(to_socket, ‘[ DM ' + get_username(socket) + ' ]: ‘ + message);
- }
- var s = net.Server(function(socket) {
- sockets.push(socket);
- socket.on(‘data’, function(d) {
- ddata = d.toString(‘utf8′).trim();
- // check if it is a command
- var cmd_re = /^\/([a-z]+)[ ]*(.*)/g;
- var dm_re = /^@([a-z]+)[ ]+(.*)/g;
- cmd_match = cmd_re.exec(data)
- dm_match = dm_re.exec(data)
- if (cmd_match) {
- var command = cmd_match[1];
- var args = cmd_match[2];
- execute_command(socket, command, args);
- }
- // check if it is a direct message
- else if (dm_match) {
- var recipient = dm_match[1];
- var message = dm_match[2];
- send_private_message(socket, recipient, message);
- }
- // if none of the above, send to all
- else {
- send_to_all(data, socket);
- };
- });
- socket.on(‘close’, function() {
- sockets.splice(sockets.indexOf(socket), 1);
- delete_user(socket);
- });
- });
- s.listen(8001);
#p#
稍微高級一點(diǎn)的主題:聊天服務(wù)器的負(fù)載平衡
通常,負(fù)載按比例增長也是部署到云環(huán)境的理由之一。這種部署需要實(shí)現(xiàn)一些負(fù)載平衡機(jī)制。
大多數(shù)輕量級 Web 服務(wù)器,比如 nginx 和 lighttpd,都能夠針對多臺 HTTP 服務(wù)器進(jìn)行負(fù)載平衡,但如果您想要在非 HTTP 服務(wù)器之間實(shí)現(xiàn)平衡,nginx 可能無法滿足要求。而且盡管存在通用的 TCP 負(fù)載平衡器,您可能不會喜歡它們使用的負(fù)載平衡算法。或者它們沒有提供您想要使用的一些功能。或者,您只是想享受構(gòu)造自己的負(fù)載平衡器的樂趣。
下面是最簡單的負(fù)載平衡器。它沒有實(shí)現(xiàn)任何故障恢復(fù),希望所有的目的地都是可用的,而且沒有進(jìn)行任何錯誤處理。它十分簡約。基本的理念是,它接收一個來自客戶端的套接字連接,隨機(jī)挑選一個目標(biāo)服務(wù)器進(jìn)行連接,然后將來自客戶端的所有數(shù)據(jù)轉(zhuǎn)發(fā)給該服務(wù)器,并將來自該服務(wù)器的所有數(shù)據(jù)都發(fā)回到客戶端。
- net = require(‘net’);
- var destinations = [
- ['localhost', 8001],
- ['localhost', 8002],
- ['localhost', 8003],
- ]
- var s = net.Server(function(client_socket) {
- var i = Math.floor(Math.random() * destinations.length);
- console.log(“connecting to ” + destinations[i].toString() + “\n”);
- var dest_socket = net.Socket();
- dest_socket.connect(destinations[i][1], destinations[i][0]);
- dest_socket.on(‘data’, function(d) {
- client_socket.write(d);
- });
- client_socket.on(‘data’, function(d) {
- dest_socket.write(d);
- });
- });
- s.listen(9001);
destinations 的定義是我們要進(jìn)行平衡的后端服務(wù)器的配置。這是一個簡單的多維數(shù)組,主機(jī)名是***個元素,端口號是第二個元素。
Server() 的定義類似于聊天服務(wù)器的例子。您創(chuàng)建一個套接字服務(wù)器,并讓它監(jiān)聽一個端口。這次它將監(jiān)聽 9001 端口。
針對 Server() 定義的回調(diào)首先隨機(jī)選擇一個要連接到的目的地:
- var i = Math.floor(Math.random() *
- destinations.length);
您可能已經(jīng)使用過輪詢算法或使用“最少連接數(shù)“算法完成一些額外的工作然后離去,但我們想盡可能地保持簡單。
這個例子中有兩個指定的套接字對象: client_socket 和 dest_socket。
◆ client_socket 是負(fù)載平衡器與客戶端之間的連接。
◆ dest_socket 是負(fù)載平衡器與被平衡服務(wù)器之間的連接。
這兩個套接字分別處理一個事件:接收到的數(shù)據(jù)。當(dāng)它們其中一個收到數(shù)據(jù)時,就會將數(shù)據(jù)寫到另一個套接字。
讓我們完整地了解當(dāng)一個客戶端通過負(fù)載平衡器連接到通用網(wǎng)絡(luò)服務(wù)器上,發(fā)送數(shù)據(jù),然后接收數(shù)據(jù)時發(fā)生的事情。
當(dāng)一個客戶的連接到負(fù)載平衡器時,Node.js 在客戶端與自己本身之間創(chuàng)建一個套接字,我們稱之為 client_socket。
當(dāng)連接建立之后,負(fù)載平衡器挑選一個目的地并創(chuàng)建一個指向該目的地的套接字連接,我們稱之為 dest_socket。
當(dāng)客戶端發(fā)送數(shù)據(jù)時,負(fù)載平衡器將相同的數(shù)據(jù)推送到目的地服務(wù)器。
當(dāng)目的地服務(wù)器做出響應(yīng)并將一些數(shù)據(jù)寫到 dest_socket 時,負(fù)載平衡器通過client_socket 將這些數(shù)據(jù)推送回客戶端。
可以對這個負(fù)載平衡器進(jìn)行一些改進(jìn),包括錯誤處理,在同一個進(jìn)程中嵌入另一個進(jìn)程以動態(tài)增加和移除目的地,增加不同的平衡算法,以及增加一些容錯處理。
超越原生解決方案:Express Web 框架
Node.js 配備有 HTTP 服務(wù)器功能,但較為低級。如果要在 Node.js 中構(gòu)建一個 Web 應(yīng)用程序,您可能會考慮 Express——一個為 Node.js 打造的 Web 應(yīng)用程序開發(fā)框架。它彌補(bǔ)了 Node.js 的一些不足。
在下一個例子中,讓我們重點(diǎn)關(guān)注使用 Express 勝過簡單的 Node.js 的一些明顯優(yōu)勢。請求路由就是其中之一,還有一個是為 HTTP “verb” 類型注冊一個事件,比如“get”或“post”。
下面給出了一個十分簡單的 Web 應(yīng)用程序,它只是演示了 Express 的一些基本功能。
- ar app = require(‘express’).createServer();
- app.get(‘/’, function(req, res){
- res.send(‘This is the root.’);
- });
- app.get(‘/root/:id’, function(req, res){
- res.send(‘You sent ‘ + req.params.id + ‘ as an id’);
- });
- app.listen(7000);
這兩行以 app.get() 開始的代碼是事件處理器,當(dāng) GET 請求進(jìn)入時就會觸發(fā)。這兩次方法調(diào)用的***個參數(shù)是一個正則表達(dá)式,用于指定用戶可能傳入的 URL。第二個參數(shù)是真正處理請求的一個函數(shù)。
正則表達(dá)式參數(shù)是路由機(jī)制。如果請求類型(GET、POST等)與資源(/, /root/123)匹配,就會調(diào)用處理器函數(shù)。在***次app.get() 調(diào)用中,/ 被簡單地指定為資源。而在第二次調(diào)用中,在指定/root 時后面還加了一個 ID。映射 regex 的 URL 中資源前面的冒號(:) 字符表明,這部分稍后可作為一個參數(shù)使用。
當(dāng)請求類型與正規(guī)表達(dá)式匹配時,就會調(diào)用處理器函數(shù)。此函數(shù)帶有兩個參數(shù),一個請求(req)和一個響應(yīng)(res)。前面提到的參數(shù)被附加給請求對象。而 Web 服務(wù)器傳回給用戶的消息被傳入到響應(yīng)對象。
這是一個非常簡單的例子,但已經(jīng)清楚地說明“真正的應(yīng)用程序“如何利用這個框架來構(gòu)建更加豐富和完整的功能。如果插入一個模板系統(tǒng)和一些數(shù)據(jù)引擎(傳統(tǒng)的或 NoSQL 均可),您可以輕松構(gòu)建出一組功能來滿足真正應(yīng)用程序的需求。
Express 的特點(diǎn)之一是高性能。這與其他快速 Web 應(yīng)用程序框架的常見特性一起,讓Express 在注重高性能和海量可伸縮性的云部署領(lǐng)域中占據(jù)了重要的位置。
應(yīng)了解的知識
有兩個概念/趨勢需要了解:
◆ 鍵/值數(shù)據(jù)庫的突然流行。
◆ 其他異步的 Web 范型。
鍵/值數(shù)據(jù)庫… 為什么突然流行?
因?yàn)?JavaScript 是 Web 的通用語言,對于 JavaScript Object Notation (JSON) 的討論通常遠(yuǎn)遠(yuǎn)落后于 JavaScript 相關(guān)的研究。 JSON 是在 JavaScript 與一些其他語言之間交換數(shù)據(jù)的最常用途徑。JSON 本質(zhì)上是一種鍵/值存儲,因此天生適用于對鍵/值數(shù)據(jù)庫感興趣的JavaScript 和 Node.js 開發(fā)人員。畢竟,如果能夠以 JSON 格式存儲數(shù)據(jù),JavaScript 開發(fā)人員的工作就將變得輕松很多。
有一個不太相關(guān)的趨勢,在 NoSQL 數(shù)據(jù)庫環(huán)境中也會涉及鍵/值數(shù)據(jù)庫。CAP 定理(也叫做 Brewer 定理)指出,一個分布式系統(tǒng)有 3 個核心屬性:一致性、可用性和分區(qū)容忍性(formal proof of CAP)。這條定理是 NoSQL 發(fā)展背后的推動力量,它為犧牲傳統(tǒng)關(guān)系數(shù)據(jù)庫的某些特性以換取(通常是高可用性)提供了理論基礎(chǔ)。一些流行的鍵/值數(shù)據(jù)庫包括Riak、Cassandra、CouchDB 和 MongoDB。
異步 Web 范型
事件驅(qū)動的異步 Web 框架已經(jīng)存在了相當(dāng)長一段時間。其中***和***的異步 Web 框架是 Tornado,它使用 Python 語言編寫,在 Facebook 內(nèi)部使用。下面這個例子說明了hello_world 在 Tornado 中(在下載示例文件中叫做 hello_tornado.py )是什么樣子。
- import tornado.ioloop
- import tornado.web
- class MainHandler(tornado.web.RequestHandler):
- def get(self):
- self.write(“Hello, world”)
- application = tornado.web.Application([
- (r"/", MainHandler),
- ])
- if __name__ == “__main__”:
- application.listen(8888)
- tornado.ioloop.IOLoop.instance().start()
Twisted.web 也是用 Python 語言寫的,工作方式也十分類似。
***談到真正的 Web 服務(wù)器本身,與 Apache 不同,nginx 不使用線程,而是使用一種事件驅(qū)動的(異步)架構(gòu)來處理請求。異步 Web 框架使用 nginx 作為其 Web 服務(wù)器是十分常見的情況。
結(jié)束語
Node.js 在 Web 開發(fā)人員中非常引人關(guān)注。它允許開發(fā)團(tuán)隊(duì)同時在客戶端和服務(wù)器端上編寫 JavaScript。它們還可以結(jié)合與 JavaScript 相關(guān)的強(qiáng)大技術(shù):JQuery、V8、JSON 和事件驅(qū)動的編程。另外還有基于 Node.js 開發(fā)的生態(tài)系統(tǒng),比如 Express Web 框架。
Node.js 的優(yōu)點(diǎn)引人關(guān)注,它也存在一些缺點(diǎn)。如果是 CPU 密集型編程,就無法體現(xiàn)Node.js 提供的非阻塞 I/O 方面的優(yōu)點(diǎn)。有些架構(gòu)可以解決這類問題,比如將一個池中的進(jìn)程分流到每個 Node.js 實(shí)例上運(yùn)行,但需要由開發(fā)人員去實(shí)現(xiàn)它。
原文:http://www.cssor.com/use-node-js-for-cloud-stack.html
【編輯推薦】