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>