2012年9月11日 星期二

聊天廣播 - iOS 與 HTML 利用 Socket.io

在日前某篇的文章中,筆者教大家如何建立一個 Node.js 寫的 Server 利用 socket.io 這個 module 讓不同的 Browser 可以即時聊天。這篇是介紹大家怎麼在 iOS 的環境下,也利用 socket.io 讓手機可以和 Browser 一起聊天。這篇文章,我們利用到幾個 OpenSource 的套件,分別是 socket.io-objcSocketRocket,這兩個,其中 socket.io-objc 是主要我們直接使用的套件,其利用 SocketRocket 然後包成一個很好用的介面讓我們使用,也是 Socket.IO 在 GitHub Wiki 提到 Objective-C 語言的實作範例。而這次要用 JSON 的格式來傳遞資料,所以我們要來修改一下前篇提到的 chatServer.js 和 chat.html。chatServer.js 更改如下
 
var app = require('http').createServer(handler) 
 , io = require('socket.io').listen(app)
 , fs = require('fs');
app.listen(8124);

function handler (req, res) { 
    fs.readFile(__dirname + '/chat.html', function (err, data) {
 if (err) { res.writeHead(500);
 return res.end('Error loading chat.html'); 
    }
    res.writeHead(200);
    res.end(data); });
}
io.sockets.on('connection', function (socket) {
    socket.on('addme',function(jsonMessage) {
 socket.username = jsonMessage.name;
 var toClient = { sender : "SERVER", message : "Good to see your "+ jsonMessage.name};
 socket.emit('chat', toClient);
 toClient.message =  jsonMessage.name + " is on the Desk";
 socket.broadcast.emit('chat', toClient);
    });
    socket.on('sendchat', function(data) { 
 io.sockets.emit('chat', { sender : socket.username, message : data.message});
    });

    socket.on('disconnect', function() {
 var bye = { sender : "SERVER", message : socket.username+"has left the building"};
 io.sockets.emit('chat', bye);
    });
});
 
所有的訊息資料的傳遞都用 JSON 格式,和之前不一樣的地方特別提出來說明一下。先看 addme 這個事件
socket.on('addme',function(jsonMessage) {
 socket.username = jsonMessage.name;
 var toClient = { sender : "SERVER", message : "Good to see your "+ jsonMessage.name};
 socket.emit('chat', toClient);
 toClient.message =  jsonMessage.name + " is on the Desk";
 socket.broadcast.emit('chat', toClient);
    });
除了 event 的名稱外,只接收一個參數,jsonMessage,因為 JSON 格式本身就可以帶很多資訊,所以 function 不需要多個參數。接著把要傳給 client 的訊息,裡面包含 sender 和 message 的訊息,一樣用 JSON 包起來存在 toClient,然後先 emit 給這個 socket 的 client,再更改 toClient 的 message,然後 broadcast 給所有的 client。
再來看到 sendchat 事件
socket.on('sendchat', function(data) { 
 io.sockets.emit('chat', { sender : socket.username, message : data.message});
    });
聽到 sendchat 事件後,就把 sender 和 message 的訊息用 chat 這個事件傳給所有的 client。 最後看一下 disconnect 事件
socket.on('disconnect', function() {
 var bye = { sender : "SERVER", message : socket.username+"has left the building"};
 io.sockets.emit('chat', bye);
    });
都是包成 JOSN 包含 sender 和 message。如此 Server 就修改完了,接下來看看 client,chat.html

<html lang="en"> 
<head>
<meta charset="utf-8">
<title>Chat Room</title> 
<script src="http://localhost:8124/socket.io/socket.io.js"></script>
<script>
var socket = io.connect('http://localhost:8124'); 
socket.on('connect', function() {
    socket.emit('addme',{name : prompt('Who are you?')});
});
socket.on('chat',function(data) { 
    var p = document.createElement('p');
    p.innerHTML = data.sender + ': ' + data.message;
    document.getElementById('output').appendChild(p); 
});
window.addEventListener('load',function() { 
    document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', {message : "say "+text});
    }, false);
}, false);
</script>
</head>
<body>
<div id="output"></div> <div id="send">
<input type="text" id="data" size="100" /><br />
<input type="button" id="sendtext" value="Send Text" /> </div>
</body> </html> 

也都是把交換的訊息用 JSON 格式來傳遞。一個個來看一下
socket.on('connect', function() {
    socket.emit('addme',{name : prompt('Who are you?')});
});
我們可以直接把 prompt() 的結果放在 JSON 物件內,再傳出去
socket.on('chat',function(data) { 
    var p = document.createElement('p');
    p.innerHTML = data.sender + ': ' + data.message;
    document.getElementById('output').appendChild(p); 
});
收到 data 可以直接用 data.message 就把真正的訊息抓出來了。
 document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', {message : "say "+text});
    }, false);
當網頁上的 Send Text 按鈕被按下的時候,會把 Text Field 的內容,包成 JSON 格式,在這裡,Text Field 的內容存在 text 它不是 string, 在放到 JSON 之前加上 "say " 就會被轉成 string。
接下來就是 iOS Client 的部分了,首先下載這兩個 Proejct,socket.io-objc SocketRocket 下載完了,先來開啟一個 Single View 的 Project,命名為 ChatClient
並把 Class Prefix 命名為 CC。如下
接著可以把需要的 .h/.m 從,socket.io-objc SocketRocket 把它們拉到 ChatClient 這個 Project 裡如下。
 最左邊的是我們的 Project : ChatClient。右邊兩個分別是 Socket.io-objc 和 SocketRocket 的 source code。
接著我們要為 ChatClient 新增一些 Framework,這些是 SocketRocket 必要的,所以要先新增,如下陳列。
  • libicucore.dylib
  • CFNetwork.framework
  • Security.framework
  • Foundation.framework
其中 Fundation.framework 已經有了,就不用再新增,新增 Framework 可以在下圖的地方新增。
 新增三個,新增完如下。
 這樣一來就準備好了,我們先 run 一下,看看有沒有錯誤?如果成功執行也沒有任何的 Warning 或是 Error 那就可以了。如下的畫面
首先來新增一些 UI 元件,打開 Storyboard,從 Library 放上如下元件
右上是兩個 UIButton,左邊上方是一個 UITextField 下方是一個 UITextView。把 Text Field 和一個 IBOutlet 關連在一起,命名為 inputTextField。如下
接著再連結底下的 Text View。命名為 resultTextView 。如下表示
接著將 Login 的 UIButton 和 IBAction login: 連結在一起,如下

下方的 Send UIButton 則和名為 sendText: 的 UIAction 連結。如下
因為要引用 Socket.IO 我們在 CCViewController.h 加入如下程式碼。
 
#import <UIKit/UIKit.h>
#import "SocketIO.h"

@interface CCViewController : UIViewController<SocketIODelegate>
@property (weak, nonatomic) IBOutlet UITextField *inputTextField;
@property (weak, nonatomic) IBOutlet UITextView *resultTextView;

@end
 
我們 import SockeIO.h 和採用 SocketIODelegate 這個 protocol 接下來準備兩個 ivar,寫在 CCViewController.m 如下
 
#import "CCViewController.h"

@interface CCViewController (){
    NSMutableString * resultString;
    SocketIO *socketIO;
}

@end
 
其中 resultString 是用在保存從 server 來的訊息,而 socketIO 是主要的變數用來和 server 溝通。接下來就是初始化這兩個物件,如下我們寫在 CCViewController.m 的 viewDidLoad 如下
 
- (void)viewDidLoad
{
    [super viewDidLoad];
    socketIO = [[SocketIO alloc] initWithDelegate:self];
    resultString = [[NSMutableString alloc] initWithCapacity:10];
    // Do any additional setup after loading the view, typically from a nib.
}
 
主要就是 socketIO 初始化就把 delegate 物件設定好。socketIO 在這個例子會用到的 delegate method 如下列出。
 
- (void) socketIO:(SocketIO *)socket didReceiveEvent:(SocketIOPacket *)packet;
- (void) socketIODidConnect:(SocketIO *)socket;
- (void) socketIODidDisconnect:(SocketIO *)socket;
 
之後我們會用到。 接著就是第一個動作,筆者寫在 login: 這個 method,按下之後先跳出一個 UIAlertView 讓使用者輸入名稱,然後按下確定後,才真正傳訊息給 server,如下表示
 
- (IBAction)login:(id)sender {
    UIAlertView * nameAlert = [[UIAlertView alloc] initWithTitle:@"Welcome to ChatRoom" 
 message:@"Who are you" delegate:self cancelButtonTitle:nil 
otherButtonTitles:@"OK", nil];

    nameAlert.alertViewStyle = UIAlertViewStylePlainTextInput;
    [nameAlert show];
}
-(void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    NSString * inputName = [alertView textFieldAtIndex:0].text;
    [socketIO connectToHost:@"localhost" onPort:8124];
    NSDictionary * hello = [NSDictionary dictionaryWithObjectsAndKeys:inputName,@"name", nil];
    [socketIO sendEvent:@"addme" withData:hello ];
}
 
這個 UIAlertView 是一個 UIAlertViewStylePlainTextInput,當使用者按下 Alert View 的確定後,會先 connect 到 server, 再準備一個 hello dictionary 當資料,key 為 name,然後用 sendEvent:withData 把 event 名稱寫成 addme 再把 hello 傳給 server。然後我們準備接收從 server 來的訊息需要 implement delegate method。如下
 
-(void) updateResult{
    resultTextView.text = resultString;
}

#pragma SocketIO Delegate
- (void) socketIO:(SocketIO *)socket didReceiveEvent:(SocketIOPacket *)packet{
    if ([packet.name isEqualToString:@"chat"]) {
        NSDictionary * stringData = (NSDictionary *) [NSJSONSerialization JSONObjectWithData:
[packet.data dataUsingEncoding:NSUTF8StringEncoding] options:
NSJSONWritingPrettyPrinted error:NULL];        
        NSDictionary * messageData = [[stringData objectForKey:@"args"] objectAtIndex:0];
        [resultString appendFormat:@"%@ say %@\n",
 [messageData objectForKey:@"sender"],[messageData objectForKey:@"message" ]] ;
    }
    [self updateResult];
}
 
我們必需要 implement socketIO:didReceiveEvent: 才會收到 server 來的資料,在這個 method 裡,先用 data.name 來得知 event 的名稱,再判斷是不是 chat, 由 packet.data 包起來的訊息有點複雜我們來看其原本的樣子。如下

{"name":"chat","args":[{"sender":"SERVER","message":"Good to see your iOS"}]}
接著來解釋一下 didReceiveEevent中的幾個動作
 NSDictionary * stringData = (NSDictionary *) [NSJSONSerialization JSONObjectWithData:
[packet.data dataUsingEncoding:NSUTF8StringEncoding] options:
NSJSONWritingPrettyPrinted error:NULL];   
上方的程式碼是把 packet.data 轉成 NSData 再轉成 JSONObject 最後轉成 NSDictionary * stringData 的方法,為什麼這麼麻煩是 packet.data 是一個由 JSON 轉成的 string,我們將之轉成 dictionary 會比較好取得其內容。
 NSDictionary * messageData = [[stringData objectForKey:@"args"] objectAtIndex:0];
        [resultString appendFormat:@"%@ say %@\n",
 [messageData objectForKey:@"sender"],[messageData objectForKey:@"message" ]] ;
上方的程式碼是把 packet.data 中 args 這個 key 的值取出來,再把其中的 message key的值取出來接在 resultString 的最後面。最後會用到 [self updateResult]; 這個 method 也只是把 resultString 放到 resultTextView.text 變數裡。
如此一來就可以先打開 server ,用 node chatServer.js 執行後,就可以執行 iOS ,正常的情況下就會出現如下畫面。
先輸入名稱

看到 Server 來的訊息
這樣協定的部分差不多是可以了,再來就是從 iOS 發出訊息的部分,我們寫在 CCViewController.m 如下

- (IBAction)sendText:(id)sender {
    NSDictionary * chat = [NSDictionary dictionaryWithObjectsAndKeys:inputTextField.text,@"message", nil];
    [socketIO sendEvent:@"sendchat" withData:chat];
    [inputTextField resignFirstResponder];
}
單純地把 inputTextField.text 包在 Dictionary 裡面,用 message 當 key 就送給 server。簡單地動作,重新執行後,這樣一來就可以和 Brwoser 溝通了,如下畫面
 當然看著做一定會順暢,筆者還是建議把兩個 delegate method 補上,這樣可以知道那邊出了問題。如下

- (void) socketIODidConnect:(SocketIO *)socket{
    NSLog(@"did connect to %@", socket);
}
- (void) socketIODidDisconnect:(SocketIO *)socket{
    NSLog(@"did disconnect to %@", socket);
}
一樣,程式碼都放在 GitHub,包含了 Server,Client,iOS。Good to see you again, Bye.

2012年9月10日 星期一

聊天廣播 - Socket.io

關於網路的架構,Server-Client 這樣的結合己經延用己久,相信大家會了解 HTTP 的協定是 stateless,一但 client 和 server 通訊完,server 就不知道 client 跑去那了?如果 server 有更新想要 Push 給 client,就要反過來從 Client 定期的去向 server 要求。今天要來使用 socket.io 這個 module 就是可以達到 server 和 client 的雙向溝通,只要一建立連線 server 可以一直傳訊息給 client,反過來也行,直接某一方斷線為止。先姑且不論 socket.io 底層是什麼,我們就用一個聊天廣播室來建立這個服務。client 端我們用 HTML 來實現,建立如下的 HTML,命名為 chat.html。
 
<html lang="en"> 
  <head>
  <meta charset="utf-8">
  <title>Chat Room</title> 
  </head>
  <body>
    <div id="output">
    </div>
    <div id="send">
    <input type="text" id="data" size="100" /><br />
    <input type="button" id="sendtext" value="Send Text" />
    </div>
  </body>
</html>
 
這個 HTML 在 Browser 執行會是如下畫面
有一個 Text Field 可以輸入文字然後有一個按鈕
接著加上一些 javascript,在 <head> </head> 中間。如下
 
<head>
<meta charset="utf-8">
<title>Chat Room</title> 
<script src="http://localhost:8124/socket.io/socket.io.js"></script> 
<script>
var socket = io.connect('http://localhost:8124'); 
socket.on('connect', function() {
    socket.emit('addme', prompt('Who are you?')); 
});
socket.on('chat',function(username, data) { 
    var p = document.createElement('p'); 
    p.innerHTML = username + ': ' + data;
    document.getElementById('output').appendChild(p); 
});
window.addEventListener('load',function() { 
    document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', text);
    }, false); 
}, false);

</script>
</head>

一開始這個文件用了 <script src="http://localhost:8124/socket.io/socket.io.js"> 因為要用到 socket.io 的功能,所以要指向 server 的 socket.io.js ,在 chat.html 使用 。
我們一一來看一下。
var socket = io.connect('http://localhost:8124'); 
socket.on('connect', function() {
    socket.emit('addme', prompt('Who are you?')); 
});
這個是要用 io 去連結 server,然後就等待, connect 這個事件,也就是和 server 連上線後,就會觸發。觸發的動作就是發送一個訊息給 server 事件名為 addme,內容為 prompt('Who are you?')。prompt() 的用途是會跳出一個需要輸入的視窗,然後把使用者在這個視窗輸入的值,再經由 socket.emit 傳給 server。接著再來看一下,下一個 socket 事件
socket.on('chat',function(username, data) { 
    var p = document.createElement('p'); 
    p.innerHTML = username + ': ' + data;
    document.getElementById('output').appendChild(p); 
});
在 socket.on 等待 chat 事件,然後 server 會傳兩個值過來分別存在 username 和 data 這兩個變數中,接著更新網頁內容在 <output> 裡多加一個 <p> 內容就是某個 user 打了什麼字,這樣。
在 Client 端最後,我們看到
window.addEventListener('load',function() { 
    document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', text);
    }, false); 
}, false);
這個是新增按鈕按下去的觸發動作,主要就是把 TextField 中使用者輸入的文字,抓出來
var text = document.getElementById('data').value; 
然後再利用 socekt.emit 送給 server。這樣 Client 端就完成了。 Server 端也非常之簡單,如下就是全部了,存成 chatServer.js。

var app = require('http').createServer(handler) 
 , io = require('socket.io').listen(app)
 , fs = require('fs');
app.listen(8124);

function handler (req, res) { 
 fs.readFile(__dirname + '/chat.html', function (err, data) {
     if (err) { 
                res.writeHead(500);
         return res.end('Error loading chat.html'); 
            }
     res.writeHead(200);
     res.end(data); 
        });
}
io.sockets.on('connection', function (socket) {
 socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 });
 socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });

 socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
});
一開始需要用到 http,socket.io,fs 等 module 就引用就好了。然後來看一下 handler 這個 function 被用在一開始的 require('http').createServer(handler) 。
function handler (req, res) { 
 fs.readFile(__dirname + '/chat.html', function (err, data) {
     if (err) { 
                res.writeHead(500);
         return res.end('Error loading chat.html'); 
            }
     res.writeHead(200);
     res.end(data); 
        });
}
這個 funciton 主要是說不管 client 端在 Browser 輸入什麼路徑,都是執行這個 function ,無論是輸入
http://127.0.0.1:8124/ashdf;asdlf/has;dfasdf
或是
http://127.0.0.1:8124/
結果都是一樣的,就是把同一個目錄下的 chat.html 讀出來然後送給 Browser, 也就是說,現在 Browser 看到的就是一開始我們寫的 chat.html ,一個 Text Field 和一個按鈕的樣式。
io.sockets.on('connection', function (socket) {
 socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 });
 socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });

 socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
});
接著看到一滿大的 function,就是 io.socket.on 等待 connection 事件, 然後會得到一個 socket 也就是和一個 client 連上線。要注意的是這個 socket 會因為不同的 client 而有不同的記憶體 接著看裡面一點
socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 }); 
然後這個 socket 會等待 addme 這個事件,會得到一個 username 再存到 socket.name 保存下來。 然後用 socket.emit 回給連上來的 client,再用 socket.broadcast.emit 廣播給所有在線上其他的client。接著再往下看。
socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });
這個 socket 等待 sendchat 的事件,然後收到資料後用 io.sockets.emit 廣播給所有線上的 client。說到這裡,我們來看這三個相似的 function
  • socket.emit - 對於一個特定的 socket 傳訊息
  • socket.broadcast.emit - 對於除了目前這個 socket 之外所有線上的 socket 傳訊息
  • io.sockets.emit - 對於所有線上 socket 傳訊息
然後就是最後一個 function 了
socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
也就是 socket 等待 disconnect 之後,就廣播給其他 client 說某 socket 離線了。 先執行 node chatServer.js
測試的方法就是用兩個不同的 Browser 試試連看看,這樣就可以互相聊天了。
 原始碼都放在 GitHub
補充說明一下,在這個例子我們都使用 socket.emit() 來傳送訊息,也許讀者有參考到其他教學有用到 socket.send() 這個 function,我們用一個例子來解釋
socket.send("Hi") - 相當於 socket.emit("message", "Hi");
也就是說,socket.send("Hi") 省略了 socket.emit("message", "Hi"); 前面的第一個 "message",而接受的一端就是用

socket.on('message', function (message) {
  console.log(message);
});
來接收 socket.send("Hi") 的訊息。

2012年9月8日 星期六

NoSQL Database - MongoDB & Node.js

這篇文章主要是介紹 MongoDB 這個資料庫,也會用 Node.js 與其相連,不過在這之前,先來解說一下一些名詞,首先就是 NoSQL,從 Wikipedia 看來就是不用 SQL 當做查詢的語言,換句話說,如果用 MongoDB 就不用學 SQL 了,呵。看來不錯,再來看看 MongoDB 有什麼特色,MongoDB 是一種文件導向的資料庫,什麼文件?長什麼樣子?舉一個例子來說
{
    name : "Michael"
    gender : "Male"
}
長得和 JSON 好像啊,嗯啊,可以直接把 JSON 丟到 MongoDB 裡,這個就是文件,還可以很複雜如下
 {
     "query": "Pizza",
     "locations": [
         1210054,
         1201167
     ]
 }
就是一個 JSON 所以用 Javascript 存取 MongoDB 非常直覺。OK 讓我們來安裝一下 MongoDB
安裝有幾種方式比如有 Homebrew 直接用
brew install mongodb
或者到這安裝執行檔,有幾個作業系統可以選如下
 安裝完之後在 Terminal 可以輸入以下指令,來確定安裝成功。
mongod --version
可以看到如下畫面
 這樣就是成功安裝了。好吧,我們開啟這個 MongoDB 服務,很簡單,直接輸入 mongod 這樣就可以了
mongod
不過,讀者有可能會看到以下的畫面,代表沒有開啟服務成功
 紅色框起來的部分,寫著
MongoDB starting : pid=4116 port=27017 dbpath=/data/db/ 64-bit host=chronoers-MacBook-Air.local
MongoDB 想要在 port 27017 開啟,而且把資料放在 /data/db/ 這個資料夾下,然後就是shutdown 了,很有可能就是 port 27017 被別人用了,或是 dbpath 這個目錄不存在,一開始沒有別的服務的話,dbpath 的這個目錄不存在的可能性很高,這篇文章的做法就是先在當下目錄下建立一個新的目錄名為 dbs,如下指令
mkdir dbs
然後再啟動 MongoDB ,這次要改變一些參數,如下
mongod --dbpath=dbs
這樣一來 mongod 就會用當前的目錄下的 dbs (剛剛我們建立的),來存放資料,執行成功後會看到如下畫面
最後寫 waiting for connections on port 27017 然後整個游標停在那,這樣就表示正常啟動了。
我們先用 Terminal 來新增一些資料到 dbs 裡面,在先前有提過 document 的觀念,基本上可以視為一個 JSON 物件。接者要來說另一個名詞叫 collection,很多筆 document 就組合成一個 collection ,所以我們要新增一筆 document 之前要先建立一個  document ,這是 MongoDB 的架構,OK。
怎麼新增呢?我們在安裝完成後,除了有 mongod 這個指令外,還有另一個叫做 mongo 要注意,只差一個 d,符號,mongo 就是要和 mongod 來溝通的指令,首先先來看一下版本
直接執行
mongo
會看到如下畫面
 游標停在 > 之後,代表可以直接輸入指令,控制 mongod,如下輸入
show dbs
代表把所有的 database 呈現在 Terminal 上,應該會看到如下


local (empty) 就是有一個資料庫名為 local 但是沒有資料在裡面。我們就來新增一個資料庫名為 mydb,如下輸入
use mydb
會看到
雖然沒有明確看到建立的動作,  因為 mydb 裡面如果沒有 document 或 collection 是沒有意義的,暫時這樣,先來看一下這個 mydb 一些基本內容
db.stats()
這樣會看到
還記得 collection 和 document 嗎?我們就來建立一下。輸入
db.users.insert({name : "Michael", gender : "Male"})
如果沒有發生錯誤,就會看到如下
users 這個就是 collection  而 {name : "Michael", gender : "Male"} 就是 document ,insert 就是寫入的動作,就這樣很直接的寫就可以新增資料進入 mydb 了,我們用
show collectons 就可以看到如下,有出現  users

users 真得有寫入 mydb 接下來看 users 這個 collection 裡面的內容,使用如下指令
db.users.find()
會看到如下
真得有一筆資料,接下來我們做個實驗
再輸入一次
db.users.insert({name : "Michael", gender : "Male"})
再用
db.users.find()
會發生什麼事?

兩筆我們認為一樣的資料寫入了,MongoDB 不會因為 name, gender 一樣的值就自行判斷不加入哦,對 MongoDB 而言就是兩筆資料,而兩筆的 _id 是不一樣的,那接下來把其中一個刪除吧。用下列指令
db.users.remove({"_id" : ObjectId("5048d6b704f6c8a66750e12b")})
因為 _id 才是可以辦識不一樣的key 所以要輸入 _id 才可以明確指出要 remove 那一筆 document 。接下來我們來更改一下內容,用 update,如下
db.users.update({gender : "Male"}, { $set :{ name : "James" }})
再用
db.users.find()
把結束呈現出來會看到下方結果

 name 改成了 James 了,我們來看一下 update 這個 function 有兩個參數,第一個參數是要尋找的條件,我們設定為 {gender : "Male"} 目前也只有一個 document 符合,接下來第二個參數是
{ $set : { name : "James"}}
 前面要加上 $set 會依照原本的值 {name : "Michael", gender : "Male"} 做為改變的依據,把 name 這個值改成 James,如果沒有加上 $set 比如說
db.users.update({gender : "Male"}, { name : "James" })
則會把原本的 {name : "Michael", gender : "Male"} 用 { name : "James" } 取代,也就是說  { gender : "Male "} 並不會被保留。
好我們介紹了 insert, find, update, remove 這幾個操作就是有名的 CRUD - (Create,Read,Update,Delete),接下來我們來看一下怎麼從 Node.js 來存取 MongoDB 的資料。
首先把上面的 MongoDB 停止,需要輸入  Ctrl + c。然後再另一個資料夾再開啟一個新的 Server。我們就建立一個名為  mongoDemo 的資料夾,然後在 mongoDemo 之下再建立 dbs 。然後開啟 MongoDB 如下輸入
mongod --dbpath dbs
我們必需要為 Node.js 準備一個 MongoDB 的 Driver,名為 mongojs 如下安裝
npm install -g mongojs
也許有些網友比較熟 mongodb 這個官方網站介紹的 module ,不過這篇文章開頭是用 mongo 這個 interactive shell 來介紹存取 MongoDB 的而 mongojs 的寫法和 mongo 比較像,所以就先採用這個,mongojs 的官方 GItHub 在。而對官方的 mongodb 比較有興趣的讀者可以看這篇解釋
接著準備一個名為 accessMongo.js 的檔案, 先輸入如下內容。

var express = require("express");
var app = express();
app.listen(8800);
var config = {
    "hostname":"localhost",
    "port":27017,
    "db":"mydb"
}
var dbURL = "mongodb://" + config.hostname + ":" + config.port + "/" + config.db;
var collections = ["users"];
var db = require("mongojs").connect(dbURL, collections);
 一開始筆者還是使用了 express,為了方便可以讓使用者看到結果。預計用
http://127.0.0.1:8800/create
http://127.0.0.1:8800/read
http://127.0.0.1:8800/update
http://127.0.0.1:8800/remove
這四個 GET 連結來來代表對 Database 執行 CRUD 四個動作。
接著我們會看到 config 這個 JSON Object 就是關於要存取 MongoDB Database 和  Collection 的設定。最後利用 mongojs 的 connect 和 MongoDB 連上線並設定要存取的 collection 是 users。接下來寫 read 的部分。


app.get("/create",function  (req, res) {
            db.users.save({name: "Michael", gender: "Male"}, function(err, saved) {
             if( err || !saved ) console.log("User not saved");
                 else {
                          res.send("User saved");
             }
                  res.end();
            });
});
在這裡我們可以看到 db.users.save() 幾乎和 mongo 終端機的寫法一樣,第一個參數就是 document 也就是 {name: "Michael", gender: "Male"},如果 saved 是正確的,代表寫入正確再來我們來寫 read,如下。

app.get("/read",function  (req, res) {
    db.users.find({}, function(err, users) {
      if( err || !users) console.log("No  users found");
      else users.forEach( function(user) {
        res.send(JSON.stringify(user)+"\n");
      } );
      res.end();
    });
});
用 db.users.find() 第一個參數是 {} 代表任一個 document 都符合,然後第二個參數是個 function,其中 users 就是從 users.find() 找到的所有的 document 就會存在這個 users array 裡面。然後用 users.forEach() 把每個 document 送給 Browser 呈現出來。如此一來就可以先用 Browser 測試,記得要先開啟 mongod 才能執行 node accessMongo.js。
在 Browser 輸入 http://127.0.0.1:8800/create
再來測試 read http://127.0.0.1:8800/read
恭喜大家表示成功了。
再來就是 update。新增程式碼如下。

app.get("/update",function  (req, res) {
    db.users.update({name: "Michael"}, {$set: {name: "James"}}, function(err, updated) {
      if( err || !updated ) console.log("User not updated");
      else {
          res.send("User updated");
          res.end();
      }
    });
});
和 mongo 的 interactive shell 非常像用 db.users.update 然後第一,二個參數就是 {name: "Michael"}, {$set: {name: "James"}} 和 Terminal 寫法一樣。沒有錯誤的話,
輸入 http://127.0.0.1:8800/update 就會在 Browser 看到如下
表示 update 成功,然後再輸入 http://127.0.0.1:8800/read
同一個 id 名字改成了 James。
最後我們要來測試一下 Delete ,新增如下程式碼

app.get("/remove",function  (req, res) {
    db.users.remove({gender: "Male"});
    res.send("removed ");
    res.end();
});
remove 這個 function 就是把尋找的條件丟入,就可以了,如果用 {} 表示要刪除全部的 documents。在Browser 輸入 http://127.0.0.1:8800/remove
 然後用 read 確定,輸入
空空如也,就是我們要的了。好吧,今天就到此了,咱們下次見。例子一樣放在 GitHub。謝謝各位看倌們。

2012年9月7日 星期五

解析 XML - Node.js

這篇文章我們會來解析一份 HTML 文件,因為一份寫得完整的 HTML 就是一份 XML 文件,而 HTML 文件比 XML 文件取得容易,因為網頁的原始碼就是 HTML 文件。不過話說在前頭,有些寫的很差的 HTML 是沒辦法當成 XML 來解析的,雖然 Browser 看得懂,至少要是 一個  Well Formed 的 HTML。通常來說如果不是手寫的,用程式產生出來的 HTML 符合 XML Well Fromed 規則的機會是很高的。那我們就先來看一下,今天要被解析的主角
 
<html>
    <body>
        <table>
            <tr>
                <td>Apples</td>
                <td>44%</td>
            </tr>
            <tr>
                <td>Bananas</td>
                <td>23%</td>
            </tr>
            <tr>
                <td>Oranges</td>
                <td>13%</td>
            </tr>
            <tr>
                <td>Other</td>
                <td>10%</td>
            </tr>
    </body>
</html>
 
把這個檔案命名為 hello.html 存在資料夾中,這份 HTML 執行出來是這樣的結果
不過結果不重要,我們要用 Node.js 來解析它,Node.js 需要一個 module 名為 libxmljs,用 npm install 它,如下
npm install -g libxmljs
然後產生一個新的 js 檔名為 parseXML.js,內容如下
 
var libxmljs = require("libxmljs");
var fs = require('fs');
fs.readFile("hello.html", 'utf8', function(err, data) { 
  if (err) throw err;
  var xmlDoc = libxmljs.parseXmlString(data);
  // xpath queries
  var gchild = xmlDoc.find('//td');
  for (var i = 0; i < gchild.length; i++){
      console.log(gchild[i].text());
  };
});  
一開始需要 libxmljs 這個 module ,接下來還需要 fs 這個 module,然後就用 fs 的 readFile 指定第一個參數就是我們要解析的 hello.html,第二個參數是這個檔案的文字編碼為 utf8 ,第三個參數就是處理從 disk 讀到的 hello.html,放在 data 這個變數裡。
data 被 parseXMLString 這個 function 解析完成之後會把整個 XML 文件架構放在 xmlDoc裡,需要去尋找某一個 tag 的時候,利用 find() 這個 function,裡面的參數是用 XPath 來描述的,在這個例子是 //td,代表的意思就是整份文件裡的 td tag,然後把結果放到 gchild 這個變數。 find() 這個 function 會回傳一個 array 筆者就用 for-loop 把它的結果用 console.log() show
在 Terminal 上面,會看到
Apples
44%
Bananas
23%
Oranges
13%
Other
10%
的結果,是這樣沒錯,寫到這讀者應該會查覺,要找到任一個 tag 就要用 XPath 去描述,有關 XPath 的說明可以參考這邊。接下來我們再試試一個例子。
需求是這樣的。想要找到包含 Apple 這個 tag 之後的第一個 tag 的值。在我們的例子就是想要找到下圖框起來的部分。
我們要這樣想,先找到一個 tag 的值是 Apple ,然後再去找這個 tag 的下一個 tag 的值。這樣的 XPath 寫法會是。
var xpath = '//tr[td = "Apples"]';
 這個寫法的意思是先找到 <td> 的 text 中為 "Apples" 的 <tr>,然後再用 XML Parser 執行,
var tr = xmlDoc.get(xpath);
然後我們用 console.log() 印出來。

 console.log("Up node is : " + tr.text());
 
 console.log("Child nodes : "+tr.childNodes());

這樣會在 Terminal 看到

Up node is : Apples44%
           
Child nodes : <td>Apples</td>,<td>44%</td>,
第一個是用 text(),直接印出,第二個是用 childNodes 其輸出是個 array ,然後被 Node.js 當成 string 就會看到
<td>Apples</td>,<td>44%</td>,
的結果,接著我們可再從 tr.childNodes() 找出我們要的 tag,Apples 是第一個,那下一個就是第二個,就是我們要的,就用
console.log("Next nodes : "+tr.childNodes()[1].text());
就會看到
Next nodes : 44%
不過這個前提是 XML 檔或是 HTML 檔中 tag 和 tag 中沒有其他的文字,比如上方一開始的 HTML 要改成如下,才會解析成功。
<html>
    <body>
        <table>
            <tr><td>Apples</td><td>44%</td>
            </tr>
            <tr>
                <td>Bananas</td>
                <td>23%</td>
            </tr>
            <tr>
                <td>Oranges</td>
                <td>13%</td>
            </tr>
            <tr>
                <td>Other</td>
                <td>10%</td>
            </tr>
        </table>
    </body>
</html>
筆者刻意把看到第一個 <tr> 和 <td> 中間沒有任何的空間,也許這樣對人眼是不好看的,但是它還是一個合法的 XML 而且更適合解析。
今天解析 XML 就到這,咱們下次見,檔案一樣放在 GitHub

2012年9月2日 星期日

Hosting Service - Node.js

寫 Web 的服務,筆者是覺得總會經過利用別人的 Hosting Service 這個過程。Web Service 最麻煩的就是管理和擴增服務,有許多流量和後台的程式要處理,對Node.js 來說其標榜就是可以很簡單地分散式處理,而管理的部分交由第三方的平台來管理對中小型服務來說是一個非常好的起點。
今天要介紹一個不錯的,Node.js Hosting 的服務,名為 Nodejitsu。這個 Repository 好用的地方是設定簡單,支援的 Module 也不少,對於 iOS Developer 來說,最重要的 Server 服務之一就是 APNS 了,筆者測試過,是可以成功執行沒有擋掉 APNS 需要的 Port。
好那我們就登入 Nodejitsu 的首面看到下面的頁面,Try Nodejitsu for free.
接下來會有幾個步驟需要填寫,如下,填寫 useranme 只允許字母數字和-

寫 e-mail address
填寫你想要建立的 Node.j 服務的類型和任何其他在使用的技術,也可以不寫。
好了之後應該會在 mail 收到 Nodejitsu 寄來的信,類似下方。
信用還提到第一次啟用帳號要做的事情如下

在終端機執行下列指令,
  1. sudo npm install jitsu -g
  2. jitsu users confirm <帳號> <認證碼>
這樣就可以了,實際情況如下,步驟 2,執行後會問一些問題,第一個是設定密碼
設定完之後,再輸入一次,就啟用完成,如下
接下我們就要放一個簡單的 web server 到 Nodejitsu 上面去。
先用 mkdir ( make directory ) 在任一目錄下建立一個新的目錄如下
mkdir myJitsu
 然後進入用 cd myJitsu 如下
我們新增 Node.js 學習資源這篇提到的 hello.js ,到這個資料夾下,也取名為 hello.js
var http = require('http');
 
http.createServer(function (request, response) {
    response.writeHead(200, {'Content-Type': 'text/plain'});
    response.end('Hello World\n');
}).listen(8000);
然後執行佈署的指令,如下
jitsu deploy
接著會看到 jitsu 和 Server 溝通,會再問一些問題,第一個是 App Name: 在其後有一個 (myJitsu) 的意思是,如果不寫,就用這個名字,我們來改一下,用 MyJitsu
再來是 subdomain 的名稱,這個不改也 ok
接下來比較重要,是一開始要執行的 script,也就是 hello.js,如下設定

再來設定版本,我們設定為 0.0.1,如下
 最後是需要的 Node.js 版本,這個例子為 0.8.x
完成後,會提醒我們做確認的動作,並把剛剛的設定都寫入一個 package.json 的檔案中,
寫 yes 之後,就會看到這樣的裝態,就是完成了

上圖用白色框起來的部分,就是這個服務的網址,試著在Browser 的網址中輸入
http://incensome.myjitsu.jit.su/
就會看到如下
 然後我們回到 http://nodejitsu.com/ 來看一下剛剛佈署到 Nodejitsu 的服務,如下,按右上的 Login。
輸入帳號密碼之後,就會看到如下
一個上線的服務完成了,重點是右邊視窗有一個 Log 的 Tab,點選後如下
這裡可以看到在程式中 console.log 的結果,雖然目前我們並沒有用到 console.log。
接下來我們要來看一下這個檔案 package.json
{
  "name": "MyJitsu",
  "subdomain": "incensome.myJitsu",
  "scripts": {
    "start": "hello.js"
  },
  "version": "0.0.1",
  "engines": {
    "node": "0.8.x"
  }
}
這個檔案描述這個 Node.js 服務的設定,這個例子只用到 http 的 module,如果用到其他的,不是包在 Node.js 核心的東西,比如這篇提到的 Express,就會看到如下的 package.json
{
  "name": "TeachRepo",
  "subdomain": "incensome.teachrepo",
  "scripts": {
    "start": "web.js"
  },
  "version": "0.0.1-3",
  "engines": {
    "node": "0.8.x"
  },
  "dependencies": {
    "express": "*"
  }
}
jitsu 會自行去看 *.js 檔然後把 dependencies 加到 package.json 上,在上面的例子是寫,
"dependencies": {
    "express": "*"
  }
代表會用到 express 而不拘版本,(其實,3.0 和 2.0 有差別,有注意一下)。
那如果 hello.js 有更新怎麼辦?在同一個目錄下,用 jitsu deploy 就可以了,jitsu 會自動把版號加上-1,比如原本是 0.0.1 會變成 0.0.1-1。

最後我們來討論一下,Nodejitsu 這個服務,付費的時候怎麼計算?
從這個網頁我們可以看到,個人,或是小型公司來說 一個 drone 一個月只要 $3 美金,是相當便宜。如下是Individual Plan
那什麼是 Drone 呢?在這 FAQ 有提到,就是指處理 App 的能力,當然它有提到通常一個 App 就只會需要一個 drone,同理可推是一個 App。同時平行處理的話也許會需要比較多 drone, 不過 Node.js 本身就是 None-blocking,比較不需要自行再產生 Thread 之類的,但真正計算的方法,也只能靠經驗法則了。
那試用版的可以用多久呢? FAQ 有提到,現在是 3 個月,過期怎麼辦?再申請一個 e-mail 就好了 orz..
好,那咱們就下次見。