Skip to content

Instantly share code, notes, and snippets.

@brad-jones
Created January 23, 2014 06:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brad-jones/8573837 to your computer and use it in GitHub Desktop.
Save brad-jones/8573837 to your computer and use it in GitHub Desktop.
The SCGI protocol is a replacement for the Common Gateway Interface (CGI) protocol. It is a standard for applications to interface with HTTP servers. It is similar to FastCGI but is designed to be easier to implement. This is a PHP Implementation.
<?php
/**
* Class: MyListener
* =============================================================================
* This sets up the underlying socket server.
* Using all this nice new LibEvent code which is now part of PHP.
*/
class MyListener
{
/**
* Property: base
* =========================================================================
* This is where the EventBase instance is stored.
*/
public $base;
/**
* Property: listener
* =========================================================================
* This is where the EventListener instance is stored.
*/
public $listener;
/**
* Property: connections
* =========================================================================
* This is where we store an array of MyConnection objects.
*/
private $connections = array();
/**
* Method: __construct
* =========================================================================
* Creates the EventBase and EventListener objects, sets up some call backs.
* Along with some very basic error handling. It finally dispatches the
* events.
*
* Parameters
* -------------------------------------------------------------------------
* $who - Who are we listening too, can be IP:PORT or unix:/tmp/my.sock
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function __construct($who)
{
// Create the base
$this->base = new EventBase();
// Check the base got created okay
if (!$this->base)
{
echo "Couldn't open event base";
exit(1);
}
// Create the listener
$this->listener = new EventListener
(
$this->base,
[$this, "acceptConnectionCb"],
null,
EventListener::OPT_CLOSE_ON_FREE | EventListener::OPT_REUSEABLE,
-1,
$who
);
// Check the listener got created okay
if (!$this->listener)
{
echo "Couldn't create listener";
exit(1);
}
// Add an error handler for the listener
$this->listener->setErrorCallback([$this, "acceptErrorCb"]);
// Start the server basically
$this->base->dispatch();
}
/**
* Method: acceptConnectionCb
* =========================================================================
* When ever a new connection to our server comes in this is called.
* It creates a new instance of MyConnection and it then takes over the
* processing of the request.
*
* Parameters
* -------------------------------------------------------------------------
* $listener - A copy of EventListener
* $fd - The file descriptor or a resource associated with the listener.
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function acceptConnectionCb($listener, $fd)
{
$this->connections[] = new MyConnection($this->base, $fd);
}
/**
* Method: acceptErrorCb
* =========================================================================
* When ever the listener has some sort of error this will be called.
*
* Parameters
* -------------------------------------------------------------------------
* n/a
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function acceptErrorCb()
{
// Output the error
fprintf
(
STDERR,
"Got an error %d (%s) on the listener. Shutting down.\n",
EventUtil::getLastSocketErrno(),
EventUtil::getLastSocketError()
);
// And stop the server.
$this->base->exit(NULL);
}
/**
* Method: __destruct
* =========================================================================
* Destroy all the connections
*
* Parameters
* -------------------------------------------------------------------------
* n/a
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function __destruct()
{
foreach ($this->connections as &$c) $c = NULL;
}
}
/**
* Class: MyConnection
* =============================================================================
* This class works with an individual connection to our server.
* And deals more with the actual SCGI protocol.
*/
class MyConnection
{
/**
* Property: bev
* =========================================================================
* This is where we store the EventBufferEvent object.
*/
private $bev;
/**
* Method: __construct
* =========================================================================
* This creates the EventBufferEvent object along with some call backs.
*
* Parameters
* -------------------------------------------------------------------------
* $base - A copy of EventBase
* $fd - The file descriptor or a resource associated with the listener.
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function __construct($base, $fd)
{
$this->bev = new EventBufferEvent
(
$base, $fd, EventBufferEvent::OPT_CLOSE_ON_FREE
);
$this->bev->setCallbacks
(
[$this, "readCallback"],
[$this, "writeCallback"],
[$this, "eventCallback"],
NULL
);
if (!$this->bev->enable(Event::READ))
{
echo "Failed to enable READ\n";
return;
}
}
/**
* Method: readCallback
* =========================================================================
* This is where we read in an actual request.
* For details on the protocol see: http://www.python.ca/scgi/protocol.txt
*
* Parameters
* -------------------------------------------------------------------------
* $bev - The buffer event
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function readCallback($bev)
{
// Grab the scgi request
$request = $bev->input->read($bev->input->length);
// Grab the length of the netstring
$length = substr($request, 0, strpos($request, ':'));
// Read in the headers
$headers_string = substr($request, strpos($request, ':')+1, $length);
// Parse the headers
$headers = [];
preg_match_all('/(.*?)\x00(.*?)\x00/s', $headers_string, $matches);
foreach ($matches[1] as $x => $name) $headers[$name] = $matches[2][$x];
// Output the headers to console for debug purposes
echo 'Request Headers:'."\n";
print_r($headers);
// Write the response
$bev->write
(
'Status: 200 OK'."\r\n".
'Content-Type: text/plain'."\r\n".
"\r\n".
time()
);
}
/**
* Method: writeCallback
* =========================================================================
* This is called after we have written to the buffer. Because the SCGI
* protocol calls for the connection to be closed. We must then destroy
* ourselves.
*
* Parameters
* -------------------------------------------------------------------------
* $bev - The buffer event
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function writeCallback($bev)
{
echo "Response Sent\n\n";
$this->__destruct();
}
/**
* Method: eventCallback
* =========================================================================
* Some basic error handling.
*
* Parameters
* -------------------------------------------------------------------------
* $bev - A copy of the buffer event.
* $events - Bit mask of events
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function eventCallback($bev, $events)
{
if ($events & EventBufferEvent::ERROR)
{
echo "Error from bufferevent\n";
}
if ($events & (EventBufferEvent::EOF | EventBufferEvent::ERROR))
{
$this->__destruct();
}
}
/**
* Method: __destruct
* =========================================================================
* Destroy ourselves.
*
* Parameters
* -------------------------------------------------------------------------
* n/a
*
* Returns:
* -------------------------------------------------------------------------
* void
*/
public function __destruct()
{
$this->bev->free();
}
}
// Start the server
new MyListener('127.0.0.1:9000');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment