PHP: Basic Web Server Example

Wednesday, February 25, 2009

Now, we will create a web server in PHP, serving only GET requests and basic file types. Given that it will be written in PHP, however, we will add an easy feature: Every file served through this web server will be treated as PHP and parsed for commands. After all, why else would you write a web server in PHP except to run PHP in it?

To make this web server, we'll start by creating our service on a specific port and IP address. It will be set to the local loopback address 127.0.0.1 and port 8088 to not conflict with any other web server on the same machine. We then listen for connections. To handle more than one client connection at a time, when a connection comes in, we will launch a new process using pcntl_fork(). This creates a new process that can handle the request and return the data, leaving the parent/server process to immediately be ready to accept another connection.

This setup will not scale well; someone could easily bring down your machine by rapidly connecting to the machine, causing you to spawn thousands of processes. If this server was ever put into production, it would at the very least need to have a limiting factor built into it, keeping the number of processes to a certain maximum. Of course, after that there are many other issues with this basic web server in terms of performance, features, and so on. However, it gives a good overview of how to write an Internet server.

<?php
$port = 8088;
$host = '127.0.0.1';
$docroot = '/html';

set_time_limit(0);

if (!($server = stream_socket_server("tcp://{$host}:{$port}",
        $err_num, $err_string))) {
    exit("ERROR: Failed to Open Server - {$err_num} - {$err_string}\n");
}


for (;;) {
    $client = stream_socket_accept($server, -1);

    if ($client) {
        $pid = pcntl_fork();

        if ($pid == -1) {
            exit("ERROR: Could not create new process!");
        }
        elseif (!$pid) {
            $command = fgets($client, 2048);

            while($line = fgets($client, 2048)) {
                if (trim($line) === '') { break; }
            }

            $request = explode(' ', $command);

            if ($request[0] != 'GET') {
                @fwrite($client, 
"HTTP/0.9 501 Not Implemented Server: Bare-Basic-PHP-WebServer Content-Type: text/html
<html><head><title>501 Not Implemented</title></head>
<body><h1>501 Not Implemented</h1>
<p>This is a very basic web server that only implements GET</p>
</body></html>");
                fclose($client);
                exit();
            }
            $parts = explode('?', $request[1]);
            if ($parts[0]{strlen($parts[0])-1} == '/') {
                $parts[0] .= 'index.php';
            }
            if (!is_file($docroot . $parts[0])) {
                @fwrite($client,
"HTTP/0.9 404 Not Found
Server: Bare-Basic-PHP-WebServer
Content-Type: text/html

<html><head><title>404 Not Found</title></head>
<body><h1>404 Not Found</h1>
<p>We looked, but the file you requested was nowhere to be found!</p>
</body></html>");
                fclose($client);
                exit();
            }

            $path = pathinfo($parts[0]);
            switch ($path['extension']) {
                case 'gif':
                case 'png':
                case 'jpg':
                    $mime = "image/{$path['extension']}";
                    break;
                case 'html':
                case 'xml':
                case 'css':
                    $mime = "text/{$path['extension']}";
                    break;
                case 'js':
                    $mime = 'application/x-javascript';
                    break;
                case 'php':
                    $mime = 'text/html';
                    break;
                default:
                    $mime = 'application/octet-stream';
            }

            if (isset($parts[1])) {
                parse_str($parts[1], $_GET);
            }

            ob_start();
            include "{$docroot}{$parts[0]}";
            $output = ob_get_contents();
            ob_end_clean();

            $length = strlen($output);
            @fwrite($client,
"HTTP/0.9 200 Ok
Server: Bare-Basic-PHP-WebServer
Content-Type: {$mime}
Content-Length: {$length}

{$output}");

            fclose($client);
            exit();
        }
    }
    @fclose($client);
}
?>


Note that this program as designed will run forever and will need to be manually killed off. When calling pcntl_fork(), your process splits into two identical copies with only a slight difference. In the parent, the call to pcntl_fork() returns the process id of the child. In the child, it returns 0. This allows you to then run different code depending on whether the current process is the parent or the child of the fork.

Caution
The pcntl (process control) functions of PHP are not available on Windows, because Windows has a different process model than UNIX. Also, even on UNIX they are not compiled into the server by default and must be specifically included upon compilation. To learn more about these functions and how to enable them, visit http://php.net/pcntl.

Hope it helps.

0 comments: