Socket.ioを使ったチャットルーム ロジックの実装

Node.jsの勉強がてら地図上でお話しできる[チャットアプリ](https://github.com/ynunokawa/machiawase-map-sub)を作成していたら、チャットルームの管理が面倒なので、FacebookのMessengerで管理しているグループと連動できないかと思ってやってみた雑記です。
[FacebookメッセンジャーのConversation IDを利用してチャットルームUIを作る](http://qiita.com/ynunokawa/items/48ecf8c65c69bba72870)の続編。

## チャットルーム ロジック実装

### socket.ioのRoomsを使ったチャットルームの作り方

チャットルームはSocket.ioのRoomsを使って実現します。RoomsはSocketのチャンネルであり、一度Roomsでチャンネルを初期化してしまえば、Socket.ioでチャンネルごとに送信先を分けることができます。

* socket.io: Rooms

http://socket.io/docs/rooms-and-namespaces/#rooms

io.js(Room実装前)

```JavaScript:io.js
    var socketio = require('socket.io');

    function io(server) {
      var io = socketio.listen(server);

      io.on('connection', function (socket) {
        socket.on('chat message', function(msg) {
          io.emit('chat message', msg);
        });
        socket.on('map message', function(msg) {
          io.emit('map message', msg);
        });
      });
    }
    module.exports = io;
```

上記コード内に以下のコードを追加してRoomを初期化。

    socket.join('<Room ID>');

メッセージ配信時に特定のRoomにのみ配信を行うためにRoom ID指定したioでemitする。

    io.to('<Room ID>').emit('chat message', msg);

これだけ。
以下、Room実装後のio.js。function io内抜粋。

io.js(Room実装後)

```js:io.js
    var io = socketio.listen(server);

    var store = {};

    io.on('connection', function (socket) {
      socket.on('join', function(msg) {
        usrobj = {
          'room': msg.roomid,
          'name': msg.name
        };
        store[msg.id] = usrobj;
        socket.join(msg.roomid);
      });

      socket.on('chat message', function(msg) {
        io.to(store[msg.id].room).emit('chat message', msg);
      });
      socket.on('map message', function(msg) {
        io.to(store[msg.id].room).emit('map message', msg);
      });
    });
```

この後はおまけです。

チャットルームを永続的に残さない、かつユーザー1人に対して参加できるチャットルームを1ルームのみとして、FacebookのMessengerで管理されているグループと同期してチャットルームを生成する仕組みです。

## チャットルームUI実装で取得したFacebookのConversation IDをRoom IDに使用して実装

ルームに入る際にルーム初期化のための送信メッセージ('join')をemitする。
roomidにConversation IDが入ります。
Conversation IDの取得は[FacebookメッセンジャーのConversation IDを利用してチャットルームUIを作る](http://qiita.com/ynunokawa/items/48ecf8c65c69bba72870)を参考にしてください。

```js:view/index.ejs
          socket.emit('join', {
            roomid: e.target.id,
            name: myname,
            id: myid
          });
```

socket.on('join')で渡されたmsg.idにはFacebookのConversation IDが入ってるので、これをRoom IDとしてsocket.joinする。

```js:io.js
    socket.join(msg.roomid);
```

これにより、RoomはFacebookのメッセンジャーグループと同期される。

連想配列(store = {})を作って、そこに接続ユーザーの情報を格納して、emitする際にstoreからRoom IDを取得して利用する。

```js:io.js
    io.to(store[msg.id].room).emit('chat message', msg);
```

## 退出の実装

最後に、Socket切断時の自動退出の実装です。

socket.idとFacebookユーザーIDを紐付けて管理します。

```js:io.js
    var idstore = {};
    
    // JOIN時に格納
    idstore[socket.id] = { 'id': msg.id };
```

Webアプリなのでセッションが切れると接続も切れる。
(※今回は明示的に切断するためのUIは作ってない)

Socketのdisconnect時にidstore[socket.id].idで退出したユーザーを確定する。退出メッセージ送信後は不要なidstoreを削除。

```js:io.js
      socket.on('disconnect', function() {
        if (idstore[socket.id]) {
          var _roomid = store[idstore[socket.id].id].room;
          socket.leave(_roomid);
          io.to(_roomid).emit('chat message', {
            id: idstore[socket.id].id,
            name: store[idstore[socket.id].id].name,
            text: '退出！'
          });
          delete idstore[socket.id];
        }
      });
```

メモ：if (idstore[socket.id])内で実行するようにしておかないとサーバー側のエラーが出てHerokuで動作させているアプリがエラーで使えなくなった。ログを見てると切断後ポーリングをして接続状況を確認してるのか、再度disconnectが発行されて処理に失敗してる感じ。

以上です。