Writing a JavaScript/PHP Chat Server

Disclaimer

This tutorial was written in 2003, which is truly prehistoric in internet
terms. If you're looking to write a modern chat server that works in all
browsers, here are a couple considerations:

Don't write your own, just adapt an existing one. There are plenty of good
free (and bad) ones out there. A quick google search reveals lots of things
like this.

If you are intent on writing your own, don't follow this guide (unless you
are a time traveler from 10 years ago, or a masochist).
There are nicer ways of implementing the same functionality...

Front-End

Iframe polling is silly in this day and age, you should use
XHR
instead (if you're using
jQuery,
that's the reliable old
$.ajax).
This has been available in every browser for years.

If you want to be bleeding edge, try using Web Sockets
instead of AJAX polling, though that will require server support...

Back-End

Apache+PHP+MySQL is not the best architecture for a
realtime chat application. If I were writing one today, I'd probably use
web sockets with an event-based back-end (e.g. eventmachine, node.js, etc.).
I'd might just forgo the DB and keep stuff in memory, though if you had to scale
across multiple app servers, etc., Redis
might come in handy.

If you still really want to go the LAMP+iframe route, that's cool and all, but
note that I probably won't respond to questions about it or be able to offer
you any debugging tips.

—Jon
2012-06-12

created 2003-12-19 by jonlast updated 2006-08-16 by jon

Introduction

Table of Contents

Internet Chat is one of the most efficient forms of long-distance communication. It makes it possible to interact with
people across the globe in real-time at virtually no cost to the end-user. Whether it's used to check up on the folks,
get answers from a customer service rep, or talk to potential clients, Internet Chat is an invaluable tool for
commercial and home users alike.

In this article we will build a basic chat application, jenChat, which can be customized for your needs.
In a general sense, we will learn how to implement remote scripting with just a little DHTML.

Creating a chat application from
scratch may seem like a daunting task, but it's really quite simple when you think about it. On the most basic level,
all a chat server really does is take messages submitted by a participant and push them out to everyone else.
Everything else from emoticons to buddy lists are merely extensions of this and will be left as an exercise to the
reader.

This article assumes a working knowledge of PHP,
MySQL and JavaScript,
but novice programmers should be able to follow along without too much difficulty. If you are planning on implementing a production chat server, refer to the
additional notes for a couple pointers and considerations.

What you'll need

An hour of free time

Access to a MySQL database and a web server running PHP

Two or more web browsers for testing

jenChat will be powered by PHP and MySQL, but we could just as easily write it using ASP and MS SQL. In fact,
most of the work is done client-side via JavaScript, and even that is kept to a bare minimum. See the
end product to get an idea of what we'll be making. If you want to skip the tutorial and start
customizing jenChat immediately, you can go straight to the code.

A few ground rules

Before we get started, let's establish some requirements for our chat server. There's nothing worse than having to
start over because you didn't plan fully and efficiently.

The chat application should work across all major platforms.

There should be no flickering in the chat window (in other words, we shouldn't merely be refreshing a page)

It should be scalable—there should be no theoretical upper limit on the number of participants. (see the note on scalability)

It should use minimal bandwidth—communication between client and server should be kept to a minimum.

If a user does navigate away from the chat window, his/her session will end automatically.

Let's focus briefly on the first point: compatibility. If you wish to code for Netscape 4,
feel free, but in this article we will only focus on modern browsers that support the W3C DOM, such as Internet
Explorer 5+, Opera and Mozilla. It is the opinion of the author that there are enough options available on all
platforms that users should not confine themselves to obsolete browsers.

The (X)HTML

In programming, it's generally a good idea to design the basic UI before implementing the functionality. Incidentally,
this will help us tackle the biggest problem right off the bat—the issue of page reloads/flickering. Normally
when a user wants the most up-to-date version of a page, he/she must send a new request to the server, which causes
the page to refresh. By the same token, when a user submits information via a form, the server sends back a page,
which in most browsers causes a flicker as it is loaded. The solution to both of these problems is a hidden IFRAME.
Let's set up a basic page to illustrate how this can be done:

Save this page as "chat.php". This will be the backbone of our chat application. Let's look at what's there.

First, notice the "chatContents" IFRAME, where the chat messages
will appear. The page we load in the IFRAME is a simple XHTML document with a div for the
contents of the chat. As we will see later, because one of our goals is to avoid any flickering, we will never
actually reload the contents of this IFRAME. Rather, we will programmatically append messages to it. So why use an
IFRAME at all? Why not just use a div? The reason is that there is no W3C standard for programmatically scrolling an
element. But virtually all modern browsers provide a means of causing an IFRAME to scroll a specified amount, and so
we use one here.

Following the chat contents is a simple form with an input and a submit button. Notice something interesting—the
target of the form is "post". This means that when we submit this form, data will be submitted via the IFRAME named
"post" rather than the main window. Because of this, the contents of the IFRAME will reload each time we send a
message, but the main page will not. The second IFRAME will be the all-purpose worker frame; rather than reload the
current page to get new messages, we will use "thread" instead. This will help us create the illusion that the server
is pushing content to the browser.

The Database

The next step is to set up our database so that we have a central location for all the messages of the chat. We will
set up two tables—one for participants and another for messages. The participants table will have just three
columns—a unique id, a username/handle and a timestamp of when the user last communicated with the server. The
messages table will store the id of the participant that submitted it, the message itself, a timestamp of when it was
sent, and a unique id. This is the SQL used to create the tables:

With regards to database performance, see the note on scalability for ways to make
jenChat as fast as possible, such as using a MySQL-based session handler and MEMORY (HEAP) tables.

A note about our implementation: if you plan on integrating this into an existing web application that already tracks
users, you should probably use your existing table, as well as make other necessary changes. The reason is that in our
chat server, participants only exist for as long as they are in the chat room—we will delete participants once
they have exited. By the same token, we also delete messages shortly after they are received by the server. This will
help keep the size of the database to a bare minimum, even if there is a significant amount of activity. Your
application may have different needs/requirements, so some things we do in this example may not be fully applicable.

The PHP

First, we will create an include file that does a few important things for us, like initialize our session and
database connections. Change the appropriate database information and save this file as "init.php".
Be sure to read the comments and links in this file.

<?php
session_start();
/*
* replace the parameters used here with the appropriate information
* for your system.
*/
$dbhandle = mysql_connect("server","user","password");
mysql_select_db("database_name");
/*
* IMPORTANT: magic quotes are bad. Ideally, you should turn them off
* in your php.ini, but if you are unable to, the code below will fix
* the $_POST array for you.
*
* See http://www.php.net/manual/en/security.magicquotes.php
*
* If you aren't using prepared statements (mysqli, Pear:DB) or manually
* escaping every variable that goes into a query, you are asking to get
* pwned. For maximum portability, jenChat uses mysql_real_escape_string,
* but prepared statements are generally the way to go.
*
* If you didn't understand that last paragraph (or even if you
* did), read up on SQL Injection and why you need to worry about it.
*
* http://www.unixwiz.net/techtips/sql-injection.html
*
* OK, carry on
*/if(get_magic_quotes_gpc()){
$_POST = array_map('stripslash', $_POST);
}
functionstripslash($value){
if(is_array($value))
returnarray_map('stripslash', $value);
elsereturnstripslashes($value);
}
?>

Because this tutorial is an exercise in remote scripting and not in creating user logins and so forth, I have provided
a simple login script for use with the chat server. Whenever someone wants to join the chat,
this script creates a new participant (if the requested handle is available) and then redirects the user to the chat
page. Download the file and save it as "login.php".

Our next task is to store messages in the database as soon as they are sent. Let's create "post.php", the first of our
two IFRAMEs. This script will actually serve an additional purpose—garbage collection. It deletes any messages
that are over 30 seconds old, as well as any inactive participants. Inactive does not mean they are simply
lurking—it means their browser is no longer communicating with the chat server, e.g. they closed the browser,
navigated elsewhere, etc. Here is the basic source code:

You may wonder why we redirect to the same page, especially considering that the page has no content. While this does
impose an additional hit on the server every time a message is posted, it helps avoid navigation woes that would
otherwise be an issue. Normally, when you post to a URL several times in succession, it will be added to your
browser's history each time. This is true of IFRAMEs as well as normal windows, and in this case would make the "Back"
button useless. But if you simply request the same URL multiple times in a row via GET, it is only added to your
history the very first time (IE 5.0 Win and Mozilla 0.6 are the exceptions). By sending this header to browsers, the
history problem is history.

Next we will create our script that will poll the server for new messages, namely "thread.php". All this script needs
to do is grab any new messages that did not exist the last time it talked to the server.

Now every time we refresh this page, if there are any new messages, they will be put in divs with the corresponding
handle.

Our last bit of PHP goes in "chat.php". When they first view the page, we need verify that they are actually in the
chat room (logged in). The first "if" statement verifies that they are in fact authenticated, and the second one
verifies their session hasn't timed out. Insert the following code at the beginning of your document:

The JavaScript

The final step is to tie our pages together via JavaScript. Our remaining tasks are to cause "thread.php" to
auto-refresh at regular intervals, to append its contents into our chatContents area in "chat.php", and to
auto-scroll chatContents as it's updated.

Because the messages are being submitted via "post.php", the contents of the form field are never cleared
automatically. Every time "post.php" reloads, this script will call a function in the parent frame to reset the text
field and give it focus. The following goes immediately after the PHP in "post.php":

This next snippet of code should go into "thread.php" right before the closing body tag. Each time the page loads,
this will call a function in the parent to notify it if there are new messages from the server. It will then schedule
a refresh to poll the server again in one second (or the interval of your choice).

The last chunk of code goes into "chat.php" in the document head. This is where it all comes together. To start off,
the function chat_init is called when the page loads. This sets up cDocument and cWindow—references to the
document and window objects of our chat area.

As you'll remember from above, once "thread.php" has loaded, the getMessages function
passes any new messages to insertMessages, which in turn will append them to the chatContents area.
After appending the messages, we scroll to the bottom of the IFRAME.

Finally, as explained earlier, the function resetForm simply resets the text field and gives it focus each time a message is sent. Here is the code for "chat.php":

Conclusion

And with that, we're done. Go ahead and give it a spin in your favorite browser(s).
In case you think you may have missed something, you can view/download the individual
files below, or download them all in a zipped archive (jenChat.zip).
If you'd like to see a demo, try this slightly optimized version.

Above and beyond

Now that wasn't so bad—we created a working chat application in no time at all.
While it's true that jenChat is lacking in features, its simplicity makes it very easy to customize.
Inventive coders should have no trouble adding things such as rooms, buddy lists, emoticons, and other features that
help make chat such a powerful tool and pleasant waste of time.

Questions? Corrections? Suggestions? Email me. Feel free to use these
scripts throughout your site with or without modification or acknowledgement, although a link to me is
always appreciated. Email me if you would like permission to reproduce this article.

Every time a client polls the server
there are two database queries, as well as a read and write of the PHP session data. That's on top of everything
Apache/PHP does to read/parse/execute the PHP script. We're talking multiple reads/writes to multiple files for every
user every second. With a significant number of users,
your hard disk will have a hard time keeping up. So by moving the sessions to the database, and the database to memory,
the slowdown is greatly reduced.

Using in-memory tables, I found that 25 concurrent users accessing a P4 1.4GHz machine over a
LAN cost Apache about 4 MB of memory and kept CPU utilization right around 20%. YMMV. Note that you can only use AUTO_INCREMENT
columns on MEMORY tables as of MySQL 4.1, so if you have an earlier version you should either upgrade or rethink your database
schema.

Here are some other ideas you may want to try to speed it up even further:

Use a PHP accelerator/optimizer, such as APC or
eAccelerator. This should dramatically decrease CPU utilization, since PHP won't have to re-read/parse the PHP script every time.

Go one step beyond the MySQL-based session handler and don't use PHP sessions ($_SESSION) at all. Instead, store the
session cookie
and any session vars you need in the jenChat_Users table. Then you can limit session reads/writes to when you actually need
them, and can avoid serialization/unserialization of session data with every request.

Only do periodic database garbage collection (as opposed to every time a message is posted).

Bandwidth

If you follow the suggestions above, your next bottleneck will probably be bandwidth.
jenChat is designed to use as little bandwidth as possible; how much you need is
directly proportional to
the number of concurrent users. You may consider stripping out extraneous markup from the polling responses, though
may not make much of a dent, seeing as the number of packets sent/received will not change significantly.

As a baseline, consider that there will be two TCP packets per user per second (polling request
and response). On top of that you will also have the packets sent in posting messages to the server. So in just
about any scenario, if you anticipate 4 packets (max 512 bytes each) per user per second, you should be able to
determine your user/bandwidth limits. Remember that these numbers include both directions, with the outbound packets
being the larger ones. So if you are only concerned about outgoing bandwidth, you will be more than covered if you
plan on needing 1K per user per second.

If bandwidth is an issue, you could always increase the polling interval, but that would obviously affect the responsiveness.

Responsiveness

Because jenChat polls the server once a second, you may notice a delay between when you post a message and when you
actually see it. It is very easy to modify the code so that it appears instantaneous, just as other chat applications
do. The first modification is to append the message to the contents via JavaScript each time you hit send. The second
is to alter the SQL query that polls the server to ignore messages from the user requesting them, since they are
already being appended to the contents.

AJAX

Many people have requested/suggested I rewrite jenChat using AJAX.
While I use XMLHTTPRequest for most of my
remote scripting nowadays, I have been reluctant to update jenChat for the following reasons: I want to keep jenChat
simple and have it support as many browsers as possible. Changing it to an AJAX-only script would exclude older
versions of all currently supported browsers. I could make it use AJAX or IFRAMEs depending on browser support, but
that would further complicate the code and tutorial. From a server/bandwidth point of view, AJAX would not make it run
any more efficiently; after all, the same amount of data would be transmitted with each request.

However, AJAX does bring some significant benefits to the client. It eliminates the loading/status indicators that get
triggered with each reload, and it requires less CPU and memory than an IFRAME.

AdBlock

If you use Mozilla and have AdBlock installed, remember that sloppiness in setting up your regular expressions and
wildcards can cause unexpected results. jenChat stopped working for me in Firefox one day because I had set up
an over-inclusive regexp filter that blocked anything ending with "ad", effectively disabling thread.php.

Compatibility

IE 5.0+ (Windows)

IE 5.3+ (Mac)

Mozilla 0.6+ (All)

Opera 7.0+ (All)

Konqueror 3.1+ (All)

Safari (all)

If you have tested jenChat on any other browsers/platforms, please let me know if/how it worked.

Mozilla 0.6+ includes Netscape 6+, Phoenix, Firebird, Firefox, and just about any Gecko-based browser.