Web チャット?

Comet 勉強の続き (試しにチャットを作ってみましたという話).


ShootingStar のコードを読む前に,WEBrick のコードを読んでみました.
(呼び出し階層についてはこちら→ http://d.hatena.ne.jp/KrdLab/20090410 の下の方)


WEBrick の HTTPServer では,クライアントからの接続に対して 1 つのスレッドを発行する様になっています (Servlet なので).
(ちなみに,http_version = 1.1 かつ リクエスト/レスポンスともに KeepAlive ならば,スレッドの内部でループする)


しばらく読んでいたら,ふと,
「数人規模のチャットシステムなら作れるんじゃないか?」と思い,作ってみました.

コンセプトコードなので,サーバ/クライアントコードともに手抜きしているのはあしからず (→ 完成度はとても低いですよ (>_<)).

server.rb

require 'monitor'
require 'webrick'
include WEBrick

# ドキュメントルート
$DOC_ROOT = Dir.pwd

# 排他制御も何もしていないのはご勘弁を.
$tmp = "" # XXX "とりあえず" の保管場所 (TODO DB に変更し,履歴を残す)

# チャットサーブレット
class ChatServlet < HTTPServlet::AbstractServlet
  def do_GET(req, res)
    if req.query['type'] == 'wait' then
      do_main(req, res)
    else
      connect_first(req, res)
    end
  end

  def do_POST(req, res)
    $tmp = "[#{req.query['user']}]: #{req.query['message']} (#{Time.now.to_s})"
    wakeup_waiters
    res.content_type = 'text/plain;charset=UTF-8'; res.body = '' # 捨てデータ
  end

private
  def connect_first(req, res)
    # 初回接続時は強制的に index.html を返す
    res.content_type = 'text/html;charset=UTF-8';
    res.body = open($DOC_ROOT + '/index.html').read
  end

  def do_main(req, res)
    begin
      wait_message
      if not closing? then
        res.content_type = 'text/plain;charset=UTF-8'
        res.body = $tmp # XXX 差分送信の管理も厳密にする
      end
    ensure
      release_waiting
    end
  end

  def wait_message
    @server.add_to_waiters(Thread.current)
    Thread.stop
  end

  def release_waiting
    @server.remove_from_waiters(Thread.current)
  end

  def closing?
    Thread.current[:CloseServerRequired]
  end

  def wakeup_waiters
    @server.wakeup_waiters
  end
end

# http サーバ
server = HTTPServer.new(
  :Port => 8001,
  :DocumentRoot => $DOC_ROOT
)

# 拡張
# ConditionVariable ぽいこともやっちゃってる...
class << server
  def add_to_waiters(thread)
    @lock.synchronize {
      @waiters << thread if not @waiters.include?(thread)
    }
  end

  def remove_from_waiters(thread)
    @lock.synchronize { @waiters.delete(thread) }
  end

  def wakeup_waiters
    @lock.synchronize {
      @waiters.each {|th| th.run }
    }
  end

  def start(&block)
    @waiters = []
    @lock = Monitor.new
    super
  end

  def shutdown
    @lock.synchronize {
      @waiters.each {|th|
        th[:CloseServerRequired] = true
        th.run
      }
    }
    super
  end
end

# 準備
server.mount('/chat', ChatServlet)

['INT', 'TERM'].each {|s|
  Signal.trap(s) { server.shutdown }
}

# 開始
server.start

index.html

<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>Chat テスト</title>
</head>
<body onload="wait_message(END_POINT)">
  <!-- ブラウザを閉じるときに httpObj.abort() を呼び出す必要がある -->
  <!--  → 何かボタンを押させるか? -->
  <div>
    <form method="" action="">
      <input type="text" name="user" size="20" />
      <input type="text" name="message" size="30" />
      <input type="button" value="post"
          onclick="post_message(END_POINT,user.value,message.value)" />
    </form>
  </div>
  <hr />
  <div id="message_list"></div>
</body>
<script type="text/javascript">
var END_POINT = 'http://localhost:8001/chat';
function create_http_request() {
  if (window.ActiveXObject) {
    try {
      return new ActiveXObject("Msxml2.XMLHTTP");
    } catch (e) {
      try {
        return new ActiveXObject("Microsoft.XMLHTTP");
      } catch (e2) {
        alert('ごめん,動かない.');
        return null;
      }
    }
  } else if (window.XMLHttpRequest) {
    return new XMLHttpRequest();
  } else {
    alert('ごめん,動かない.');
    return null;
  }
}
function wait_message(to_url) {
  var req = create_http_request();
  if (!req) { return; }
  req.open('GET', to_url + '?type=wait', true);
  req.onreadystatechange = function() {
    if (req.readyState == 4) {
      // 確定
      on_complete(req);
    }
  }
  req.send(null);
}
function on_complete(res) {
  if (res.status == 200) {
    on_success(res);
  } else {
    on_failure(res);
  }
  // TODO サーバから終了信号がきたら wait_message は呼び出さない
  wait_message(END_POINT);
}
function on_success(res) {
  var out_p = document.getElementById('message_list');
  out_p.innerHTML = '<p>' + res.responseText + '</p>' + out_p.innerHTML;
}
function on_failure(res) {
  // TODO
}
function post_message(to_url, user, message) {
  var req = create_http_request();
  if (!req) { return; }
  req.open('POST', to_url, true);
  req.setRequestHeader('Content-Type',
      'application/x-www-form-urlencoded;charset=UTF-8');
  req.send('user=' + user + '&message=' + message);
}
</script>
</html>

実行結果

http://localhost:8001/chat にアクセス.

チャットっぽく動いている.


来週に続く...かも.