了解如何使用 Asynchronous JavaScript™ + XML (Ajax) 和 PHP 在 Web 应用程序中建立聊天系统。您的客户不需要下载或安装任何专门的即时消息通讯软件,就能和您及其他客户讨论网站的内容。<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
|
Web 2.0 一词出现以来,开发人员都在说社区。不论您是否认为这有点夸大其辞,但让用户或读者能够方便地实时讨论页面主题或者销售的产品,这一想法还是很吸引人的。但是怎么办呢?能否在推销产品的页面中加入聊天,而不必让客户安装任何特殊的软件包括 Adobe Flash Player 呢?当然!实践证明,用免费的现成工具如 PHP、MySQL、动态 HTML (DHTML)、Ajax 和 Prototype.js 库就能完全做到。
不再罗嗦了,让我们立即开始吧。
聊天首先要有一个身份标识。这就需要一个简单的登录页,如 清单 1 所示。
<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 所示。
注意: 该例中需要登录窗口是因为我希望知道谁在说话。对于您的应用程序,可能已经存在一个登录页面,使用自己已有的用户名即可。
|
|
聊天系统实质上就是一个字符串表格,每个字符串属于一个发言者。最简单的模式如 清单 2 所示。
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
。为了尽量简化例子,我采用了最简单的模式。
建立数据库和加载模式使用了下列命令:
% mysqladmin create chat % mysql chat < chat.sql |
根据 MySQL 服务器的设置及其安全设定和口令,命令可能略有不同。
最基本的聊天用户界面(UI)如 清单 3 所示。
<?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 所示。
请注意
getMessages()
函数,页面实际上每 1000 毫秒(1 秒)轮询一次服务器,检查是否有新消息,并把结果输出到页面上方的消息区域。本文
后面
还要详细介绍轮询,我想首先完成基本的实现,messages.php 页面返回当前的消息列表。该页如
清单 4
所示。
<table> <?php // Install the DB module using 'pear install DB' 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 库连接到数据库,这个库可从 PEAR 下载(请参阅 参考资料 )。如果还没有安装这个库,可通过下面的命令完成:
% pear install DB |
PEAR 安装后,脚本可以查询当前的消息,检索每一行,输出用户名和消息文本。
最后还有 add.php 脚本,从页面上
addmessage()
函数的 Prototype.js Ajax 代码中调用。该脚本从会话中取得消息文本和用户名,然后在消息表中插入新的一行。代码如
清单 5
所示。
<?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 所示。
<?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 页面返回所有的消息。此后则发送到目前为止显示过的最后一条消息的 ID。
XML 响应用
onSuccess
处理程序分解成元素,每个元素使用标准 DHTML 文档对象模型(DOM)函数添加到表格中,如
insertRow()
、
insertCell()
和
appendChild()
。
修改后的 messages.php 文件返回 XML 而不是 HTML,如 清单 7 所示。
<?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 显示了新的改进后的版本。
从外观上来说没有什么改变。但是和原来的相比效率要高得多。
|
|
如果刚接触 Ajax 或者仅对该领域有所了解,“轮询” 的概念可能让您感到害怕。不幸的是,轮询是惟一的办法。要在客户机和服务器之间建立连续管道,同时又不需要在两端安装特定软件,尚不存在可实现此目的的跨平台、跨浏览器方法。即便这样,可能还需要对防火墙进行专门配置才行得通。因此,如果需要人人能用的一种简便办法,Ajax 和轮询是惟一的可能。
但是不断宣传和鼓吹的 “实时” 在哪儿呢?轮询不可能是实时的。真的如此吗?我认为这取决于您对 实时 的定义。我过去编写电生理学数据检索代码时, 实时 意味着毫秒。我相信地质学家在某些情况下把分、日甚至年看作是 实时 。
如果查阅 Wikipedia,即会发现人类的平均反应时间大约在 200 到 270 毫秒之间。也就是击一次球的时间。阅读一条消息并形成答复的时间要长得多,即使您非常投入。因此,等待聊天消息时,200 毫秒左右(可能再长一点)的时间应该足够了。我设置为 1 秒,而且没有感觉到不舒服。
作为 developerWorks Ajax 论坛(请参阅 参考资料 )的主持人, 轮询 和 实时 的问题每月至少遇到一次。我希望对于 Ajax 来说已经揭穿了轮询和所谓 实时 的面具。建议在考虑某种极其复杂的实时解决方案之前尝试一下轮询。这样至少可以知道尝试自定义的解决方案之前使用现成的工具能够做什么。
|
|
希望本文为您提供了一个不错的起点,以此为基础在您的应用程序中实现自己的聊天系统。下面是一些建议:
- 记录用户: 在聊天窗口的旁边列出目前参加会谈的人员。这样可以告诉人们谁参加了谈话,什么时候来的,什么时候退出的。
- 允许多个会谈: 允许多个关于不同话题的谈话同时进行。
-
支持表情字符:
将
:-)
这样的字符组合翻译成适当的笑脸图像。 - 使用 URL 解析: 在客户端 JavaScript 代码中使用正则表达式发现 URL 并转化成超链接。
-
处理 Enter 键:
取消
Add
按钮,通过检查
textarea
的onkeydown
事件看看用户是否按下了 Enter 或 Return 键。 - 显示用户输入时间: 用户开始输入的时候通知服务器,会谈的其他人可以看到有人在回复。这样如果有人打字慢可以将谈话结束的感觉减到最低。
-
限制消息的大小:
保持谈话顺畅的另一个办法是避免消息过长。限制
textarea
中的最大字符数 — 同样通过捕获onkeydown
— 有助于提高交谈的速度。
这仅仅是修改上述代码进行改进的部分想法。如果您这样做了并且希望在社区中分享您的成果,请告诉我,我可以将其放到 下载 的源代码中。
|
|
我承认我不大喜欢聊天。我从未打开我的聊天客户机。很长时间内仅使用过一次文本消息。我的聊天标识符是 idratheryouemail 。够严肃的。不过我发现结合当前环境的聊天,比如本文所述的这种情况很吸引人。为什么?因为它主要集中在网站有关的主题上,可以最大限度的避免关于最近 “TomKat” 新闻这类的东拉西扯。
在您的 Web 应用程序中尝试这段代码。看看能否让您的读者和客户进行实时交谈,并通过 developerWorks Ajax 论坛告诉我效果如何。希望能给您以惊喜。