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.

3 則留言:

  1. 很詳實的介紹教學,感謝分享!

    回覆刪除
    回覆
    1. 不客氣。Caesar,推廣 Node.js 加油 !!

      刪除
  2. 最後看到node chatServer.js 整個充滿問號

    後來去nodejs.org下載Node.js for Mac 安装包 打了sudo npm install socket.io才可以打node chatServer.js

    最後終於可以執行成功

    第一次動手做 拉線那邊我也是充滿疑問 也是google才知道怎麼拉的

    感覺還不錯!!

    回覆刪除