9 min read

Writing a webserver in pure PHP - Tutorial

Writing a webserver in pure PHP - Tutorial

Well, this is pretty useless, but it is possible. But again its pretty.. uesless. This tutorial will hopefully help you to better understand how a simple webserver could work and that it's no problem writing one in PHP. But again using this in production would be trying to eat a soup with a fork. So just, .... just don't. Let me shortly explain why this is not a that good idea.

PHP is a scripting language that simply is not really designed for such tasks.

A webserver is a long running process which PHP is simply not made for. Also PHP does not natively support multi-threading ( pthreads ), which will make developing a good performing web-server a really hard/impossible task. PHPs memory allocations can be quite wasteful and even if the interpreter is incredibly fast it still has the overhead of an interpreter.
But these things might change in the future.
If you met some programmers, there was at least one who made jokes about how bad PHP is and there are obvious reasons why PHP became such an industry inside joke. But the language and its eco-system has evolved, ...alot.
There are still people who can't see behind the jokes, a lot of them either didn't touch PHP for at least 10 years or copycat other peoples opinions to be a cool kid.
I would bet a few bucks that things like long running php applications with embedded web servers and other things will show up more and more.

Use the Source, Luke

The entire source is available on Github: https://github.com/ClanCatsStation/PHPWebserver

Basics

Enough warnings about doing what I'm about to explain how to do. let's get started eating the god damit soup with god damit chopsticks.

How does a webserver basically work?

  1. The server listens for incoming connections.
  2. A client connects to the server.
  3. The server accepts the connection and handles the input.
  4. The server responds to the client.

Structure

I'm going to build this as an abstraction of the Request and Response. There are many ways designing an application, As a first step I prefer writing the part of application that consumes my API. Followed by writing the actual API.

So later I wan't to be able to use the thing like this:

// create a new server instance
$server = new Server( '127.0.0.1', 80 );

// start listening
$server->listen( function( Request $request ) 
{
	return new Response( 'Hello Dude' );
});

The directory structure (just as help):

server
composer.json
src/Request.php
src/Response.php
src/Server.php
src/Exception.php

I'm going to use PSR-4 autoloading. So start by creating a new composer.json file. ( Don't forget to run composer install afterwards )

{
	"autoload":
	{
		"psr-4": 
		{
			"ClanCats\\Station\\PHPServer\\": "src/"	
		}
	}
}

Initializing

Next we create the script file (server) which will take care of starting the server. We don't add the .php extension. So that bash knows what do do, add the following header:

#!/usr/bin/env php
<?php 

We are always going to bind the server to localhost but we wan't to be able to define the port as command line argument.

// we never need the first argument
array_shift( $argv );

// the next argument should be the port if not use 80
if ( empty( $argv ) )
{
	$port = 80;
} else {
	$port = array_shift( $argv );
}

This allows as to start the server later like this:

$ sudo php server 8008

If we combine that with what we defined before we get the following file:

#!/usr/bin/env php
<?php 
use ClanCats\Station\PHPServer\Server; 
use ClanCats\Station\PHPServer\Request;
use ClanCats\Station\PHPServer\Response;

// we never need the first argument
array_shift( $argv );

// the next argument should be the port if not use 80
if ( empty( $argv ) )
{
	$port = 80;
} else {
	$port = array_shift( $argv );
}

require 'vendor/autoload.php';

// create a new server instance
$server = new Server( '127.0.0.1', $port );

// start listening
$server->listen( function( Request $request ) 
{
	return new Response( 'Hello Dude' );
});

Server Object

Next lets create our src/Server.php file which is going to handle the socket.

<?php namespace ClanCats\Station\PHPServer;

class Server 
{
}

Our server class is going to hold the host, the port and the socket resource so add the following class variables.

protected $host = null;
protected $port = null;
protected $socket = null;

Create a socket

To bind a socket we need to have one so create the createSocket function.

protected function createSocket()
{
	$this->socket = socket_create( AF_INET, SOCK_STREAM, 0 );
}

The first argument specifies the domain / protocol family of the socket. AF_INET is for IPv4 TCP and UDP protocols.

The second argument defines the communication type of the socket. SOCK_STREAM is a simple full-duplex connection based byte stream.

The third argument sets the protocol.

Bind the socket

This is pretty self explaining. The socket_bind function returns false when something goes wrong. Because this should never happen we throw an exception with the socket error message.

protected function bind()
{
	if ( !socket_bind( $this->socket, $this->host, $this->port ) )
	{
		throw new Exception( 'Could not bind: '.$this->host.':'.$this->port.' - '.socket_strerror( socket_last_error() ) );
	}
}

Create and bind the socket on construct

We could also create a connect function, but to keep stuff simple we just do it in the constructor.

public function __construct( $host, $port )
{
	$this->host = $host;
	$this->port = (int) $port;
	
	// create a socket
	$this->createSocket();
	
	// bind the socket
	$this->bind();
}

Listen for connections

Beacuse I don't want to split this function in 20 segments just to explain what happens, I added my bullshit to the comments.

public function listen( $callback )
{
	// check if the callback is valid. Throw an exception
    // if not.
	if ( !is_callable( $callback ) )
	{
		throw new Exception('The given argument should be callable.');
	}
	
    // Now here comes the thing that makes this process
    // long, infinite, never ending..
	while ( 1 ) 
	{
		// listen for connections
		socket_listen( $this->socket );
		
		// try to get the client socket resource
		// if false we got an error close the connection and skip
		if ( !$client = socket_accept( $this->socket ) ) 
		{
			socket_close( $client ); continue;
		}
		
		// create new request instance with the clients header.
		// In the real world of course you cannot just fix the max size to 1024..
		$request = Request::withHeaderString( socket_read( $client, 1024 ) );
		
		// execute the callback 
		$response = call_user_func( $callback, $request );
		
		// check if we really recived an Response object
		// if not return a 404 response object
		if ( !$response || !$response instanceof Response )
		{
			$response = Response::error( 404 );
		}
		
		// make a string out of our response
		$response = (string) $response;
		
		// write the response to the client socket
		socket_write( $client, $response, strlen( $response ) );
		
		// close the connetion so we can accept new ones
		socket_close( $client );
	}
}

Request Object

Now it's time to create the src/Request.php file which is going to handle the user input.

<?php namespace ClanCats\Station\PHPServer;

class Request 
{
}

Our Request is going to hold the HTTP request method, the uri, parameters and headers. So add these class variables:

protected $method = null;
protected $uri = null;
protected $parameters = [];
protected $headers = [];

Parse the header

Now in our listen function we already pass the socket input / request header to the withHeaderString function. A http header looks like this:

GET / HTTP/1.1
Host: 127.0.0.1:8008
Connection: keep-alive
Accept: text/html
User-Agent: Chrome/41.0.2272.104
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,de;q=0.6

So what we need to do is parse that data. The first line indicates the request method, uri and protocol. Followed by key, value header parameters.

public static function withHeaderString( $header )
{
    // explode the string into lines.
	$lines = explode( "\n", $header );
	
	// extract the method and uri
	list( $method, $uri ) = explode( ' ', array_shift( $lines ) );
	
	$headers = [];
	
	foreach( $lines as $line )
	{
		// clean the line
		$line = trim( $line );
		
		if ( strpos( $line, ': ' ) !== false )
		{
			list( $key, $value ) = explode( ': ', $line );
			$headers[$key] = $value;
		}
	}	
	
	// create new request object
	return new static( $method, $uri, $headers );
}

Our constructor recives $method, $uri and $headers. We could simply just assign them to a class variable. But for this example I want to split and parse the query parameters.

public function __construct( $method, $uri, $headers = [] ) 
{
	$this->headers = $headers;
	$this->method = strtoupper( $method );
	
	// split uri and parameters string
	@list( $this->uri, $params ) = explode( '?', $uri );

	// parse the parmeters
	parse_str( $params, $this->parameters );
}

Create request getter methods

Because our class variables method, uri, parameters and headers are protected we need to create some getters to make the request data accessible.

There is nothing specail with the method and uri getters. They just return..

public function method()
{
	return $this->method;
}
public function uri()
{
	return $this->uri;
}

Now the header and param getter should allow giving a default value. Which get return if no data with the given key is found.

public function header( $key, $default = null )
{
	if ( !isset( $this->headers[$key] ) )
	{
		return $default;
	}
		
	return $this->headers[$key];
}
public function param( $key, $default = null )
{
	if ( !isset( $this->parameters[$key] ) )
	{
		return $default;
	}
		
	return $this->parameters[$key];
}

Response Object

Being muted isn't much fun. Of course we wan't to be able to respond to our request. As you see in the listen function, the given callback has to return a Response object. Otherwise a 404 response is returend.

How does a http response look like? Actually pretty much the same as the request. We have a header and a body. And we will simply write them both into the socket to respond to the client.

Again this is not the optimal way for a solid implementation its an example..

Create a new file src/Response.php.

<?php namespace ClanCats\Station\PHPServer;

class Response 
{
}

Status codes

404 in tha house! To be able to build our header string we need to know the http status codes. We could also set them manually, but who the hell wants to write stuff manually?

This array pretty much covers the http status codes definitions. Taken from CCF.

protected static $statusCodes = [
	// Informational 1xx
	100 => 'Continue',
	101 => 'Switching Protocols',

	// Success 2xx
	200 => 'OK',
	201 => 'Created',
	202 => 'Accepted',
	203 => 'Non-Authoritative Information',
	204 => 'No Content',
	205 => 'Reset Content',
	206 => 'Partial Content',

	// Redirection 3xx
	300 => 'Multiple Choices',
	301 => 'Moved Permanently',
	302 => 'Found', // 1.1
	303 => 'See Other',
	304 => 'Not Modified',
	305 => 'Use Proxy',
	// 306 is deprecated but reserved
	307 => 'Temporary Redirect',

	// Client Error 4xx
	400 => 'Bad Request',
	401 => 'Unauthorized',
	402 => 'Payment Required',
	403 => 'Forbidden',
	404 => 'Not Found',
	405 => 'Method Not Allowed',
	406 => 'Not Acceptable',
	407 => 'Proxy Authentication Required',
	408 => 'Request Timeout',
	409 => 'Conflict',
	410 => 'Gone',
	411 => 'Length Required',
	412 => 'Precondition Failed',
	413 => 'Request Entity Too Large',
	414 => 'Request-URI Too Long',
	415 => 'Unsupported Media Type',
	416 => 'Requested Range Not Satisfiable',
	417 => 'Expectation Failed',

	// Server Error 5xx
	500 => 'Internal Server Error',
	501 => 'Not Implemented',
	502 => 'Bad Gateway',
	503 => 'Service Unavailable',
	504 => 'Gateway Timeout',
	505 => 'HTTP Version Not Supported',
	509 => 'Bandwidth Limit Exceeded'
];

Just a little thing aside there is a repository that covering the 7xx http status codes: https://github.com/joho/7XX-rfc They are hilarious :)

Response constructor

The general parameters our response object should implement are the http status, body and other headers.

protected $status = 200;
protected $body = '';
protected $headers = [];

Body should be a must argument in the constructor, while the status and other headers should be optional. Also the constructor should set some default values like the current Date or the Server header.

public function __construct( $body, $status = null )
{
	if ( !is_null( $status ) )
	{
		$this->status = $status;
	}
	
	$this->body = $body;
	
	// set inital headers
	$this->header( 'Date', gmdate( 'D, d M Y H:i:s T' ) );
	$this->header( 'Content-Type', 'text/html; charset=utf-8' );
	$this->header( 'Server', 'PHPServer/1.0.0 (Whateva)' );
}

Setting headers

To be able to add new header parameters to the object we need to create a setter method.

public function header( $key, $value )
{
	$this->headers[ucfirst($key)] = $value;
}

ucfirst is loved by lazy folks like me. It uppercases ( if thats actually a word ) the first character of a string. This way you can create new responses like this:

$response = new Response( 'Hello World' );
$response->header( 'date', '13.09.1959' );

Building the header string

We made eveything so fancy abstracted but we cannot simply pass our response object to the socket writer. We need to build a string out of our data.

A http header response string will look like the following:

HTTP/1.1 200 OK
Date: 13.09.1959
Server: PHPServer

We have all the data we need so we can create the following function:

public function buildHeaderString()
{
	$lines = [];
		
	// response status 
	$lines[] = "HTTP/1.1 ".$this->status." ".static::$statusCodes[$this->status];
		
	// add the headers
	foreach( $this->headers as $key => $value )
	{
		$lines[] = $key.": ".$value;
	}
		
	return implode( " \r\n", $lines )."\r\n\r\n";
}

Let the magic happen

And because again we are all lazy fucks that don't want to execute buildHeaderString just to build a header string, we create the magic __toString method that returns the entire string written to the open connection.

public function __toString()
{
	return $this->buildHeaderString().$this->body;
}

Thats it!

Well thats everything, hopefully... You should now be able to start your server just like this:

$ sudo php server 8008

And access it with your browser:

http://127.0.0.1:8008/everything/you/want/?cool=yes

post-scriptum

When I started this article I did not thought it would be that that long and would consume so much of my time. So I have to admit that I got a bit annoyed after the first hour or so. Please excuse that the quality is not consistent or even close to good. But I hope the tutorial and especially the source might still help people who work primarily with PHP and are interested to better understand how a Webserver works.