Servidor de Sockets en PHP

Servidor de Sockets en PHP

Un servidor de sockets es un servidor que posibilita la comunicacion entre multiples computadoras por medio de sockets, consume mucho menos recursos y bandwitdh que la comunicacion http, y es ideal para los juegos Multijugadores y Online hechos con Action Script de Flash o cualquier otro programa o lenguaje.

En esta entrada te explicaremos de manera rapida como Crear tu propio servidor de Sockets en PHP.

Fuente en Ingles: http://djz.hu/2007/07/26/php-socket-server-chat-gateway-for-flash-clients/

Primero que nada, no pienso escribir un manual paso a paso de “COMO CREAR UN SOCKET SERVER CON PHP PARA FLASH O JUEGOS MULTIJUGADORES EN 10 MINUTOS” o algo asi.
Lo que haremos sera solo explicar el funcionamiento basico del servidor de sockets con php y comunicarlo con flash en un puerto determinado.

Presentare ademas del codigo del servidor de sockets, un sencillo archivo en Flash, capaz de demostrar el funcionamiento de Flash con un Servidor de Sockets en php.

La idea fundamental y la base del sistema la hemos extraido de aqui : kirupa.com – PHP 5 Sockets with Flash 8

Para empezar necesitamos de preferencia un servidor dedicado propio, o un servidor virtual al cual podamos tener acceso a shell o crear demonios.

Entonces, necesitamos crear un demonio php, sin limite de tiempo de ejecucion. Asi podra correr ilimitadamente o hasta que se reinicie el server.

Empezando con el codigo:

Declaramos la IP address donde nos conectaremos y el puerto escucha. Recuerden que debe de estar abierto en caso que haya un firewall.

#!/usr/bin/php -q

Vamos a crear un array para las conexiones de datos, (si lo usa para chatear, sería útil para almacenar apodos en el mismo.), Y empezar a crear y escuchar un socket. Por favor tenga en cuenta, que todo lo que se imprime con "echo" va a un archivo "log" en nuestro caso.
 $_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 es una variable que establece el número de conexiones que puede manejar su máquina. En el mundo Unix se puede ajustar en tiempo de compilación del kernel, o se puede ajustar via sysctl.
Después de esto vamos a crear un bucle persistente para tramitar las solicitudes.

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... ;) 

      */
    }
}

Así pues, estos son los conceptos básicos. Hasta este punto, este código es casi el mismo que Raymond Fain del socketShell.php antes mencionados.

Como ya he comentado vamos a editar sólo una parte del código.
Por lo que cada código que cito ahora, irá dentro de el bucle principal. Despues definiremos las funciones. Estas funciones se van fuera, el bucle.
Lo ideal para realizar un servidor de sockets, es que cuente con los permisos "cross-domain-policy", ya que asi posibilita la interaccion entre servidores con flash.

El "Cross-domain-policy" es una serie de datos, en formato XML, que se parece a esto, en nuestro caso:
Por lo tanto, almacenarla en una variable, y si se hace una solicitud debemos mostrarlo:

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='';

  socket_write($socket,$contents);
  $contents="";

  $index = array_search($socket, $read_sockets);
  unset($read_sockets[$index]);
  socket_shutdown($socket, 2);
  socket_close($socket);
}

Al recibir una peticion de cross-domain-policy debemos cerrar el socket, ya que si se envia correctamente el permiso entonces se abre un socket nuevo con la conexion adecuada.
Ahora, cuando un usuario pierde la conexion, se sale del sitio o cualquier otra razon, debemos de cortar el socket y borrar sus variables.

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, "");
  echo "[".date('Y-m-d H:i:s')."] QUIT ".$id."\n";
}

Y ahora, la comunicacion real del socket:

else
{
  $allclients = $read_sockets;
  array_shift($allclients);

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

Suponiendo un codigo como el del protocolo IRC: MSG PARA MENSAJE

Debemos separar por medio del "espacio" para formar un array, y detectar el codigo que vamos a efectuar, despues, unir el resto que sera el "mensaje":

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;
}

Ahora, vamos con las funciones, declaradas despues del bucle. Debemos recalcar, que para enviar datos al cliente debemos de anexar un chr(0) por alguna razon de la comunicacion.

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);

Es escencial que cerremos la conexion a la base de datos, que si no en un par de horas se acomularan los intentos de conexion a la base de datos y se colapsara el server.

  }
  else $s=0;

  //   We'll answer to the flash in XML form.
  //   But we receive in plain text format.

  if ($s == 1)
  {
    $out = "";
    send_Message($allclients, "");
    // 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 = "";
            }
            elseif(empty($nicks[$id]))
            //not online or something similar
            {
               //backto the sender
               $_client = $socket;
               $out = "";
            }
        }
    }
    else
    {
        //backto the sender
        $_client = $socket;
        $out = "";
    }
    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
    }
}

Ahora creamos la funcion encargada de enviar un mensaje a todos los conectados:

    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 = "";
      foreach($allclients as $client)
      {
        if (!empty($_sockets[$client]['nick']) && ($_sockets[$client]['nick'] != ""))
        {
          $out .= "";
        }
      }
      $out .= "";
      socket_write($socket, $out.chr(0));
    }
?>

Y eso es todo por parte de PHP, que basicamente es lo primordial para entender el funcionamiento, de ahi podemos agregar mas funciones y comandos.
El archivo fuente PHP esta AQUI
El archivo FLASH lo publicare mas adelante.
Les dejo un codigo BASH de regalo que es para arrancar el demonio php.

#!/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

Tags: , , , ,

Leave a Reply