Logn Pollingを使ったストレスの少ないチャットを作ってみた
従来ののチャットといえば、メタタグやJavascript等で定期的に更新処理がされていたのでストレスだったはず。サーバが送りたいタイミング(プッシュ型通信)でクライアントに更新をかけられれば、無駄な更新はしなくても良いし、無駄なトラフィックも無くなる。
しかし、Webブラウザではサーバからのプッシュ型通信はできない。ユーザの操作とかタイマとかをトリガにして動かすしかない。。そこで登場した技術がLong Polling。詳しくはググってもらった方が良くわかると思うので、ここでは省略。基本は、Ajaxの非同期通信を利用して、ブラウザが投げたリクエストをサーバ(今回はPerlCGI)で掴みっぱなしにして、サーバの好きなタイミングでレスポンスを返すというもの。
とりあえず、Long Pollingを使ったチャットを作ってみました。性格にはプッシュ型とはいえないかもしれないけど、Long Pollingという手法を使ってやってみた。
でも、Cometd*1とかShootingStar*2とかを使いたくなかったのでPerlのみで実装してみた。
意外と簡単に出来たので公開です。
※なぜかIEではワガママを言って動かないので、FFとかOperaで2ウィンドウ開いてやってみてくらはい。
「Please say ほげほげ.」:http://www.xtal-u.com/lp_chat/
index.cgi
#!/usr/bin/perl use strict; use warnings; use CGI qw(:standard); my $MAX_POLLING = 30; my $DEFAULT_LIST_NUM = 10; my $TITLE_NAME = "Please say ほげほげ."; my $LOG_FILE = './log_file.txt'; my $cgi = new CGI; my $max_no = getMaxNo(); # GET if ( lc($ENV{REQUEST_METHOD}) eq 'get' ) { my $cmd = $cgi->param('cmd') || ''; my $get_no = $cgi->param('no') || 0; # Long Polling printJson($get_no) if $cmd eq 'polling'; # メイン画面 printHTML() if $cmd eq ''; } # POST elsif ( lc($ENV{REQUEST_METHOD}) eq 'post' ) { my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(); my $date = sprintf("%4d/%02d/%02d %02d:%02d:%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec); my $name = htmlEscape($cgi->param('name') || ''); my $comment = htmlEscape($cgi->param('comment') || ''); my $get_no = $cgi->param('no') || 0; if ( $name ne '' && $comment ne '') { open (FH, ">> $LOG_FILE"); print FH join('<>', ($max_no + 1, $date, $name, $comment)) . "\n"; close(FH); } } exit; # メイン画面出力 sub printHTML { print "Content-Type: text/html; charset=utf-8\n\n"; print <<HTML; <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja"> <head> <meta http-equiv="content-script-type" content="text/html;charset=UTF-8" /> <meta http-equiv="content-style-type" content="text/html;charset=UTF-8" /> <title>$TITLE_NAME</title> <link rel="stylesheet" type="text/css" href="_.css" media="screen" /> <script type="text/javascript" language="javascript" src="prototype.js"></script> <script type="text/javascript" language="javascript" src="_.js"></script> <script type="text/javascript"> //<![CDATA[ var no = $max_no - $DEFAULT_LIST_NUM; longPolling(); //]]> </script> </head> <body> <h1>$TITLE_NAME</h1> <form name="say_form" onsubmit="return checkSend();"> <input type="text" id="IName" value="なまえ" onfocus="onFocusTB('IName');" onblur="onBlurTB('IName')" /> says <input type="text" id="IComment" value="ほげほげ" onfocus="onFocusTB('IComment');" onblur="onBlurTB('IComment')" />. <input type="submit" value="send" /><br /> </form> <hr /> <div id="Say"></div> <hr /> <div id="footer">Copyright 2008 Htz. All Rights Reserved.</div> </body> </html> HTML } # HTMLエスケープ sub htmlEscape { my $str = shift || return undef; $str =~ s/&/&/g; $str =~ s/</</g; $str =~ s/>/>/g; $str =~ s/\"/"/g; $str =~ s/\'/'/g; $str =~ s/[\r\n]//g; return $str; } # 一番新しい番号を取得 sub getMaxNo { open(IN, $LOG_FILE); my @log = <IN>; close(IN); return (sort {$b <=> $a} map {(split(/<>/, $_))[0]} @log )[0] || 0; } # JSON形式で新しいメッセージを送信 sub printJson { my $get_no = shift; # ここで新しいメッセージを発見するまで監視。 for (my $t = $MAX_POLLING; $t > 0 && $max_no <= $get_no; $t--) { $max_no = getMaxNo(); sleep 1; } # メッセージをファイルから取り出す open(IN, $LOG_FILE); my @line = <IN>; close(IN); my @data = (); foreach ( @line ) { my ($no, $date, $name, $comment) = split /<>/, $_; $comment =~ s/\s*(.*)\s*/$1/; push(@data, "{no : \"$no\", date : \"$date\", name : \"$name\", comment : \"$comment\"}") if $no > $get_no; } # JSON形式で、新しいメッセージを全て送信 print "Content-Type: text/plain; charset=utf-8\n\n"; print "{no : \"$max_no\", data : [" . join(',', @data) . ']}'; }
_.js
var focus_flg = { IName : false, IComment : false }; var focus_msg = { IName : 'なまえ', IComment : 'ほげほげ' }; function onFocusTB(id){ if(!focus_flg[id]){ $(id).value = ''; $(id).style.color = '#000000'; focus_flg[id] = true; } } function onBlurTB(id){ if(focus_flg[id] && $F(id) == ''){ $(id).value = focus_msg[id]; $(id).style.color = '#c0c0c0'; focus_flg[id] = false; } } function checkSend(){ var name = document.getElementById('IName').value; var comment = document.getElementById('IComment').value; if(name == '' || comment == ''){ alert('Please input name and comment.'); return false; } var param = { no : no, name : $F('IName'), comment : $F('IComment'), now : new Date().getTime() }; $('IComment').value = ''; onBlurTB('IComment'); new Ajax.Request( "index.cgi", { method : 'POST', parameters : $H(param).toQueryString() } ); return false; } function longPolling(){ var param = { cmd : 'polling', no : no, now : new Date().getTime() }; new Ajax.Request( "index.cgi", { method : 'GET', parameters : $H(param).toQueryString(), onComplete : update, onFailure : longPolling } ); } function update(httpObj){ var json = eval('(' + httpObj.responseText + ')'); no = json.no; json.data.each(function(d){ new Insertion.Top('Say', '<div class="Say1 autopagerize_page_element"><div class="Say2"><div class="Comment">' + d.comment + '</div><div class="Name">' + d.name + ' (' + d.date + ')</div></div></div>'); }); longPolling(); }