2012/02/08

【その後】 やったー Titanium Mobile でも Socket.io が動いたよー \(^o^)/

ポスト @ 23:43:17 | , , ,     このエントリーを含むはてなブックマーク

久々に書いてます。

前回 やったー Titanium Mobile でも Socket.io が動いたよー \(^o^)/ って喜んでたら、socket.io のバージョンアップでgdgdしてしまっている間に
masuidrive さんのところで、ti-websocket-client が作られてたので、僕のトコで作ってた socket.io-titanium にそのまま放り込んでまま放置プレイにしてたので、ちょっと書いてみようかなと。

チャットルームみたいなものを作ってみようと思います。
結論から書くと、下の動画みたいになってます。
また、コードは https://github.com/nowelium/socket.io-titaniumにそのまま置いてます。

チャットルームみたいなチャンネル付きチャット

サーバの実装(node.js)

node.js + socket.io でこんなコードを用意します。

var io = require('socket.io').listen(8080);

var archiveMessages = {};
var channels = ['foo channel', 'bar channel'];

var chat = io.of('/chat');
chat.on('connection', function(socket){
  console.log('connected: %s', socket.id);

  // push available channel list
  socket.emit('available_channel', channels);

  socket.on('join', function(value){
    console.log('%s joined channel: %s', socket.id, value.channelId);

    socket.join(value.channelId);
    socket.set('channel_id', value.channelId, function(){
      var messages = archiveMessages[value.channelId] || [];
      socket.emit('joined', messages);
      socket.broadcast.to(value.channelId).emit('user:join', {
        id: socket.id
      });
    });
  });

  socket.on('post', function(message){
    socket.get('channel_id', function(err, channelId){
      console.log('%s says<%s channel>: %s', socket.id, channelId, message);

      if(!(channelId in archiveMessages)){
        archiveMessages[channelId] = [];
      }
      archiveMessages[channelId].push(message);

      socket.emit('posted', {
        message: message
      });
      socket.broadcast.to(channelId).emit('user:message', {
        id: socket.id,
        message: message
      });
    });
  });

  socket.on('disconnect', function(){
    console.log('%s disconnected', socket.id);
    socket.get('channel_id', function(channelId){
      socket.leave(channelId);
      socket.broadcast.to(channelId).emit('user:leave', {
        id: socket.id
      });
    });
  });
});

ここで重要なのは、socket.join(channel) なところと、 socket.broadcast.to(channel)、そして、socket.leave(channel) なところでしょうか。
これを使うことで、同じ channel に join してる人に broadcast できたりするので、今回はここからスタート

ルームを取得する

こっから titanium になります。

server 側で available_channel を socket.connection イベント時(接続したとき) に返してもらっているので、それを素直に読み取ってTableViewに詰めます。

var win = Titanium.UI.currentWindow;

var io = require('socket.io-titanium');
var socket = io.connect('169.254.10.100:8080');

var channelTable = Titanium.UI.createTableView();

var chat = socket.of('/chat');
chat.on('available_channel', function(channels){
  //
  // channel view
  //
  var rows = channels.map(function(channel){
    var row = Titanium.UI.createTableViewRow({
      title: channel
    });
    row.addEventListener('click', function (){
      channelTable.fireEvent('click:channel', {
        channelId: channel
      });
    });
    return row;
  });
  channelTable.setData(rows);
});
win.add(channelTable);

socket.ioの通信はJSONをそのまま扱ってくれるので、JSON.parseとか気にしなくていいので楽ですね。

ルームチャットを実装する

サーバ側では join イベントを待っているので、win.open 時に呼び出してあげてます。
また、サーバ側から送られてくるイベントもの(ここでは user:join や user:message など)は、一般的な EventEmitter 的な待ち受けを行い、イベントを受け取ったら処理をする。という一般的なイベントドリブンな感じで実装します。

channelTable.addEventListener('click:channel', function(evt){
  //
  // chat view
  //
  var chatWindow = Titanium.UI.createWindow({
    title: 'room: ' + evt.channelId + ' chat'
  });

  var lastRowIndex = 0;
  var chatTable;
  if(/android/i.test(Titanium.Platform.osname)){
    var input = Titanium.UI.createSearchBar({
      showCancel: false
    });
    chatTable = Titanium.UI.createTableView({
      search: input
    });
    chatWindow.add(chatTable);
  } else {
    var header = Titanium.UI.createView({
      top: 0,
      height: 60,
      width: win.width,
      borderWidth: 2,
      borderColor: '#333'
    });
    var input = Titanium.UI.createTextField({
      top: 10,
      height: 40,
      width: 200,
      color: '#333',
      hintText: 'message here'
    });
    chatTable = Titanium.UI.createTableView({
      top: 60,
      height: win.height - 60,
      width: win.width
    });

    chatWindow.add(header);
    chatWindow.add(input);
    chatWindow.add(chatTable);
  }

  chat.on('joined', function (messages){
    var rows = messages.map(function(message){
      return Titanium.UI.createTableViewRow({
        title: message,
        color: '#999'
      });
    });
    var section = Titanium.UI.createTableViewSection({
      headerTitle: 'archived message(s)'
    });
    rows.forEach(function(row){
      section.add(row);
    });
    chatTable.setData([section]);
    // delay
    setTimeout(function (){
      chatTable.scrollToIndex(rows.length - 1, { animated: false });
      lastRowIndex = rows.length;
    }, 100);
  });
  var addMessage = function(message){
    var row = Titanium.UI.createTableViewRow({
      title: message
    });
    chatTable.appendRow(row, { animated: true });
    // delay
    setTimeout(function (){
      chatTable.scrollToIndex(lastRowIndex, { animated: true });
      lastRowIndex = lastRowIndex + 1;
    }, 100);
  };
  chat.on('posted', function(value){
    return addMessage('you posted: ' + value.message);
  });
  chat.on('user:join', function(value){
    return addMessage(value.id + ' joined this channel');
  });
  chat.on('user:leave', function(value){
    return addMessage(value.id + ' leaved this channel');
  });
  chat.on('user:message', function(value){
    return addMessage(value.id + ' says ' + value.message);
  });
  input.addEventListener('return', function (){
    var messageValue = input.value;
    if(/^\s+$/.test(messageValue)){
      return;
    }

    chat.emit('post', messageValue);
    input.value = '';
  });
  chatWindow.addEventListener('open', function (){
    chat.emit('join', {
      channelId: evt.channelId
    });
  });
  chatWindow.addEventListener('close', function (){
    chat.disconnect();
  });

  chatWindow.add(chatTable);
  return Titanium.UI.currentTab.open(chatWindow);
});

うーん。短い。(色々冗長だけど)
基本的な部分はこんな感じで実装しました。

ついでにwebViewでも実装を書いてみる

これも github の方にはそのまま入ってますが、一応記述するとするとこんな感じ

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="height=device-height, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>chat sample</title>
    <style>
    #rooms button {
      display: block;
    }
    </style>
    <script type="text/javascript" src="../socket.io/dist/socket.io.js"></script>
    <script type="text/javascript">
    var $ = function(id){
      return document.getElementById(id);
    };
    window.addEventListener('load', function (){
      var socket = io.connect('169.254.10.100:8080');

      var chat = socket.of('/chat');
      chat.on('available_channel', function(channels){
        channels.forEach(function (channelId){
          var button = document.createElement('button');
          button.appendChild(document.createTextNode(channelId));

          $('rooms').appendChild(button);
          button.addEventListener('click', function(){
            $('chat').style.display = '';
            chat.emit('join', {
              channelId: channelId
            });
          });
        });
      });

      var addMessage = function(message){
        var li = document.createElement('li');
        li.appendChild(document.createTextNode(message));
        $('messages').appendChild(li);
      };
      chat.on('joined', function(messages){
        messages.forEach(function(message){
          var li = document.createElement('li');
          li.appendChild(document.createTextNode(message));
          $('archives').appendChild(li);
        });
      });
      chat.on('posted', function(value){
        return addMessage('you posted: ' + value.message);
      });
      chat.on('user:join', function(value){
        return addMessage(value.id + ' joined this channel');
      });
      chat.on('user:leave', function(value){
        return addMessage(value.id + ' leaved this channel');
      });
      chat.on('user:message', function(value){
        return addMessage(value.id + ' says ' + value.message);
      });

      $('submit').addEventListener('click', function (){
        var messageValue = $('message').value;
        if(/^\s+$/.test(messageValue)){
          return;
        }

        chat.emit('post', messageValue);
        $('message').value = '';
      });
    });
    </script>
  </head>
  <body>
    <div id="rooms">
    </div>
    <div id="chat" style="display:none">
      <input type="text" id="message" value="" placeholder="message here" />
      <input type="submit" id="submit" value="send" />
      <p>archives</p>
      <ul id="archives" style="color: #999"></ul>
      <p>messages</p>
      <ul id="messages"></ul>
    </div>
  </body>
</html>

手抜きながら短いですね。

FAQ. 169.254.10.100 ってIPアドレスは何?

これは、iphonesimulator とか android simulator で 127.0.0.1 とか 使えないので、 localhost(127.0.0.1) の alias を切ってます(OSX)

こんな感じのコマンドで割り当てます

shell > sudo ifconfig lo0 alias 169.254.10.100 netmask 0xffffff

その他。

これはまったく、未確認情報で、hybiの実装を見てないんだけど、socket.io が parse error を吐く!ってことがあったら、https://gist.github.com/1768174 のpatch を使ってみると治ることがあるらしい。

ということで、ひさびさにブログを更新した今日この頃。


1 Trackback

spec.Uz

ハタさんのブログ(復刻版) : 【その後】 やったー Titanium Mobile でも Socket.io が動いたよー \(^o^)/

From : spec.Uz @ 2013-04-23 04:57:25

Track from Your Website

http://blog.xole.net/trackback/tb.php?id=779