socket.io × SIOSocketでマルチルーム対応 iOSチャットアプリを作ってみた

# 経緯
[前回slackのライブラリを調べた時](http://qiita.com/mito_log/items/68da5f5be6545113adf7)にwebソケットライブラリ、[rocketsocket](https://github.com/square/SocketRocket)が入っていて、面白そうだなと思ったので、早速使ってみようかと。webソケットといえばチャット！チャットアプリを作ってみます。

まずは、そのまんまsocketrocketを見てみたんですが、理解ゼロからチャットやろうとすると、結構大変そうだな...と思い、socket.ioに[rooms](http://socket.io/docs/rooms-and-namespaces/)という機能があるのをみつけました。お、これは使えそうだと調べて行ったら、iOSでもwebViewでsocket.ioのクライアントを書いてくれている人がいました。[SIOSocket](https://github.com/MegaBits/SIOSocket)というやつです。これはいけそうだなということで、やってみました。結果、わりと大変でしたが、なんとか動くものはできたかなーとおもったのでアップしてみます。

# できたもの
なんかびみょーな仕様ですが...

 - アプリ起動時に自動的に自分のroomに入る
 - 今のところフォアグラウンドでの動作のみ（バックグラウンドでは気が向いたら...)
 - もし他の人もアプリを立ち上げていれば左側のドロワーにでてくるので、そこから選択してjoinできます。同時に複数のroomにjoinできます。

ナビゲーションバーのタイトルの部分がいわゆる部屋の名前です...便宜上socket idにしてるので、無機質極まりないです。名前とか吹き出しの下に出せればよかったです。追々やってみます。
※ ちなみにこのフキダシは [JSQMessagesViewController](https://github.com/jessesquires/JSQMessagesViewController) というものをつかっています！使い方は、[Developer.IOさんの記事](http://dev.classmethod.jp/smartphone/iphone/jsq-messages-view-controller/)をご参考に。

![iOS Simulator Screen Shot Mar 17, 2015, 07.07.15.png](https://qiita-image-store.s3.amazonaws.com/0/46489/1132e574-cd11-77db-65ee-53929c42a3ac.png)

お部屋一覧がみえます。一応ニックネームの変更もできます。
![iOS Simulator Screen Shot Mar 17, 2015, 07.05.34.png](https://qiita-image-store.s3.amazonaws.com/0/46489/1985a3b2-77bd-6fd7-176b-873752afde18.png)


# 用意したもの
###### ()の中は今回利用したversionなど
 - ec2インスタンス (Amazon Linux AMI release 2014.09)
 - node.js (v0.12.0)
 - socket.io (1.3.5)

# 構成

今回は、iOSアプリ上のSIOSocket <-> ec2上のnodeサーバ with socket.io って感じでやってみます。vagrantだと感動が多少減るので、ec2に置いてリアルインターネットで。

# 手順
## 1. nodeサーバを用意
websocket通信ではtcp3000番ポートを使うのでそこだけ開けるように注意

![Screen Shot 2015-03-06 at 16.44.45.png](https://qiita-image-store.s3.amazonaws.com/0/46489/63c1ef18-2dc1-5a23-c6be-bb86a1831008.png)

```bash
# nodebrewをインストール
wget git.io/nodebrew
perl nodebrew setup

# nodebrewのパスを通す
vim ~/.bashrc
PATH=$HOME/.nodebrew/current/bin:$PATH

# nodeの最新安定版(バイナリ)をインストール
nodebrew install-binary stable
# lsして出てきたものをuse
nodebrew ls
nodebrew use v0.12.0
# 上記versionが出てきたら成功
node -v

# 適当なディレクトリに移動
mkdir ~/SIOChatServer
cd ~/SIOChatServer

# socket.ioのインストール
npm install socket.io
```

node用サーバにscpでserver.jsを配置

```bash
scp -i path/to/your.pem server.js ec2-user@yourhostname:/home/ec2-user/SIOChatServer
```

あとは実行するだけ。のはず。

```bash
# nodeサーバ実行 (デバッグしたい場合は DEBUG*= node server 使うといいかも)
node server
```

## 2. アプリのソースを準備
[https://github.com/mitolog/SIOChat](https://github.com/mitolog/SIOChat) 
こちらにソースを上げたので、cloneして使って下さい。その際、**AppConst.h**ファイルの**kWebSocketHostName**を適宜ご自身のものに変えてください。その際、スキーマを`ws://`にしといたほうがいいと思います。あとポート指定も。

# WebSocket通信を担っている箇所

## アプリ側

WebSocketの受け側は、SIOSocketのメソッドで一発です。コールバックをblocksの中で定義してあげてます。

```objc:ChatViewController.h
- (void)connectWs
{
    // Close existing connection if needed
    if(self.socket){ [self.socket close];}
    
    [SIOSocket socketWithHost: kWebSocketHostName response: ^(SIOSocket *socket){
        
        self.socket = socket;
        
        __weak typeof(self) weakSelf = self;
        self.socket.onConnect = ^()
        {
            /* ~ここ省略~ */
            [weakSelf.socket emit: @"init" args: @[[param paramStr]]];
        };
        
        [self.socket on: @"join" callback: ^(SIOParameterArray *args)
         {
             /* ~ここ省略~ */
         }];
        
        [self.socket on: @"update" callback: ^(SIOParameterArray *args)
         {
             /* ~ここ省略~ */
         }];
        
        [self.socket on: @"rooms" callback: ^(SIOParameterArray *args)
         {
             /* ~ここ省略~ */
         }];
        
        [self.socket on: @"disappear" callback: ^(SIOParameterArray *args)
         {                  
             /* ~ここ省略~ */
         }];
    }];
}

```

他のノードに通知する場合は、

```objc:ChatViewController.h
[self.socket emit:@"post" args:@[[param paramStr]]];
```

こんなふうにかけます。

## サーバ側

これだけ！

```js:server.js
var io = require('socket.io')(3000);

var sepStr = ',';
var roomIdx = 0;
var isParentIdx = 1;
var nicknameIdx = 3;
var joinedRooms = [];
var nicknames = [];

io.on('connection', function (socket) {
	
    console.log(socket['id'] + ' has connected!');
  
    // join the specific room everytime socket connected
    socket.on('init', function(data){
        
	var params = data.split(sepStr);
        var isParent = params[isParentIdx];
 	var room = params[roomIdx]
        var output = data;	

	if(isParent == '1'){
            room = socket['id'];
	    output = room;
            output += data;
	    nicknames[socket['id']] = params[nicknameIdx];
	}else{
            socket.join(room);
	}
      	
	joinedRooms[socket['id']] = socket.rooms;
 
	io.emit('rooms', io.sockets.adapter.rooms);
	io.to(room).emit('join', output);

        console.log('joined room ' + room);
    });

    // bradcast the specific room everytime someone posted some
    socket.on('post', function (data) {
        
        console.log(socket['id'] + ' ' + data);

        var room = data.split(sepStr)[roomIdx];
        io.to(room).emit('update', data);
    });
    
    // clients are disconnected automatically everytime they going background 
    socket.on('disconnect', function (data) {
       	
	io.emit('rooms', io.sockets.adapter.rooms);
	
	var message = nicknames[socket['id']];
	message += ' left room';
	message += sepStr;
	var prevJoinedRooms = joinedRooms[socket['id']];
 	for(var i=0; i<prevJoinedRooms.length; i++){
	    var room = prevJoinedRooms[i];
	    io.to(room).emit('disappear', message + room);
	    console.log(message + room);
	}
        console.log(socket['id'] + ' has disconnected!');
    });
    
});
```


# 感想
 - アプリもサーバも、まだまだ無駄なソースあるんですが、それでも結構少ないコードですんだと思っていて、nodeすごいなーsocket.ioすごいなーと感動しました。

 - リアルタイムにチャットが飛んできた時はちょっと感動しました。

 - slackってやっぱりすげーなー...
 - [socket.io-redis](http://github.com/automattic/socket.io-redis)と[socket.io-emitter](http://github.com/automattic/socket.io-emitter)を使えばsocket.io以外のプロセスからsocket.ioにデータを送れるので既存のphpとかrailsのAPIからでも連携できるし、スケールアウトもできるのは良いのでは。

## 参考

[Socket.io](http://socket.io/)

[SIOSocket](https://github.com/mitolog/SIOChat)

[(公式ページ)Rooms and Namespaces](http://socket.io/docs/rooms-and-namespaces/)

[WebSocket 事始め by Node.js + Socket.IO](http://qiita.com/ogwmtnr/items/f88c8b5433b5469322f1)

[Node.jsにてsocket.ioのjoinでチャットルーム実装！](http://d.hatena.ne.jp/hekyou/20111222/p1)