レベル: 中級 Jack D Herrington (jherr@pobox.com), Senior Software Engineer, Leverage Software Inc.
2007年 12月 04日 Ajax (Asynchronous JavaScript™ + XML) と PHP を使って Web アプリケーションにチャット・システムを組み込む方法を学んでください。チャット・システムを組み込めば、特別なインスタント・メッセージ・ソフトウェアをダウンロードまたはインストールしなくても、サイトのコンテンツについてサイトの利用者が意見を述べたり、利用者同士が話し合えるようになります。
Web 2.0 の話題となると、開発者たちはコミュニティーについて雄弁に語り出します。そして大げさに誇張していると思うかもしれませんが、サイトの利用者や読者が身近なトピックやサイトで販売している製品についてその場で話し合えるようにするという考えは、かなり魅力的な発想です。しかし、どうやってそれを実現するのでしょう。製品リストと同じページにチャット・ボックスを配置して、利用者が特別なソフトウェアや、さらには Adobe Flash Player でさえもインストールしなくて済むようにできるのでしょうか。いや、ご心配には及びません。PHP、MySQL、DHTML (Dynamic HTML)、Ajax、それに Prototype.js ライブラリーといった容易に手に入れられる無料のツールだけで実現できます。
前置きはこれくらいにして、早速実装に取り掛かりましょう。
ログイン
チャットする際の最初のステップは ID を取得することです。それには、リスト 1 に記載するような基本的なログイン・ページが必要になります。
リスト 1. index.html
<html>
<head><title>Chat Login</title></head>
<body>
<form action="chat.php" method="post">
Username: <input type="text" name="username">
<input type="submit" value="Login">
</form>
</body>
</html>
|
図 1 は、このページのスクリーンショットです。
図 1. チャット用ログイン・ウィンドウ
注: このサンプルでこのログイン・ページが必要なのは、誰が何を言っているのかを判別するためだけです。実際のアプリケーションで既にログインが済んでいる場合は、ログイン時のユーザー名を使用して構いません。
基本的なチャット・システム
チャット・システムとは単なる文字列のテーブルで、このテーブルに含まれるそれぞれの文字列がチャットに参加している誰かに属しています。もっとも基本的なスキーマをリスト 2 に記載します。
リスト 2. chat.sql
DROP TABLE IF EXISTS messages;
CREATE TABLE messages (
message_id INTEGER NOT NULL AUTO_INCREMENT,
username VARCHAR(255) NOT NULL,
message TEXT,
PRIMARY KEY ( message_id )
);
|
上記のスクリプトには、自動的にインクリメントされるメッセージ ID、ユーザー名、メッセージが含まれています。メッセージがいつ送信されたかをわかるようにすることが重要と考えるなら、各メッセージにタイムスタンプを追加することもできます。
それぞれにトピックが異なる複数の会話を管理するには、トピックを追跡するためのテーブルを別に用意し、関連 topic_id を messages テーブルに含める必要があります。しかしこのサンプルは単純にしておきたいので、可能な限り単純なスキーマを使用しています。
データベースをセットアップしてこのスキーマをロードするために、以下の命令を使用しました。
% mysqladmin create chat
% mysql chat < chat.sql
|
MySQL サーバーのセットアップとそのセキュリティー設定およびパスワードによって、使用するコマンドは多少異なってきます。
チャットの基本ユーザー・インターフェース (UI) をリスト 3 に記載します。
リスト 3. chat.php
<?php
if ( array_key_exists( 'username', $_POST ) ) {
$_SESSION['user'] = $_POST['username'];
}
$user = $_SESSION['user'];
?>
<html>
<head><title><?php echo( $user ) ?> - Chatting</title>
<script src="prototype.js"></script>
</head>
<body>
<div id="chat" style="height:400px;overflow:auto;">
</div>
<script>
function addmessage()
{
new Ajax.Updater( 'chat', 'add.php',
{
method: 'post',
parameters: $('chatmessage').serialize(),
onSuccess: function() {
$('messagetext').value = '';
}
} );
}
</script>
<form id="chatmessage">
<textarea name="message" id="messagetext">
</textarea>
</form>
<button onclick="addmessage()">Add</button>
<script>
function getMessages()
{
new Ajax.Updater( 'chat', 'messages.php', {
onSuccess: function() { window.setTimeout( getMessages, 1000 ); }
} );
}
getMessages();
</script>
</body>
</html>
|
スクリプトの先頭では、ログイン・ページで送信された引数からユーザー名を取得して、それをセッションに保存しています。このページでは続いて非常に重要な Prototype.js JavaScript ライブラリーをロードします。このライブラリーが、すべての Ajax 作業を自動的に処理してくれます。
ライブラリーのロードが完了すると、ページにはチャット・メッセージを表示する領域が用意されます。この領域には、ファイルの終わりにある getMessages() JavaScript 関数によってメッセージが入力されます。
メッセージ領域の下にあるのは、フォーム、ユーザーがメッセージ・テキストを入力する textarea、そしてチャットにメッセージを追加する Add というラベルが付いたボタンです。
このページは図 2 のように表示されます。
図 2. 単純なチャット・ウィンドウ
getMessages() 関数をよく見てみると、このページは 1,000 ミリ秒 (1 秒) 毎にサーバーをポーリングして新しいメッセージがあるかどうかをチェックし、ある場合はそれをページ先頭のメッセージ領域に表示していることがわかります。ポーリングについては後で詳しく説明することにして、ここではとりあえず現行のメッセージ・セットを返す messages.php ページの説明をして、チャットの基本実装を完了したいと思います。このページはリスト 4 のとおりです。
リスト 4. messages.php
<table>
<?php
require_once("DB.php");
$db =& DB::Connect( 'mysql://root@localhost/chat', array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$res = $db->query('SELECT * FROM messages' );
while( $res->fetchInto( $row ) )
{
?>
<tr><td><?php echo($row[1]) ?></td>
<td><?php echo($row[2]) ?></td></tr>
<?php
}
?>
</table>
|
スクリプトの先頭では、DB ライブラリーによってデータベースに接続しています。この DB ライブラリーは PEAR から入手することができます (「参考文献」を参照)。このライブラリーをまだインストールしていないのであれば、以下のコマンドでインストールすることができます。
PEAR がインストールされていれば、スクリプトを使って現行のメッセージにクエリーを実行して各行をフェッチし、ユーザー名とコメント・テキストを出力することができます。
最後のスクリプトとなる add.php は、ページ上で addmessage() 関数に含まれる Prototype.js Ajax コードから呼び出されるスクリプトです。このスクリプトはセッションからメッセージ・テキストとユーザー名を取得し、新しい行を messages テーブルに挿入します。このコードは、リスト 5 のとおりです。
リスト 5. add.php
<?php
require_once("DB.php");
$db =& DB::Connect( 'mysql://root@localhost/chat', array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
$sth = $db->prepare( 'INSERT INTO messages VALUES ( null, ?, ? )' );
$db->execute( $sth, array( $_SESSION['user'], $_POST['message'] ) );
?>
<table>
<?php
$res = $db->query('SELECT * FROM messages' );
while( $res->fetchInto( $row ) )
{
?>
<tr><td><?php echo($row[1]) ?></td>
<td><?php echo($row[2]) ?></td></tr>
<?php
}
?>
</table>
|
add.php スクリプトは現行のメッセージ・リストも返します。これは、当初のページでは Ajax コードが戻り HTML コードからチャット・メッセージを更新するためです。この振る舞いによって、ユーザーが会話に追加したコメントはすぐにユーザーにフィードバックされることになります。
以上が、チャット・システムの基本です。次のセクションでは、ポーリングを効率化する方法について説明します。
チャットをより快適にする方法
当初のチャット・システムでは、ページが毎秒、会話のチャット・メッセージすべてを要求します。この動作は短い会話であればそれほど問題にはなりませんが、会話が長く続いている場合にはパフォーマンスに大きな影響を与えます。幸いなことに、これにはかなり簡単なソリューションがあります。message_id は各メッセージに関連付けられており、その番号は順番にインクリメントされていきます。そこで、特定の ID に関連付けられたメッセージがあることがわかっている場合、その ID の後に発生するメッセージを要求すれば、メッセージ・トラフィックを大々的に抑えられるというわけです。ほとんどの要求で新しいメッセージを取得することがないため、パケットは非常に小さくなります。
セットアップをより効率的な設計に変えるには、chat.php ページに多少の変更が必要です (リスト 6 を参照)。
リスト 6. chat.php (修正後)
<?php
if ( array_key_exists( 'username', $_POST ) ) {
$_SESSION['user'] = $_POST['username'];
}
$user = $_SESSION['user'];
?>
<html>
<head><title><?php echo( $user ) ?> - Chatting</title>
<script src="prototype.js"></script>
</head>
<body>
<div style="height:400px;overflow:auto;">
<table id="chat">
</table>
</div>
<script>
function addmessage()
{
new Ajax.Request( 'add.php', {
method: 'post',
parameters: $('chatmessage').serialize(),
onSuccess: function( transport ) {
$('messagetext').value = '';
}
} );
}
</script>
<form id="chatmessage">
<textarea name="message" id="messagetext">
</textarea>
</form>
<button onclick="addmessage()">Add</button>
<script>
var lastid = 0;
function getMessages()
{
new Ajax.Request( 'messages.php?id='+lastid, {
onSuccess: function( transport ) {
var messages = transport.responseXML.getElementsByTagName( 'message' );
for( var i = 0; i < messages.length; i++ )
{
var message = messages[i].firstChild.nodeValue;
var user = messages[i].getAttribute('user');
var id = parseInt( messages[i].getAttribute('id') );
if ( id > lastid )
{
var elTR = $('chat').insertRow( -1 );
var elTD1 = elTR.insertCell( -1 );
elTD1.appendChild( document.createTextNode( user ) );
var elTD2 = elTR.insertCell( -1 );
elTD2.appendChild( document.createTextNode( message ) );
lastid = id;
}
}
window.setTimeout( getMessages, 1000 );
}
} );
}
getMessages();
</script>
</body>
</html>
|
上記では、すべてのメッセージを保持していた "chat" の <div> タグの代わりに <table> タグを使用しています。このタグには、新しいメッセージを受け取るたびに動的に行を追加します。ご覧のとおり、getMessages() 関数は変更され、最初のバージョンに比べていくらか大規模になっています。
この新しいバージョンの getMessages() は、messages.php ページの結果が新規メッセージの XML ブロックであることを想定しています。messages.php も id というパラメーターを取るようになっています。このパラメーターは、ページに表示された最後のメッセージの message_id です。最初この ID は 0 であるため、messages.php ページはそこに含まれるすべてのメッセージを返しますが、それ以降 getMessages() はページに表示された最後のメッセージの ID を送信します。
XML 応答は onSuccess ハンドラーによって分解され、それぞれの要素は insertRow()、insertCell()、appendChild() などの標準DHTML の DOM (Document Object Model) 関数によってテーブルに追加されます。
リスト 7 のアップグレードされた messages.php ファイルは、HTML でなく XML を返します。
リスト 7. messages.php
<?php
require_once("DB.php");
header( 'Content-type: text/xml' );
$id = 0;
if ( array_key_exists( 'id', $_GET ) ) { $id = $_GET['id']; }
$db =& DB::Connect( 'mysql://root@localhost/chat', array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
?>
<messages>
<?php
$res = $db->query( 'SELECT * FROM messages WHERE message_id > ?', $id );
while( $res->fetchInto( $row ) )
{
?>
<message id="<?php echo($row[0]) ?>" user="<?php echo($row[1]) ?>">
<?php echo($row[2]) ?>
</message>
<?php
}
?>
</messages>
|
この新しい拡張バージョンを図 3 に記載します。
図 3. 最適化されたチャット・ウィンドウ
ルック・アンド・フィールの点では何も変わっていませんが、当初よりも遥かに効率的になっています。
「リアルタイム」の神話
Ajax の初心者や現場での経験と知識が豊富な良識あるプログラマーにとって、「ポーリング」という考えは背筋がぞっとするものかもしれませんが、チャット・システムではあいにくポーリングが唯一の手段となります。クライアントにも、サーバーにも特別なソフトウェアをインストールせずに、この両者を継続的に接続するクロスプラットフォームかつクロスブラウザーの方法はありません。そのような方法があったとしても、それを実現するには特殊なファイアウォール構成が必要になってくるでしょう。したがって、誰もが使える簡単なソリューションとしては、Ajax とポーリングを使用するしか手がないのです。
それにしても、製品紹介で「リアルタイム」と主張されているのはどういったことでしょうか。ポーリングがリアルタイムのはずはありませんが、実はリアルタイムにできるのでしょうか。それは、各自がどのようにリアルタイムを定義するかによると思います。例えば私が電気生理学のデータを取得するためのコードを作成したとき、リアルタイムとは数マイクロ秒のことでした。地質学者であれば、数分、数日、あるいは場合によっては数年をリアルタイムと見なすこともあるでしょう。
ウィキペディアを調べてみると、人間の平均反応時間は 200 ミリ秒から 270 ミリ秒とあります。これは、ボールを打つなどの行動についての時間でしかありません。メッセージを読み取ってそれに対する返信を考え始めるのにかかる時間は、たとえ会話に没頭しているとしても、それより遥かに長いはずです。したがって、チャット・メッセージを待つ時間が 200 ミリ秒程度 (あるいはそれより多少長い程度) であれば、実際にはまったく問題ないのです。このサンプルではポーリング間隔を 1 秒にしていますが、それでもあまり気になりません。
developerWorks の Ajax フォーラム (「参考文献」を参照) でモデレーターを務めていると、ポーリングとリアルタイムの問題が少なくとも月 1 回は話題に上ります。Ajax でのポーリングとリアルタイムの問題については、このフォーラムで真相に迫れているとよいのですが。私が提案しているのは、途方もなく複雑なリアルタイム・ソリューションを作成する前に、ポーリングを試してみることです。せめてカスタムのソリューションを作成する前に、簡単に入手できるツールで何ができるかを調べてください。
ここから先の展開
この記事の内容が、アプリケーション内に独自のチャット・システムを実装する出発点になることを願っています。さらにこのチャット・システムを以下のように拡張することが考えられます。
-
ユーザーの追跡: アクティブに会話に参加しているユーザーのリストをチャットの横に配置します。こうすることによって、誰が現在チャットに加わっているか、そしてユーザーが参加したこと、退場したことがわかるようにします。
-
複数の会話の許可: それぞれに話題が異なる複数の会話を同時進行できるようにします。
-
エモティコン (顔文字) の許可: :-) などの文字グループを該当する笑顔の画像に変換します。
-
URL 解析の使用: クライアント側 JavaScript コードで正規表現を使用して URL を検索し、見つかった URL をハイパーリンクに変換します。
-
Enter キーの操作: Add ボタンを使用する代わりに textarea で onkeydown イベントを利用して、ユーザーが Enter キーまたは Return キーを押したかどうかを監視します。
-
ユーザー入力時の表示: ユーザーによる入力開始時にサーバーにアラートを出し、返信が作成中であることが他の参加者にわかるようにします。これにより、ユーザーが入力に時間をかけている場合に、会話が終わってしまったと認識されることが少なくなります。
-
送信メッセージのサイズ制限: 会話を維持するもう 1 つの方法は、メッセージのサイズを小さく抑えることです。この場合も onkeydown イベントを利用することによって textarea の最大文字数を制限し、会話が迅速に進むようにします。
上記はこの記事に記載したコードに考えられる拡張のほんの数例でしかありません。他の拡張を行って、それをコミュニティーと共有したい場合には、ぜひお知らせください。「ダウンロード」から入手できるソース・コードに組み入れたいと思います。
まとめ
正直言って私はたいしたチャッターではありません。自分でチャット・クライアントを立ち上げたこともなければ、テキスト・メッセージを使うのもごくまれです。私のチャットのハンドルネームは冗談抜きで idratheryouemail となっています。そうは言っても、この記事で説明したような、状況に即したチャットは実に魅力的だと思います。なぜなら、このようなチャットではサイトで扱っている主題に焦点が絞られ、最新の「TomKat (訳注: トム・クルーズとケイティ・ホームズの愛称)」ニュースなどにまつわる気の散る話が最小限に抑えられるからです。
この記事のサンプル・コードを Web アプリケーションで試してみてください。サイトの読者や利用者をリアルタイムの会話に夢中にさせられるかどうかを調べて、その結果をdeveloperWorks Ajax フォーラム・サイトに報告してください。あなたにとって結果が嬉しい驚きになることを期待しています。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Source code for chat application | x-ajaxxml8-chat.zip | 38KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について
記事の評価
|