Ajax Chat

A long time ago I created an Ajax based chat system as an example for someone on IRC. I recently found the scripts again so I cleaned them up a bit and thought I'd publish them along with a little write up about how it works. If you'd like to try it out, I have a demo available.

Overview

The system is based on a long-polling ajax request and a separate PHP based server that handles all the client connections. Long-polling allows us to establish a connection to the chat server that will remain open until new data is available. Once data is available the server will send it and close the connection.

The chat server is a small script that listens for new client connections and simply stores the messages in an array. It's not a good design for a long-running or busy server as you'd quickly hit the memory limit, but it is simple so it makes a good example.

Whenever a new message is received from a client the server also forwards the new message to all other client's that are connected to the server. Each client will take the new message and display it on screen then re-connect and wait for another message.

When a client wants to send a message the current polling request is canceled and a new request is sent with the message information.

The code

The code is split into several files, one for each class. All the code is available for download. You can also view the files individually using the links below.

ChatClient.php
This class represents a single client that is connected to the server.
ChatServer.php
This class handles client connections, stores messages sent by clients, and forwards them to other clients.
ChatUser.php
This class represents a chat user. It holds user-specific details like their nick name and last message received.
IHTTPRouter.php
A simple interface for routing http requests.
HTTPClient.php
This class is just a helper that aids with processing a HTTP request.
ISelectable.php
An interface for classes that make use of the StreamSelector class.
StreamSelector.php
This class allows the server to handle multiple simultaneous clients by using stream_select to know when any particular client needs attention.
StreamSocket.php
This class is a small helper to make working with sockets easier.
StreamSocketClient.php
This class represents a client connection and handles the reading/writing of data across the socket
StreamSocketServer.php
This class represents a server socket that can accept new incoming connections
phpchatd.php
This is the main script where everything is tied together.

Working with sockets

The primary reason I wrote this was to demonstrate how to handle several socket connections with PHP by utilizing the stream_select function.

I decided to implement this using a dedicated class that accepts socket resources and monitors them using stream_select. Classes that wish to make use of the monitoring implement the ISelectable interface.

Each socket you want to monitor is added to the selector class by calling the addStream method. Doing the actual monitoring involves calling the select method in a loop within your script. Each time this method is called it will assemble the lists of sockets to monitor and call the stream_select function.

Stream select monitors sockets for three possible conditions:

  1. Read availability
  2. Write availability
  3. Out-of-band data availability

Read availability means that the socket has data available and a call to fread will return immediately. Alternatively if you are using a listening socket then read availability indicates an incoming connection is available and a call to stream_socket_accept will return immediately.

Write availability means that there is spare room in the sockets write buffer for new data and a call to fwrite will return immediately.

Out-of-band data is data sent with a special out-of-band flag. Out-of-band is rarely used in my experience.

To monitor for each of those conditions on a socket you add it to an array and pass the array to the appropriate parameter in stream_select. Once stream_select returns it will have modified the arrays you gave it so they only contain sockets that meet the condition.

You then loop over the now modified arrays and process them. Here I'm just calling a function on the original object passed in so that the class can process the socket appropriately. Generally this means reading the data into a buffer or writing the data from a buffer to the socket.

Generally you want to read/write into buffers rather that process data directly. When reading or writing to a socket it is possible that not all data is read or written in a single call, particularly when the socket is in non-blocking mode. By using an intermediate buffer you can ensure that no data is lost due to incomplete reads or writes.

The main script

The main PHP script is designed to be run from the command line. This is not something that you should run through your browser by visiting a URL. While it may actually work via a web server, it complicates the process and makes failure more likely.

This script starts by creating a new server socket to listen for connections. You may configure the bind address and port to whatever values you wish. The address 0.0.0.0 is a special address that tells the socket to listen on all available interfaces. You may alternatively specify an IP address and the socket will then only be reachable via that specific address. If you specify an address it must be an address that is configured on one of the computers network interfaces. Check the output of ifconfig on linux (ipconfig on windows) to see what addresses are available.

The server class allows us to be notified when a new connection is made via the onAccept function. We use that to add the newly connected socket to the selector class.

Finally we just enter an a loop that will constantly wait for data by calling the select method of the selector object. The loop will exit if at some point all sockets are removed from the selector object.

In this particular implementation I never got around to adding the ability to remove sockets so the loop is essentially infinite. I'll leave fixing that as an exercise to the reader.