Added multi-threading/forking/keep-alive support to webserver. w00t, feeling all POSIX-happy today!
authorDan
Wed, 02 Apr 2008 00:23:51 -0400
changeset 15 2adca0f363fd
parent 14 7a1573676cc4
child 16 23d4cf2f183b
Added multi-threading/forking/keep-alive support to webserver. w00t, feeling all POSIX-happy today!
functions.php
greyhound.php
webserver.php
--- a/functions.php	Mon Mar 31 07:40:30 2008 -0400
+++ b/functions.php	Wed Apr 02 00:23:51 2008 -0400
@@ -39,7 +39,8 @@
 function status($msg)
 {
   $h = @fopen('php://stderr', 'w');
-  fwrite($h, "\x1B[32;1m[Greyhound] \x1B[32;0m$msg\x1B[0m\n");
+  $label = ( defined('HTTPD_WS_CHILD') ) ? 'Child ' . getmypid() : ' Greyhound ';
+  fwrite($h, "\x1B[32;1m[$label] \x1B[32;0m$msg\x1B[0m\n");
   fclose($h);
 }
 
--- a/greyhound.php	Mon Mar 31 07:40:30 2008 -0400
+++ b/greyhound.php	Wed Apr 02 00:23:51 2008 -0400
@@ -25,9 +25,22 @@
   pcntl_signal(SIGINT,  'sigterm');
 }
 
+//
+// CONFIGURATION
+//
+
+// Listen on all interfaces. If this is false, it will only listen on
+// 127.0.0.1 (the loopback interface)
 $public = true;
+// Allow control of playback. By default this is turned on but if you
+// set this to false, it will only display the playlist.
 $allowcontrol = true;
+// The default theme. This should be a name of a directory in ./themes.
 $theme = 'funkymonkey';
+// Allow forking when an HTTP request is received. This has advantages
+// and disadvantages. If this experimental option is enabled, it will
+// result in faster responses and load times but more memory usage.
+$allow_fork = true;
 
 @ini_set('display_errors', 'on');
 
@@ -42,7 +55,7 @@
 
 // start up...
 
-status('Starting WebControl v0.1-hg');
+status('Starting Greyhound Web Control v0.1-hg');
 status('loading files');
 
 require('webserver.php');
@@ -99,7 +112,28 @@
   $httpd->add_handler('scripts',              'dir',      GREY_ROOT . '/scripts');
   $httpd->add_handler('favicon.ico',          'file',     GREY_ROOT . '/amarok_icon.ico');
   $httpd->add_handler('apple-touch-icon.png', 'file',     GREY_ROOT . '/apple-touch-icon.png');
+  // load all themes if forking is enabled
+  // Themes are loaded when the playlist is requested. This is fine for
+  // single-threaded operation, but if the playlist handler is only loaded
+  // in a child process, we need to preload all themes into the parent before
+  // children can respond to theme resource requests.
+  if ( $allow_fork )
+  {
+    status('Preloading themes');
+    
+    $dh = @opendir(GREY_ROOT . '/themes');
+    if ( !$dh )
+      burnout('Could not open themes directory');
+    while ( $dir = @readdir($dh) )
+    {
+      if ( $dir == '.' || $dir == '..' )
+        continue;
+      if ( is_dir( GREY_ROOT . "/themes/$dir" ) )
+        load_theme($dir);
+    }
+  }
   $httpd->allow_dir_list = true;
+  $httpd->allow_fork = ( $allow_fork ) ? true : false;
   $httpd->default_document = 'index';
   
   status("Entering main server loop - ^C to interrupt, listening on port $port");
@@ -109,4 +143,3 @@
 {
   burnout("Exception caught while running webserver:\n$e");
 }
-
--- a/webserver.php	Mon Mar 31 07:40:30 2008 -0400
+++ b/webserver.php	Wed Apr 02 00:23:51 2008 -0400
@@ -95,6 +95,21 @@
   var $allow_dir_list = false;
   
   /**
+   * Switch to control forking support.
+   * @var bool
+   */
+  
+  var $allow_fork = true;
+  
+  /**
+   * Keep-alive support uses this to track what the client requested.
+   * Only used if $allow_fork is set to true.
+   * @var bool
+   */
+  
+  var $in_keepalive = false;
+  
+  /**
    * Constructor.
    * @param string IPv4 address to bind to
    * @param int Port number
@@ -130,8 +145,12 @@
   
   function __destruct()
   {
-    status('WebServer: destroying socket');
-    @socket_close($this->sock);
+    if ( !defined('HTTPD_WS_CHILD') )
+    {
+      status('WebServer: destroying socket');
+      @socket_shutdown($this->sock, 2);
+      @socket_close($this->sock);
+    }
   }
   
   /**
@@ -142,24 +161,59 @@
   {
     while ( true )
     {
+      // if this is a child process, we're finished - close up shop
+      if ( defined('HTTPD_WS_CHILD') && !$this->in_keepalive )
+      {
+        exit(0);
+      }
+      
       // wait for connection...
       // trick from http://us.php.net/manual/en/function.socket-accept.php
-      $remote = false;
-      switch(@socket_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), 5)) {
-        case 2:
-          break;
-        case 1:
-          $remote = @socket_accept($this->sock);
-          break;
-        case 0:
-          break;
+      if ( !defined('HTTPD_WS_CHILD') )
+      {
+        $remote = false;
+        $timeout = 5;
+        switch(@socket_select($r = array($this->sock), $w = array($this->sock), $e = array($this->sock), $timeout)) {
+          case 2:
+            break;
+          case 1:
+            $remote = @socket_accept($this->sock);
+            break;
+          case 0:
+            break;
+        }
       }
          
       if ( !$remote )
       {
+        $this->in_keepalive = false;
         continue;
       }
       
+      // fork off if possible
+      if ( function_exists('pcntl_fork') && $this->allow_fork && !$this->in_keepalive )
+      {
+        $pid = pcntl_fork();
+        if ( $pid == -1 )
+        {
+          // do nothing; continue responding to request in single-threaded mode
+        }
+        else if ( $pid )
+        {
+          // we are the parent, continue listening
+          $remote = false;
+          continue;
+        }
+        else
+        {
+          // this is the child
+          define('HTTPD_WS_CHILD', 1);
+          $this->sock = false;
+        }
+      }
+      
+      $this->in_keepalive = false;
+      
       // read request
       $last_line = '';
       $client_headers = '';
@@ -198,6 +252,12 @@
         $_SERVER[$key] = $match[2];
       }
       
+      // enable keep-alive if requested
+      if ( isset($_SERVER['HTTP_CONNECTION']) && defined('HTTPD_WS_CHILD') )
+      {
+        $this->in_keepalive = ( $_SERVER['HTTP_CONNECTION'] === 'keep-alive' );
+      }
+      
       if ( isset($_SERVER['HTTP_AUTHORIZATION']) )
       {
         $data = $_SERVER['HTTP_AUTHORIZATION'];
@@ -285,7 +345,19 @@
       
       $this->send_standard_response($remote, $handler, $uri, $params);
       
-      @socket_close($remote);
+      if ( !$this->in_keepalive )
+      {
+        // if ( defined('HTTPD_WS_CHILD') )
+        //   status('Closing connection');
+        @socket_close($remote);
+        exit(0);
+      }
+      else
+      {
+        // if ( defined('HTTPD_WS_CHILD') )
+        //   status('Continuing connection');
+        @socket_write($remote, "\r\n\r\n");
+      }
     }
   }
   
@@ -302,15 +374,17 @@
     global $http_responses;
     $reason_code = ( isset($http_responses[$http_code]) ) ? $http_responses[$http_code] : 'Unknown';
     
+    $_SERVER['HTTP_USER_AGENT'] = ( isset($_SERVER['HTTP_USER_AGENT']) ) ? $_SERVER['HTTP_USER_AGENT'] : '(no user agent)';
     status("{$_SERVER['REMOTE_ADDR']} {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} $http_code {$_SERVER['HTTP_USER_AGENT']}");
     
     $headers = str_replace("\r\n", "\n", $headers);
     $headers = str_replace("\n", "\r\n", $headers);
     $headers = preg_replace("#[\r\n]+$#", '', $headers);
+    $connection = ( $this->in_keepalive ) ? 'keep-alive' : 'close';
     
     @socket_write($socket, "HTTP/1.1 $http_code $reason_code\r\n");
     @socket_write($socket, "Server: $this->server_string");
-    @socket_write($socket, "Connection: close\r\n");
+    @socket_write($socket, "Connection: $connection\r\n");
     @socket_write($socket, "Content-Type: $contenttype\r\n");
     if ( !empty($headers) )
     {
@@ -518,6 +592,7 @@
           return true;
         }
         
+        $this->header("Content-length: " . strlen($output));
         $headers = implode("\r\n", $this->response_headers);
         
         // write headers