PHP Socket server and Chat Gateway for Flash clients

First of all, I don’t plan to write a step-by-step manual, „PHP Socket server and Chat Gateway for Flash clients in 10 minutes” or something like that.

We’ve just finished the developement of a Flash chat solution, that uses PHP as Backend and Chat Gateway. I’ll present pieces of the code, and some tricky solutions, for example how to communicate with flash on 80 port.

I won’t present the Flash part of the story, because it was done by a collegue and a friend of mine, and I won’t release his code.

For a starting point, you can check out kirupa.com – PHP 5 Sockets with Flash 8 tutorial, as we did.

So in this article I will show our solution of a chat server in php 5, for flash clients, and a webserver emulation for policy-file-request and crossdomain.xml request, and xml-socket based communication. This example only shows how to create a multiuser chat, with only one-to-one communcation, so you can have several conversations, but no „chat rooms”.

This will be a command line (CLI) PHP of course, so we won’t need a webserver for that.

So, we need to create a daemonized php, without execution time limit. So it could loop until the end of time, or the next reboot 🙂 And also we set the ip address, and the port to listen on.

#!/usr/bin/php -q
<?php
 
set_time_limit(0);
ob_implicit_flush();
 
$address = '127.0.0.1';
$port = 80;

Let’s create an array for the incoming connections data, (if you use it for chat, it would be useful to store nicknames in it.), and start creating and listening a socket. Please note, that everything I „echo” goes to a log file in our case.

 $_sockets = array();
 
if (($master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) < 0)
{
  echo "socket_create() failed, reason: " . socket_strerror($master) . "\n";
}
 
socket_set_option($master, SOL_SOCKET,SO_REUSEADDR, 1);
 
if (($ret = socket_bind($master, $address, $port)) < 0)
{
  echo "socket_bind() failed, reason: " . socket_strerror($ret) . "\n";
}
 
if (($ret = socket_listen($master, 5)) < 0)
{
  echo "socket_listen() failed, reason: " . socket_strerror($ret) . "\n";
}
else
{
  $started=time();
  echo "[".date('Y-m-d H:i:s')."] SERVER CREATED ( MAXCONN:".SOMAXCONN." ) \n";
  echo "[".date('Y-m-d H:i:s')."] Listening on ".$address.":".$port."\n";
}
 
$read_sockets = array($master);

SOMAXCONN is a kernel variable that sets how many socket connections can your box handle. In unix world it can be set at kernel compile time, or it can be adjusted throught sysctl.

After this we’ll create a persistent loop to handle requests.

while (true)
{
  $changed_sockets = $read_sockets;
  $num_changed_sockets = socket_select($changed_sockets, $write = NULL, $except = NULL, NULL);
 
  foreach($changed_sockets as $socket)
  {
    if ($socket == $master)
    {
	   if (($client = socket_accept($master)) < 0)
	   {
       echo "socket_accept() failed: reason: " . socket_strerror($msgsock) . "\n";
    	 continue;
	   }
	   else
	   {
	     array_push($read_sockets, $client);
	     echo "[".date('Y-m-d H:i:s')."] CONNECTED "."(".count($read_sockets)."/".SOMAXCONN.")\n";
	   }
    }
    else
    {
      $bytes = @socket_recv($socket, $buffer, 2048, 0);
      /*
 
      Here comes the core... ;)
 
      */
    }
}

So, these are the basics. Until this point, this code is almost the same as Raymond Fain’s socketShell.php mentioned above.
As I commented we’ll only edit the else part of the code.

So every code I quote now, will go inside this else part. Until we start defining functions. These functions will go outside, the loop.

First we’ll do the webserver emulation part. Why do we need this?
Because my plan is to communicate on port 80, because firewalls and routers won’t block communication on this. But the problem is that – as you might know – Flash can’t communicate under port 1024 by default, because of security issue.

But there is a way, to get flash to communicate on any port you want. This is called cross-domain-policy.
Crossdomain policy is a data, in XML format, that looks like this, in our case:
So I store it in a variable, and if my socket daemon gets a request from flash that looks like this:

<?xml version="1.0"?>
<cross-domain-policy>
  <allow-access-from domain="*" to-ports="80" />
</cross-domain-policy>

In the answer we’ll give back the crossdomain policy file, that means we allow connection to port 80.

If you want to know more about crossdomain policy click here.

So our php code after the socket_recv will look like this:

 
if (preg_match("/policy-file-request/i", $buffer) || preg_match("/crossdomain/i", $buffer))
{
  echo "[".date('Y-m-d H:i:s')."] CROSSDOMAIN.XML REQUEST\n";
  $contents='<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="*" to-ports="80" /></cross-domain-policy>';
 
  socket_write($socket,$contents);
  $contents="";
 
  $index = array_search($socket, $read_sockets);
  unset($read_sockets[$index]);
  socket_shutdown($socket, 2);
  socket_close($socket);
}

We close this socket, because flash request for that policy file. It closes the connection, and if it get’s a proper answer, it will reconnect, and that connection will be a true socket now.

After this flash thing, we’ll created a nice feature in our server. A server-status information. Which can be reached throught a web browser, by typing the IP address (and port if you don’t use 80) that the daemon is listening on, and /server-status.
Like http://127.0.0.1/server-status. And it will answer something like this:

OK
Clients: 48/128
Created: 2007-07-10 10:54:02
Uptime: 16 days

After this. I ignore favicon.ico requests, because if you check-out your server-status information from a browser it will automatically request it, and it will just create a piece of junk in the code.

And the last thing, I webserver emulation part, that we redirect every request coming to our server, that is a GET, POST or HTTP head request and not socket communication.
And we also log that request, and create a real HTTP redirect.

So let’s see what we should add to the exsiting code, to work that way.

elseif (( preg_match("/GET/", $buffer) || preg_match("/POST/", $buffer)) && preg_match("/HTTP/", $buffer))
{
  if (preg_match("//server-status/i", $buffer))
  {
    $uptime = floor((time()-$started)/86400);
 
    socket_write($socket,"OK\n");
    socket_write($socket,"Clients: ".count($read_sockets)."/".SOMAXCONN."\n");
    socket_write($socket,"Created: ".date('Y-m-d H:i:s',$started)."\n");
    socket_write($socket,"Uptime: ".$uptime." days\n");
    echo "[".date('Y-m-d H:i:s')."] STATUS REQUEST\n";
  }
  elseif (preg_match("/favicon.ico/i", $buffer))
  {
    //ignore :)
  }
  else
  {
    // fake web server
    socket_write($socket,"HTTP/1.1 301 Moved Permanently\n");
    socket_write($socket,"Server: PHP Chat Server by DjZoNe - http://djz.hu/\n");
    socket_write($socket,"Date: ".date("d, j M Y G:i:s T\n"));
    socket_write($socket,"Last-Modified: ".date("d, j M Y G:i:s T\n"));
    socket_write($socket,"Location: http://djz.hu/\n");
  }
  $index = array_search($socket, $read_sockets);
  unset($read_sockets[$index]);
  @socket_shutdown($socket, 2);
  @socket_close($socket);
}

Until now we did, the socket server starting, and the http request handling.
Now we do the last part, the quit part. I show what we do when the socket pipe is broken, the user left or something like that happens.

if (strlen($buffer) == 0)
{
  //we get the user's uniqe id from the database
  $id=$_sockets[intval($socket)]['nick'];
 
  $index = array_search($socket, $read_sockets);
 
  unset($read_sockets[$index]); // we clean up
  unset($_sockets[intval($socket)]); // we clean up our own data
  // cleaning up is essential when creating a daemon
  // we can't leave junk in the memory
  @socket_shutdown($socket, 2);
  @socket_close($socket);
 
  $allclients = $read_sockets; // reload active clients
 
  // $socket is now pointing to a dead resource id
  // but the send_Message() function will need it, I'll explain later
 
  send_Message($allclients, "<quit aid=\"".$aid."\" />");
  echo "[".date('Y-m-d H:i:s')."] QUIT ".$id."\n";
}

And now the real socket communication…

else
{
  $allclients = $read_sockets;
  array_shift($allclients);
 
  $piece = explode(" ",trim($buffer)); // we strip out all unwanted data
  $cmd = strtoupper($piece[0]);
}

We assume, that the a command looks like in IRC protocol.
MSG who message

We cut the content to pieces, and we glue again together when it is a message, otherwise we only need the first few arguments.

if (!empty($piece[1])) $content = $piece[1];
 
switch ($cmd)
{
  case "IDENTIFY":
    $id = trim($piece[1]);
    $passwd = trim($piece[2]);
    send_Identify($allclients, $socket, $id, $passwd);
  break;
 
  case "MSG":
    $id = trim($piece[1]);
    $msg="";
    foreach ($piece as $key=>$val)
    {
      if ($key > "1") $msg.=$val." ";
    }
    $msg = trim($msg);
    send_Msg($allclients, $socket, $id, $msg);
  break;
 
  case "LIST":
    list_Users($allclients, $socket);
    break;
}

We made the command triggering.
Until now we were in the loop.
From this point, we’ll create functions, outside.

I want to tell a story… Just joking 😉
But, we found out something. We did the socket_write, and everything, but in won’t get through the socket until we put an ASCII 0 (zero) at the end of the output buffer.
So we insert an ascii-0 code after every socket_write. I guess this is just beacause of flash xml-socket communication. If you look back to the „web server emulation” part, we just used newline.
So here are some functions, we use for authentication, message sendig and so on.

function send_Identify($allclients, $socket, $id, $passwd)
{
  global $_sockets;
  $nicks = array();
 
  $dbconf = new DATABASE_CONFIG;
 
  $db_host = $dbconf->host;
  $db_base = $dbconf->database;
  $db_login = $dbconf->login;
  $db_password = $dbconf->password;
 
  foreach ($_sockets as $_socket)
  {
    foreach ($_socket as $key=>$val)
    {
      if (empty($nicks[$val])) $nicks[$val]=1;
      else $nicks[$val]=$nicks[$val]+1;
    }
  }
 
  if (empty($nicks[$id]))
  {
    $s=1;
    //  Here will be a simple authentication.
 
    $link = mysql_connect($db_host, $db_login, $db_password);
    if (!$link) die("Could not connect:" . mysql_error() . "\n");
 
    $db_selected = mysql_select_db($db_base, $link);
    if (!$db_selected) die("Can't use $db_base :" . mysql_error() . "\n");
 
    $result = mysql_query("SELECT nick FROM members WHERE id='".intval($id)."' AND password='".crypt($passwd)."' AND active='1' LIMIT 1");
    $data = mysql_fetch_array($result);
    $name = $data['name'];
    $_sockets[intval($socket)]=array('id'=>$id, 'nick'=>$name);
 
    mysql_free_result($result);
    mysql_close($link);

It’s essential to use free and close on the sql connection.
Or I have to say you MUST close, otherwise the connection will timeout, and at the next connect the whole daemon will die.

  }
  else $s=0;
 
  //   We'll answer to the flash in XML form.
  //   But we receive in plain text format.
 
  if ($s == 1)
  {
    $out = "<identify aid=\"".$nick."\" name=\"".$name."\" />";
    send_Message($allclients, "<login aid=\"".$nick."\" name=\"".$name."\" />");
    // this goes to all active, identified clients
    echo "[".date('Y-m-d H:i:s')."] LOGIN ".$id."(".count($allclients)."/".SOMAXCONN.")\n";
  }
  else $out = "";
 
  socket_write($socket, $out.chr(0)); // write back to the client
}
 
function send_Msg($allclients,$socket,$id,$msg)
{
    global $_sockets;
 
    if (!empty($_sockets[intval($socket)]))
    {
        $nicks = array(); //amig fut a parancs ebben vannak a nickek.
 
        foreach ($_sockets as $_socket)
        {
             foreach ($_socket as $key=>$val)
             {
                  // this check's the onliners
                  if (empty($nicks[$val])) $nicks[$val]=1;
                  else $nicks[$val]=$nicks[$val]+1; // we shouldn't have duplicated nicks, but what if...
             }
        }
 
        foreach($allclients as $client)
        {
            if (!empty($_sockets[$client]['nick']) && ($_sockets[$client]['nick'] == $id))
            {
              $_client = $client;
              $out = "<msg aid=\"".$_sockets[$socket]['nick']."\" time=\"".date("H:i:s")."\" msg=\"".$msg."\" from=\"".$_sockets[$client]['nick']."\" />";
            }
            elseif(empty($nicks[$id]))
            //not online or something similar
            {
               //backto the sender
               $_client = $socket;
               $out = "<error value=\"User is already left.\"/>";
            }
        }
    }
    else
    {
        //backto the sender
        $_client = $socket;
        $out = "<error value=\"Not identified.\"/>";
    }
    if (!empty($out))
    {
       socket_write($socket, $out.chr(0)); //send to back ourself. we have to handle it in flash
       socket_write($_client, $out.chr(0)); //send to the recipient
    }
}

And now, we create the function, which sends message to all connected clients. And the last one shows, how to list identified users.

    function send_Message($allclients, $socket, $buf)
    {
      global $_sockets;
 
      foreach($allclients as $client)
      {
        @socket_write($client, $buf.chr(0));
      }
    }
 
    function list_Users($allclients,$socket)
    {
      global $_sockets;
      $out = "<nicklist>";
      foreach($allclients as $client)
      {
        if (!empty($_sockets[$client]['nick']) && ($_sockets[$client]['nick'] != ""))
        {
          $out .= "<nick aid=\"".$_sockets[$client]['nick']."\" name=\"".$_sockets[$client]['name']."\" />";
        }
      }
      $out .= "</nicklist>";
      socket_write($socket, $out.chr(0));
    }
?>

So our daemon now handles three basic commands, which is identification, user list, and message sendling.
That’s what I promised at the beginning. You can upgrade that much more. For example, we don’t have nick changing, we don’t have the action command which is known as „/me”… Etc.

So that’s all. The whole source can be downloaded here.

And I’ve got a present for you, for the end.
Here is a tiny BASH start script, or init script for the daemon.

#!/bin/sh
if [ "X$1" = "Xstart" ] ; then
    chmod +x /var/www/chat/phpircgateway.php
    /var/www/chat/phpircgateway.php >> /var/log/chat/chat.log &
    echo "Starting chat"
fi

Happy coding 😉

Download the fully

commented source code
here!

A hozzászólások jelenleg nem engedélyezettek ezen a részen.