With all of the "new school" JavaScript frameworks out there like jQuery and Dojo, it's extremely easy to get AJAX HTTP requests going without even knowing what the XMLHttpRequest object is. However, I think it's still important to understand what kind of magic jQuery is doing underneath the hood. So here is a beginner's tutorial I've recently resurrected and revised that describes how to create a responsive chat room using JavaScript and PHP.
Before we begin, you can get the example code here and checkout the demo here.
We're going to start out with our database model. Since this is a chat room we will probably need to keep track of the users in the room and all of the messages people have sent. After fleshing out the tables I came up with something like this:
CREATE DATABASE IF NOT EXISTS chatroom;
USE chatroom;
--
-- Definition of table `messages`
--
DROP TABLE IF EXISTS `messages`;
CREATE TABLE `messages` (
`id` int(10) unsigned NOT NULL auto_increment,
`userId` int(10) unsigned NOT NULL,
`message` varchar(255) NOT NULL,
`dateCreated` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `FK_messages_1` (`userId`),
CONSTRAINT `FK_messages_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`)
)
--
-- Definition of table `users`
--
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL auto_increment,
`username` varchar(45) NOT NULL,
`isActive` tinyint(1) NOT NULL,
`dateJoined` datetime NOT NULL,
`dateUpdated` datetime NOT NULL,
`ipAddress` varchar(25) NOT NULL,
PRIMARY KEY (`id`)
)
Now that our tables are defined we can get started on a basic web service that our JavaScript code will use to communicate with the server. To keep this tutorial short and sweet we will be creating the service in PHP using PHP Data Objects (PDO). So create a new file called chat.php and setup your MySQL connection:
$dbh = new PDO("mysql:host=localhost;dbname=chatroom", "YOUR_DB_USERNAME", "YOUR_DB_PASSWORD");
The main purpose of this basic service will be to recieve new messages and to return the current state of our chatroom. However, we also need to recieve new users so lets handle that scenario first. Since this is not exactly a high-security chat room we won't have a lengthy registration process. All we really care about is the username. If there is a POSTed username on the request, we check if that username already exists. If it does, we assign this new IP to that username. If it doesn't, we simply push a new user to the database with the specified username. Be sure to use prepared statements to prevent MySQL injection attacks.
/* persist user if theres a username on the request */
if(isset($_POST["username"]) && strlen(trim($_POST["username"])) > 0){
$dateNow = date("Y-m-d H:i:s");
$ip = $_SERVER["REMOTE_ADDR"];
$psUsernameCheck = $dbh->prepare("SELECT * FROM `users` WHERE `username` = ? AND `isActive` = 0 limit 1");
$psUsernameCheck->execute(array($_POST['username']));
if($psUsernameCheck->rowCount() > 0){
$user = $psUsernameCheck->fetchObject();
if(!$user->isActive){
$psUpdateUser = $dbh->prepare("UPDATE `users` SET `ipAddress` = ?, `dateUpdated` = ?, `isActive` = 1 WHERE `id` = ? limit 1");
$psUpdateUser->execute(array($ip,$dateNow,$user->id));
}
outputStatus();
}else{
/* otherwise, insert a new one */
$psNewUser = $dbh->prepare("INSERT INTO `users` (`username`,`isActive`,`dateJoined`,`dateUpdated`,`ipAddress`) VALUES (?, ?, ?, ?, ?)");
$isActive = true;
$dateJoined = $dateUpdated = $dateNow;
$psNewUser->execute(array($_POST['username'], $isActive, $dateJoined, $dateUpdated, $ip));
outputStatus($dbh);
}
}
Now that we have our extremely basic registration system in place, we will setup an even more basic authentication system. If there's a userId posted we will look it up in the database and make sure that the IP is the same as the one making the request. If it is then the user appears to be authentic so we update their dateUpdated column and move on.
if(isset($_POST["userId"]) && is_numeric($_POST["userId"])){
$psUserIdCheck = $dbh->prepare("SELECT * FROM `users` WHERE `id` = ? AND `isActive` = 0");
$psUserIdCheck->execute(array(intval($_POST["userId"])));
if($psUserIdCheck->rowCount() > 0){
$user = $resUserIdCheck->fetchObject();
if($user->ipAddress == $_SERVER["REMOTE_ADDR"]){
$psUpdateUser = $dbh->prepare("UPDATE `users` SET `dateUpdated` = ? WHERE `id` = ?");
$psUpdateUser->execute(array(date("Y-m-d H:i:s"),$user->id));
}
}
}
Now that we have an authentic user we can modify our basic service to handle incoming messages. This process will be as easy as checking for a message on the request and persisting it if it's there.
if(isset($_POST["message"]) && strlen(trim($_POST["message"])) > 0){
$id = null;
$userId = intval($_POST["userId"]);
$message = $_POST["message"];
$dateCreated = date("Y-m-d H:i:s");
$psNewMessage = $dbh->prepare("INSERT INTO `messages` VALUES (?, ?, ?, ?)");
$psNewMessage->execute(array($id,$userId,$message,$dateCreated));
}
Then we need to provide a way for the user to log out. We like short and sweet so we will just look for a posted value and log the user out if they are authentic and the value is there.
if($_POST["logout"] == "true"){
$dateNow = date("Y-m-d H:i:s");
$psUpdateUser = $dbh->prepare("UPDATE `users` SET `dateUpdated` = ?, `isActive` = 0 WHERE `id` = ?");
$psUpdateUser->execute(array($dateNow,$user->id));
}
And finally, we will dump the current status of the chatroom in xml format.
$xml = "<?xml version='1.0' encoding='utf-8'?>" . "\n";
$xml .= "<chatroom>\n";
$xml .= "<users>\n";
$psAllUsers = $dbh->query("SELECT * FROM `users` WHERE `isActive` = 1");
while($resUser = $psAllUsers->fetchObject()){
$xml .= "<user>\n";
$xml .= "<username>".$resUser->username."</username>\n";
$xml .= "</user>\n";
}
$xml .= "</users>\n";
$psNewMessages = $dbh->prepare("SELECT * from `messages` where `dateCreated` > ?");
$psNewMessages->execute(array($user->dateUpdated));
$xml .= "<messages>\n";
while($newMessage = $psNewMessages->fetchObject()){
$psUser = $dbh->prepare("SELECT `username` FROM `users` WHERE `id` = ? limit 1");
$psUser->execute(array($newMessage->id));
$user = $psUser->fetchObject();
$xml .= "<message>\n";
$xml .= "<author>".$user->username."</author>\n";
$xml .= "<content>".$newMessage->message."</content>\n";
$xml .= "</message>\n";
}
$xml .= "</messages>\n";
$xml .= "</chatroom>\n";
echo $xml;
Now that we're done with the service we can move to the front end. We will start by defining our markup in a new html file called index.html.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>AJAX Chatroom</title>
<script type="text/javascript" src="chat.js"></script>
</head>
<body>
<div id="chatroom">
<div id="messages">
</div>
<div id="users">
<fieldset>
<legend>Users (<span id="user_count">0</span>)</legend>
</fieldset>
</div>
<form id="form" action="chat.php" method="post">
<p><input type="text" id="message" /></p>
<p><input type="submit" value="Send" /></p>
</form>
</div>
<script type="text/javascript">
init();
</script>
</body>
</html>
Notice the JavaScript at the bottom of the document. That call to init will be the entry point for our client side scripts, it's at the bottom because it has to be called after the rest of the document has loaded. After we've got the markup ready to go we'll finally get started on the JavaScript. Create a new JavaScript file called chat.js and get started by defining an attachEvent function. Attaching an event is a little tricky in JavaScript because it's implemented differently depending on the user's browser. Checking which method to use is simple enough, but it's something we will do a lot so lets stuff it in a function.
function addEvent(obj, evType, fn, useCapture){
if (obj.addEventListener){
obj.addEventListener(evType, fn, useCapture);
return true;
} else if (obj.attachEvent){
var r = obj.attachEvent("on"+evType, fn);
return r;
} else {
alert("Handler could not be attached");
}
}
To create HTTP requests using JavaScript we have to use the XMLHttpRequest object which has the same problem as event handling. IE5.x and IE6 have implemented it differently. Again, since we use this everywhere, we will write another function to get an XMLHttpRequest object that accomodates everyone.
function getXmlHttp(){
var xmlhttp;
if (window.XMLHttpRequest){
// If IE7, Mozilla, Safari, etc: Use native object
xmlhttp = new XMLHttpRequest();
}
else{
// ...otherwise, use the ActiveX control for IE5.x and IE6
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
}
return xmlhttp;
}
Now lets create a function that will initialize the chatroom. The first thing we need to do when we initialize is ask the user for their username. Once they give it to us we will need to send it up to the server using an XMLHttpRequest object. The process is fairly simple, we open the request, set our headers, then call send passing our parameters. Then we add a listener to the object's onreadystatechange event, the readyState value we are looking for is 4 because that means the response is ready. You can read more about the readyState property over at quirksmode. After we know there's a response we check the status property which exposes the HTTP status code our server returned. We're looking for a 200 OK status code, which means the operation was successful. You can read more about HTTP status code's from at the W3C's website.
function init(){
var username = prompt("Please enter your username...");
xmlhttp = getXmlHttp();
xmlhttp.open('POST', 'chat.php', true);
xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xmlhttp.send('username=' + username);
xmlhttp.onreadystatechange = function(){
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
/* everything went ok, lets parse! */
}
}
}
}
If no one else is using the username the server will send us down some xml, otherwise the response will be empty and we will tell the user that they were not logged in.
if (xmlhttp.responseXML != null) {
/* log in successful */
}else{
alert("you were not logged in!");
}
In the case that we get some xml, lets start to parse it. We will use the DOM element methods like getElementById, getElementsByTagName, and createElement to parse out what we need from the XML DOM and place it into our HTML DOM. For an in-depth reference of Gecko DOM methods I highly recommend Mozilla Developer Center. Lets start simple by just grabbing the current user id and storing it in a variable.
currentUser = responseXML.getElementsByTagName('currentUser')[0].firstChild.data;
Then we will get the list of users and add each user to the unordered list inside the user fieldset.
users = new Array(xmlhttp.responseXML.getElementsByTagName('user').length);
document.getElementById("user_count").innerHTML = users.length.toString();
var userList = document.createElement("ul");
for (var i = 0; i < users.length; i++) {
var user = xmlhttp.responseXML.getElementsByTagName('user')[i];
var username = user.getElementsByTagName("username")[0].firstChild.data;
var userItem = document.createElement("li");
userItem.innerHTML = username;
userList.appendChild(userItem);
}
document.getElementById("users").getElementsByTagName("fieldset")[0].appendChild(userList);
After we get the users updated, lets parse out the messages and add them to our message list.
messages = new Array(xmlhttp.responseXML.getElementsByTagName('message').length);
var messageList = document.getElementById("message_list");
for (var i = 0; i < messages.length; i++) {
var message = xmlhttp.responseXML.getElementsByTagName('message')[i];
var author = message.getElementsByTagName("author")[0].firstChild.data;
var content = message.getElementsByTagName("content")[0].firstChild.data;
var messageItem = document.createElement("li");
messageItem.innerHTML = "<strong>"+author+":</strong> "+content;
messageList.appendChild(messageItem);
}
The process of extracting the current chatroom state from XML and updating the DOM is one thats going to happen quite a bit, so lets move it into a method called updateDOM that takes the value of xmlhttp.responseXML as a parameter. Now lets move on and start building our synchronization function. We will basically just send a request and call updateDOM with the response every five seconds.
function synch(){
xmlhttp = getXmlHttp();
xmlhttp.open('POST', 'chat.php', true);
// Send the POST request
xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xmlhttp.send('userId=' + username);
xmlhttp.onreadystatechange = function(){
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
if (xmlhttp.responseXML != null) {
updateDOM(xmlhttp.responseXML);
}else{
alert("you were not logged in!");
}
}
}
}
updateDOM(xmlhttp.responseXML);
setTimeout('synch()',3000);
}
Be sure to modify your init method to call synch after you recieve the initial response. Then get started on the send method, which will be called when our message form is submit. This method needs to take the value from the message text field, make sure it's not blank, and post it to the server. Immediately after we send the request we will clear the form.
function send(e){
e.preventDefault();
var message = document.getElementById("message").value;
if(message == null || message == ""){
alert("You must enter a message!");
return;
}
xmlhttp = getXmlHttp();
xmlhttp.open('POST', 'chat.php', true);
// Send the POST request
xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xmlhttp.send('userId=' + currentUser + '&message=' + message);
xmlhttp.onreadystatechange = function(){
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
if (xmlhttp.responseXML != null) {
updateDOM(xmlhttp.responseXML);
}else{
alert("Message could not be sent!");
}
}
}
}
document.getElementById("message").value = "";
}
The final javascript function we ned to write is the logout function. This function will send a post request to chat.php just like the others, the only difference will be that this request will have logout=true.
function logout(){
xmlhttp = getXmlHttp();
xmlhttp.open('POST', 'chat.php', true);
xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xmlhttp.send('userId=' + currentUser + '&logout=true');
xmlhttp.onreadystatechange = function(){
//we can't really do anything except hope it worked
}
}
Now lets create a function to wire up all the events call it at the end of init.
function bindEvents(){
addEvent(document.getElementById("form"),"submit",send,false);
addEvent(window,"unload",logout,false);
}
We are almost done, but there's one more modification I'd like to make. Since we can't reliably count on the logout function always being called, lets modify chat.php so that it clears "stale" users with every request. We will consider any user that hasn't been updated in the past day to be "stale".
$yesterday = date("Y-m-d H:i:s",strtotime("yesterday"));
$psClearStaleUsers = $dbh->prepare("UPDATE `users` SET `isActive` = 0 WHERE `dateUpdated` <= ?");
$psClearStaleUsers->execute(array($yesterday));
And there you have it! Your own responsive chat-room that you built using just the bare essentials, congratulations! With just a tiny bit of CSS you can make your chat-room look like this (if you're interested in that CSS it's in the example code zip).