PHP: Socket server

PHOTO EMBED

Sun Aug 28 2022 01:39:54 GMT+0000 (Coordinated Universal Time)

Saved by @marcopinero #php

<?php

class PHPSocketServer
{
  const ERROR = 0;
  const LOG   = 1;
  const DEBUG = 2;

  private $aConfig       = NULL;
  private $hSocket       = NULL;
  private $aClients      = array();
  private $iRunning      = 0;
  private $iStartTime    = 0;
  private $iLastActivity = 0;

  private static $aDefaults = array
  (
    'main' => array
    (
      'address'          => 'localhost',
      'port'             => 15151,
      'backlog'          => 200,
      'select_timeout'   => 1,
      'pid_file'         => 'phpserver.pid'
    ),

    'verbosity' => array
    (
      'echo_log'         => 1,
      'echo_debug'       => 0,
      'echo_errors'      => 1,
      'log_errors'       => 1
    )
  );

  public function getPidFile()
  {
    return $this->aConfig['main']['pid_file'];
  }

  private static function getName( $hSocket )
  {
    return socket_getpeername( $hSocket, $sAddress, $iPort ) ? "$sAddress:$iPort" : "?????:???";
  }

  private function log( $sMessage, $iType )
  {
    $sTimeStamp = @strftime( '%c' );

    if( $iType == self::ERROR )
    {
      $aBacktrace = debug_backtrace();
      $aBacktrace = $aBacktrace[1];
      $sMessage   = sprintf( '[%s] [E] %s%s%s [%d] : %s', $sTimeStamp, $aBacktrace['class'], $aBacktrace['type'], $aBacktrace['function'], $aBacktrace['line'], $sMessage );

      if( $this->aConfig['verbosity']['echo_errors'] )
        printf( "$sMessage\n" );

      if( $this->aConfig['verbosity']['log_errors'] )
        error_log( $sMessage );
    }
    else if( $iType == self::DEBUG && $this->aConfig['verbosity']['echo_debug'] )
    {
      echo sprintf( '[%s] [D] : %s', $sTimeStamp, $sMessage )."\n";
    }
    else if( $iType == self::LOG && $this->aConfig['verbosity']['echo_log'] )
    {
      echo sprintf( '[%s] [L] : %s', $sTimeStamp, $sMessage )."\n";
    }
  }

  /*
   * Handle clients here.
   */
  private function handleClientRequest( $hClient, $iClientIndex )
  {
    /*
     * ...
     */

    $this->disconnect( $iClientIndex );
  }

  private function disconnect( $i )
  {
    socket_close( $this->aClients[ $i ] );
    $this->aClients[ $i ] = NULL;
  }

  private function loadConfiguration( $sConfigFile )
  {
    if( !is_file( $sConfigFile ) || !is_readable( $sConfigFile ) )
      die( "Could not read $sConfigFile.\n" );

    else if( !( $this->aConfig = parse_ini_file( $sConfigFile, TRUE ) ) )
      die( "Could not parse $sConfigFile.\n" );

    foreach( self::$aDefaults as $sSection => $aDefaultValues )
    {
      if( !isset( $this->aConfig[ $sSection ] ) )
        $this->aConfig[ $sSection ] = array();

      foreach( $aDefaultValues as $sName => $sValue )
      {
        if( !isset( $this->aConfig[ $sSection ][ $sName ] ) )
          $this->aConfig[ $sSection ][ $sName ] = $sValue;
      }
    }
  }

  public function setConfig( $sSectionName, $sConfigName, $mValue )
  {
    if( !isset( $this->aConfig[ $sSectionName ] ) )
      $this->aConfig[ $sSectionName ] = array();

    $this->aConfig[ $sSectionName ][ $sConfigName ] = $mValue;
  }

  public function __construct( $sConfigFile )
  {
    $this->loadConfiguration( $sConfigFile );

    if( !( $this->hSocket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP ) ) )
      $this->log( 'Could not create main socket ( '.socket_strerror( socket_last_error() ).' ).', self::ERROR );

    else if( socket_set_option( $this->hSocket, SOL_SOCKET, SO_REUSEADDR, 1 ) === FALSE )
      $this->log( 'Could not set SO_REUSEADDR flag ( '.socket_strerror( socket_last_error() ).' ).', self::ERROR );
  }

  public function start()
  {
    if( !socket_bind( $this->hSocket, $this->aConfig['main']['address'], $this->aConfig['main']['port'] ) )
      $this->log( 'Could not bind on '.$this->aConfig['main']['address'].':'.$this->aConfig['main']['port'].' ( '.socket_strerror( socket_last_error() ).' ).', self::ERROR );

    else if( !socket_listen( $this->hSocket, $this->aConfig['main']['backlog'] ) )
      $this->log( 'Could not put main socket in listening mode ( '.socket_strerror( socket_last_error() ).' ).', self::ERROR );

    else if( !socket_set_nonblock( $this->hSocket ) )
      $this->log( 'Could not set main socket in non-blocking mode ( '.socket_strerror( socket_last_error() ).' ).', self::ERROR );

    else
    {
      $this->iStartTime = time();

      $this->log( 'Server started on '.$this->aConfig['main']['address'].':'.$this->aConfig['main']['port'].' .', self::LOG );

      for(;;)
      {
        $aRead = array_merge( array( $this->hSocket ), $this->aClients );

        if( socket_select( $aRead, $aWrite, $aExcept, $this->aConfig['main']['select_timeout'] ) )
        {
          if( in_array( $this->hSocket, $aRead ) )
          {
            if( ( $hClient = @socket_accept( $this->hSocket ) ) )
            {
              $this->aClients[ microtime(TRUE) ] = $hClient;
              $this->iLastActivity = time();

              $this->log( 'New incoming connection '.self::getName( $hClient ), self::DEBUG );
            }
            else
              $this->log( 'Could not accept a new connection ( '.socket_strerror( socket_last_error() ).' ).', self::ERROR );
          }
        }

        /*
         * Search for readable clients.
         */
        foreach( $this->aClients as $i => $hClient )
        {
          if( in_array( $hClient, $aRead ) )
          {
            $this->handleClientRequest( $hClient, $i );
          }
        }

        /*
         * Remove closed connections.
         */
        $this->aClients = array_filter( $this->aClients );
      }
    }
  }

  public function __destruct()
  {
    if( $this->hSocket )
    {
      $this->log( 'Shutting down ...', self::LOG );

      foreach( $this->aClients as $sId => $hClient )
      {
        if( $hClient )
          socket_close( $hClient );
      }

      socket_close( $this->hSocket );
    }

    @unlink( $this->getPidFile() );
  }
}
content_copyCOPY

This php script works like a socket service daemon. It can handle multiple calls.