Thursday 7 April 2011

AJAX - Amazing Technology





Ajax and php: Ajax for chat

Develop Chat Application

Logging in 

The first step in chatting is to have an identity. That requires a rudimentary login page, such as the one shown in Listing 1. 

Listing 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>

A basic chat system

A chat system is really just a table of strings in which each string is attributed to an individual. The most basic version of the schema is shown in Listing 2.

Listing 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 )
);

The script contains an auto-incrementing message ID, the user name, and the message. If you think it's important, you can also add a timestamp to each message to track when it was sent.

If you want to manage multiple conversations on different topics, you must have another table to track the topics and include a related topic_id in the messages table. I wanted to keep this example simple, so I used the simplest possible schema.

The basic user interface (UI) of the chat is shown in Listing 3.

Listing 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>

At the top of the script, you get the user name from the posted arguments of the login page and store it in the session. The page then continues to load the invaluable Prototype.js JavaScript library, which will handle all the Ajax work for you.

After that, the page has a spot in which the chat messages go. This area is populated by the getMessages() JavaScript function located at the bottom of the file.

Below the messages area is a form and a textarea in which users type their message text. You also have a button labeled Add that adds the message to the chat.
Careful inspection of the getMessages() function shows that the page is indeed polling the server every 1,000 milliseconds (1 second) to check for new messages and put the output of the call into the messages area at the top of the page. I talk more about polling later in the article, but for the moment, I want to finish the basic implementation of the chat by showing the messages.php page, which returns the current set of messages. This page is shown in Listing 4.

Listing 4. messages.php


<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>
At the top of the script, I connect to the database with the DB library, which is available from PEAR . If you haven't installed that library already, you can do so with the following command: 


"% pear install DB"
With PEAR installed, the script can query the current messages, fetch each row, and output the user name and the comment text.

The final script is the add.php script, which is called from the Prototype.js Ajax code in the addmessage() function on the page. This script takes the message text and the user name from the session and inserts a new row into the messages table. This code is shown in Listing 5.

Listing 5. add.php


<?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()); }
$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>
The add.php script also returns the current list of messages, because the Ajax code on the original page updates chat messages from the returned HTML code. This behavior gives users immediate feedback that they added a comment to the conversation.

These are the basics of the chat system. The next section covers how to make the polling a bit more efficient.

Making a better chat

With the original chat system, the page requests all the chat messages for the conversation every second. While not too bad for a short conversation, for something longer, it will be a real performance problem. Thankfully, there's a fairly easy solution. A message_id is associated with each message, and that number increases incrementally. So, if you know that you have messages to a certain ID, then you simply ask for any messages that occur after that ID. That keeps the message traffic down in a big way. For most requests, you're likely get no new messages, which is a very small packet.

Changing this setup to the more efficient design requires a few changes to the chat.php page, as shown in Listing 6.

Listing 6. chat.php (revised)


<?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>

Instead of a "chat" <div> tag that held all the messages, you now have a <table> tag; you add rows to that tag dynamically for each new message as it comes in. You can see the change in the getMessages() function, which has gained a little weight since its first version.

This new version of getMessages() expects the results of the messages.php page to be an XML block with the new messages. The messages.php page now also takes a parameter called id, which is the message_id of the last message the page has seen. The first time out, this ID is 0, so that the messages.php page returns everything it has. After that, it's sent the ID of the last message seen so far.

The XML response is broken up by the onSuccess handler, and each element is added to the table using standard DHTML document object model (DOM) functions such as insertRow(), insertCell(), and appendChild().

The upgraded version of the messages.php file that returns XML instead of HTML is shown in Listing 7.

Listing 7. messages.php


<?php
// Install the DB module using 'pear install DB'
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>
Moving on from here

Hopefully, what I have given you here is a good starting point for your own implementation of a chat system within your application. Here are some ideas about where to go next:
  • Track users: Put a list of the people actively engaged in the conversation alongside the chat. Doing so lets people know who is at the party and when they come and go.
  • Allow multiple conversations: Allow multiple conversations on different topics to go on simultaneously.
  • Allow emoticons: Translate character groupings such as :-) into the appropriate image of a smiley face.
  • Use URL parsing: Use regular expressions in the client-side JavaScript code to find URLs and turn them into hyperlinks.
  • Handle the Enter key: Instead of having an Add button, watch for users pressing the Enter or Return key by hooking onto the onkeydown event in the textarea.
  • Show when a user is typing: Alert the server when a user has started to type, so that other participants can see that a reply is pending. This lessens the perception that the conversation has died off to a minimum if you have slow typists.
  • Limit the size of a posted message: Another way to keep the conversation going is to keep messages small. Limit the maximum number of characters in the textarea —again by trapping onkeydown —to help speed up conversations.
Conclusion

I admit that I'm really not much of a chatter. I never have my chat client up. I only use text messages once in a long while. My chat handle is idratheryouemail. Seriously. But I have found that contextual chat, such as what is shown in this article, is actually compelling. Why? Because it's focused on the subject matter that the site addresses, which keeps distracting talk about the most recent "TomKat" news to a minimum.

Give this example code a try in your Web application. See whether you can engage your readers and customers in a real-time conversation, and let me know how it goes on the developerWorks Ajax forum site. I hope you will be pleasantly surprised.

useful links


1 comment: