Thursday, July 23, 2009

Simple PHP socket-based terminal chat

Some time ago I decided it would be interesting to write an extremely simple PHP terminal chat to try out PHP's sockets and the socket_select() function usage. If you're not in to reading, feel free to skip to the samples and/or the sources ;)

What is a socket_select() (or *_select() for that matter) you ask? In general a select() function takes a list of resources (files, sockets, etc) and waits for at least one of them to be available for reading (or writing), and when that happens it lets you know which resources from the lists you've passed are ready for action. This is extremely helpful when dealing with sockets, because they - unlike local files for example - aren't as ready to be read or written to because of net lag and such. Why am I telling you this? Because this function is crucial for writing a responsive real time chat in PHP.

Because of the fact that my first serious encounter with select() was in Communication Applications course - we coded in C - I naively believed that the socket_select() function in PHP will be glad to take file descriptors and sockets mixed just like the C select() function does. I was very wrong.

PHP actually has different (!!) selects for files and for sockets. A question arose in my head: WTF!? Doesn't that kinda beat the point of select if I can not actually monitor two different resource types?? Well, after some foruming and reading I've found out that you can spawn sockets in a file stream form, using the stream_socket_server() and the stream_socket_client() functions. Great! Now that we know some techy details, we can look at some code:

Server side setup


$PORT = 20222; //chat port
$ADDRESS = "localhost"; //adress
$ssock; //server socket
$csock; //chat socket
$uin; //user input file descriptor

$ssock = stream_socket_server("tcp://$ADDRESS:$PORT"); //creating the server sock

echo "Waiting for client...\n";
$csock = stream_socket_accept($ssock); //waiting for the client to connect
//$csock will be used as the chat socket

echo "Connection established\n";

$uin = fopen("php://stdin", "r"); //opening a standart input file stream



What did we do?
In the first two lines we define the address and the port for the socket. Then we use the stream_socket_server() function to create a TCP server socket in stream form, so it can be used later by stream_socket_accept(). The $ssock stream is used only for waiting for an incoming call from a client, it's not actually used for the communication. The $csock is the actual chat socket we will use, it will be open as soon as a client initiates communication with the server. As soon as that happens we give the user a notification that we're connected and put the standart-input stream in to $uin so that we can take user input directly from command line.

Now for the code meat. The communication/select loop!

Server communication


$conOpen = true; //we run the read loop until other side closes connection
while($conOpen) { //the read loop

$r = array($csock, $uin); //file streams to select from
$w = NULL; //no streams to write to
$e = NULL; //no special stuff handling
$t = NULL; //no timeout for waiting

if(0 < stream_select($r, $w, $e, $t)) { //if select didn't throw an error
foreach($r as $i => $fd) { //checking every socket in list to see who's ready
if($fd == $uin) { //the stdin is ready for reading
$text = fgets($uin);
fwrite($csock, $text);
}
else { //the socket is ready for reading
$text = fgets($csock);
if($text == "") { //a 0 length string is read -> connection closed
echo "Connection closed by peer\n";
$conOpen = false;
fclose($csock);
break;
}
echo "[Client says] " .$text;
}
}
}
}



What are we doing? The while loop will run umm.. while connection with the other side is open (if we got this far in the code it is already open).
Now what are the $r, $w, $e and $t variables? Understanding this part is 80% of understanding select().
  • The $r is an array of streams (sockets, files, etc) we want to read from. As soon as one of them becomes available for reading, select() returns.
  • The $w is the same as the $r only it will wait for streams to become available for writing instead of reading.
  • The $e is for high-priority in-bound communication (not interesting atm)
  • The $t variable tells the stream_select() function for how long should it wait for a change of status of any of the streams passed to it in the various arrays. We don't want a timeout (ie we don't mind waiting forever), that's why we set it to NULL. Btw, setting $t to 0 will only check once and return immediately.
A most important thing to know about the select() family is that it changes the arrays you pass to it! And leaves in the array only the streams that has actually changed status. What does it mean? Well, two things in general:
  1. Save your streams in other place, so you don't lose them after passing them to select().
  2. Re-initialize your $r, $w, and $e arrays every time you call select().
Back to our issue, when the stream_select() function returns with values greater than 0, we know that we have a stream we can read from! And that's exactly what we are doing in the foreach loop, we iterate over the changed array of streams and read from each one of the available streams. If it's the socket stream, we read the data and put it on the screen, if it's the user input, we send it to the socket.
Another thing to note: when connection on the other side closes, the socket appears to be readable, but, we can only read 0 bytes (empty string). This is how we know the connection was closed.

Now let's take a look at the client setup


$PORT = 20222; //chat port
$ADDRESS = "localhost"; //adress

$sock = stream_socket_client("tcp://$ADDRESS:$PORT"); //creating the client socket

echo "connection established\n";

$uin = fopen("php://stdin", "r"); //opening a standart input file stream



Looks pretty much like the server setup, except for the fact that we don't need two sockets. We use the same socket for connection and communication. The communication loop of the client is almost identical to that of the server so I'm not going to cover it. You can get both sources at the bottom of the post.

Running the chat
Open two terminals, one for server and one for client.
First run server.php (I'm on Linux):

$> php server.php

The server now waits for an incoming connection

then go to your other terminal and run client.php:

$> php client.php

Both programs should notify you that connection has been established, now type something in any of the terminals to test communication :)

Server:


Client:



You can find the sources here. Questions, suggestions, criticism and love letters are welcome!

9 comments:

  1. good example
    Thanks

    ReplyDelete
  2. Hello friend, very good work I used the example in php and it worked just fine
    more and can be so every time the client writes something already appears on the server, and used in the same web socket, congratulations on your work:)

    www.obsidiann.com
    kakaroto

    ReplyDelete
  3. nice .urgently need one so i didnt bother to code. will create a better one and give credit to you

    ReplyDelete
  4. Can this example be extended for one to one user chat?

    ReplyDelete
  5. @Shashi I'm not sure how the code given above is different from what you refer to as "one to one user chat", but I'm pretty sure it can be extended to most kind of chats. Please supply more details, so I can help more.

    ReplyDelete
  6. it is not working ... if i write anything on either side , it does not appears immediately on the other side . it appears when you write something on the other side.

    ReplyDelete
    Replies
    1. Hey Siddharth, I assume you're running this on Windows? I tested it on Linux and it works as expected, testing on Windows though gave the results you've described. I assume this is because Windows treat file descriptors and sockets differently than Linux.
      Anyway, if you fix it, please post back :)

      Delete