Why would you want a realtime chat/forum? Well, what if you’re at work, and the AIM port is blocked? Or you want to have a chat with several people, but not all of them are on the same IM network, or one of them has an old version of the client software that doesn’t support group chat? What if you’re reading a forum, and would never know if there have been new posts made since the last time you loaded the page? In any of these cases, it would be nice to have a program that will get around those problems. Graffiti is one such program.
Adding a new post appends it to the bottom of your page and sends it off to the server. In a standard client-server situation, it would be the server’s responsibility to send that post to every connected user. However, HTTP is limited, in that the server cannot “push” data down to clients. The clients must “pull” it from the server. How can we simulate a “push” when we can only “pull”?
By polling for new content. Each client will sit in a loop and check repeatedly for changes to the state of the program. In the interest of not killing the server, we have the clients checking once per second for new content. If there is no new content, the server will say so. If there is new content, the server will send it. This way, everyone who has the page open will be looking at exactly the same page, regardless of who edits it and when.
The first thing we’ll do is design the database. There aren’t a lot of demands for our data, so it won’t be too much of a problem. I’m using SQLite as my database, but this is SQL 92 compliant code, so you should be able to use it (or very easily port it) to whatever database you prefer.
Our database will have 3 tables and 3 triggers. The first table is the “entries” table, which holds all the posts that are displayed on the page. It needs a field for the unique id, the author, the content of the post, and the time the post was made. The other two tables are the “instracker” and “deltracker” tables, which are to keep track of the posts that have been made and deleted over the course of the program running. These tables help keep everything sync’d up, by giving the client a place to look when they need to know if anything has changed since the last update.
To use this code in SQLite, just open up your SQLite command line interface, and type these lines in verbatim. Other databases will obviously have different ways of doing it, so you’re on your own there.
% /usr/local/bin/sqlite graffiti.db
Now let’s put our triggers in. We have one trigger that will put the date into the entries table, so that each new post has an exact date that goes along with it. The other two triggers update the instracker and deltracker tables whenever a post has been added or deleted.
You can add these triggers to your SQLite database from the command line.
% /usr/local/bin/sqlite graffiti.db < triggers.sql
Note that there is a member in our class for each field in the table.
Now that our data is all set up and ready, let’s define the interface. It’s not very complicated, but we will have to modify the page at runtime quite a bit.
The page itself doesn’t need much in the way of code. We simply have two div’s, one of which will hold the list of all the posts, and the other will be where the user can add new posts.
The CSS to style this program is equally simple. We just define classes for an entry, author, entrypanel, and the bottom. The entrypanel is where the delete button for each post will go, and the bottom is where the content of the post will be displayed.
Now that we have the basic interface scoped out a little, what do we want it to do? The button in the add_div will open the Add Post region of the page at the bottom, below all the posts. The user types his name and a message, and clicks the submit button. The post shows up immediately on his page, and is sent off to the server. When someone else posts a response, it will automatically show up at the bottom of the list. If he decides he doesn’t like what he wrote, he can hit the delete button on his post, and it will disappear from everyone’s page.
Communication between the application and the database is enabled by the XMLHttpRequest object. Originally developed by Microsoft and implemented by the other browsers, this allows the browser to request and receive data from a server while the page is loaded, and the browser can do something with said data without refreshing the page. XMLHttpRequest can work synchronously or asynchronously, which means that you can define whether a request should stop the application from running while it waits for data, or if it should continue on its business even if it is waiting for a response from the server. We’re going to use both in this program.
You’ve probably heard the hype already, and I won’t go over it any more. Now … have you actually used the XMLHttpRequest object? Naturally, Microsoft does it a bit differently from everyone else. We’ll use a function that detects whether the browser supports Microsoft’s version or the Mozilla version (Safari and Opera use the Mozilla version), and returns the proper object.
You can do several things with the XMLHTTP object, but you only actually need a few of them. After you get an object from xmlhttp(), you’ll open it, set its header (needed on some browsers for some types of requests), and send a parameter string. When you open it, you define the action type (GET or POST), the URL to which to send the request, and whether the request should be processed asynchronously or not. If you’re using POST, you have to set the request header, otherwise, you don’t. If you’re using asynchronous, you have to define a callback function. Otherwise, you don’t.
Before I get too far into this and forget to set you up on PHP/SQLite, you need to create a PHP file that can talk to your database. For an application using SQLite and JSON, as we are, the top of your PHP file will look like this:
This tells PHP to use the object-oriented version of its SQLite adaptor, which we will find very convenient when we need to transfer data. The way JSON works is that it packages up an object, and since our Entry class exactly mirrors our entries table, PHP will see the exact same class structure coming out of the database and the application.
The first thing we need to do when we load the page is load all the entries that already exist in the database. We want this to be a synchronous request, because the program should not run until all the posts have been successfully loaded and put onto the page.
In case you’re wondering what that “load=1” business is, you’re about to find out. That’s the parameter string for the request. If you were loading this in a browser window, that would follow the “?” in the URL. It is also known as a query string. In this case, we’re sending a message to our PHP page that tells it to return an array of Entry objects.
This sends a query to the database, requesting everything from the entries table. It puts each row in the database into its own object, and puts all those objects into an array, which it returns to the application.
You may also have noticed that for each post that is in the database, it calls a function called “buildEntry”. What that function does is simple: it builds an entry and puts it onto the page. There are two ways to put something onto the page; one way is to use an element’s innerHTML property to simply place HTML into an object and have the browser render it. The other way is to build elements using the DOM’s createElement and appendChild functions. For the buildEntry function, we’ll be using the latter.
We create the div for the entry, then its author span, its entrypanel span, and its bottom div, populate all of them with the proper content, and put it into the page by appending it to the outer div (defined in the HTML).
Now that we can load all the pre-existing entries, we need to be able to add our own. The single button isn’t really going to allow that, so we need a function that will expand the add_div div so that new posts can be entered. openAdd() uses the innerHTML property to build its HTML.
If you can open it, you should be able to close it, so we have a corresponding closeAdd() function.
As you see above, the openAdd function calls addEntry() when its button is clicked. We now need to write that function. We get the values of the author and data fields in the add_div, and send it off to the PHP page using XMLHttpRequest. For this we want to use asynchronous processing, so that the page doesn’t get slowed down when we click the Save button. This function also updates the global mostRecent variable, which is always the id number of the post most recently added to the page.
Our PHP now has to be updated to support this. We build our query from the passed Entry object, ship it off to the database, and (this part is kind of tricky) send another request to the database to get the entire object that was just inserted. We return this object to the application. The reason we do this is because we want to get the time and id number of the post, which are only known after it has been inserted. Notice above that the addEntry function is designed for this.
Now that we can add posts, we need to be able to delete them. We wouldn’t want our page to get cluttered up with useless crap (especially when we’re testing it out, right?). The delete button on each post calls the delEntry function. This sends a request to delete an entry to the PHP. We use asynchronous processing here because we don’t want to block the rest of the program, but it doesn’t need a callback because nothing is returned from a delete request.
The PHP code to handle a deletion is probably the simplest thing you’ll have to do. It just takes the id number you sent it from the application and deletes it from the database.
Once the post has been deleted from the database, we have to remove it from the page. We do this by calling the removeEntry function. This updates the global deletions array (which is supposed to match the deltracker table) and removes the post from the page using the DOM removeChild function.
Keeping the different clients sync’d with the database and each other is the hardest part of this application. We’ll do it with a check to the database once per second for each client (the timer uses a global timerID variable). We send the most recently added entry id to the database, and it responds with a carefully crafted array. There are always two items in this array. The first is an array of new entries added after the one most recently added by this client (this will often be empty). The second is another array of entry id numbers that have been deleted. This mirrors the deltracker function.
The PHP for this is fairly complicated. It has to create the three arrays I mentioned above, check the instracker table for new entries, get all their id numbers, get those posts from the entries table, and put them into their positions in the arrays. It then needs to return the deltracker array.
When it gets the data back from the database, the program now has to make sure everything is sync’d. It loops through the new entries and builds each one. It also needs to take the deltracker array (the second item in the returned array) and merge it with the deletions array (global). The mergeDeletions function simply loops through the given array, starting at the index that would be past the end of the deletions array, and removes the entries specified.
We’re almost done now. The only thing left is to write the init function, which will run when we load the page. It needs to initialize the global deletions array, load the existing posts, and then start checking for new ones.
I mentioned the three global variables we needed as we went along. They are defined here:
- Make it look nicer. It’s either a forum or a chat, each of which have their own display requirements.
- New posts come in at the bottom of the page. This isn’t always what you want. So update the program so it prepends the new posts (puts them at the top). And make it an option!
- Think about users … what will have to be done to this program to create a login system so that only the author of a post can delete it?
- Dynamically change the timeout interval based on usage to maximize efficiency.