Build a Superfast PHP Server in Minutes with Icicle
Event-based programming is a strange topic for PHP developers. In a language as procedural; events are little more than function calls. Nothing happens between events, and all meaningful code is still blocking.
Languages like JavaScript show us what PHP could be like if event loops were at the center. Some folks have taken these insights and coded them into event loops and HTTP servers. Today we’re going to create an HTTP server, in PHP. We’ll connect it to Apache to serve static files quickly. Everything else will pass through our PHP HTTP server, based on Icicle.
You can find the example code at https://github.com/sitepoint-editors/icicle-http-server
Configuring Apache
When browsers request existing files, it’s best to serve them without involving the PHP interpreter. Apache is fast and efficient at serving these files, so let’s configure it to handle all static file requests:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*) http://%{SERVER_NAME}:9001%{REQUEST_URI} [P]
You can place this code inside a virtual host entry.
These mod_rewrite
directives tell Apache to send requests to missing files to a different port. In other words: when a browser requests example.com/robots.txt
, Apache will first look to see if the file exists. If so, Apache will return it without spinning up the PHP interpreter. If not, Apache will send the request to http://example.com:9001/robots.txt
.
A Simple HTTP Server
Icicle ships with an event loop. We can wrap an HTTP server around that, so new requests come to us in the form of events. Much of this process is abstracted away, but let’s take a look at an example anyway. To begin, let’s download icicleio/http
:
composer require icicleio/http
This installed version 0.1.0
for me. If you’re having trouble getting my examples to work, you may have a newer version. Try installing this specific version.
This will allow you to run the following code:
// server.php
require __DIR__ . "/vendor/autoload.php";
use Icicle\Http\Message\RequestInterface;
use Icicle\Http\Message\Response;
use Icicle\Http\Server\Server;
use Icicle\Loop;
use Icicle\Socket\Client\ClientInterface;
$server = new Server(
function(RequestInterface $request, ClientInterface $client) {
$response = new Response(200);
$response = $response->withHeader(
"Content-Type", "text/plain"
);
yield $response->getBody()->end("hello world");
yield $response;
}
);
$server->listen(9001);
Loop\run();
Handling Different Routes
This is the most basic HTTP server one can create. It receives all requests and replies “hello world”. To make it more useful, we would need to incorporate some kind of router. League\Route
seems like a good candidate:
composer require league/route
Now we can split up individual requests, and send more meaningful responses:
// server.php
use League\Route\Http\Exception\MethodNotAllowedException;
use League\Route\Http\Exception\NotFoundException;
use League\Route\RouteCollection;
use League\Route\Strategy\UriStrategy;
$server = new Server(
function(RequestInterface $request, ClientInterface $client) {
$router = new RouteCollection();
$router->setStrategy(new UriStrategy());
require __DIR__ . "/routes.php";
$dispatcher = $router->getDispatcher();
try {
$result = $dispatcher->dispatch(
$request->getMethod(),
$request->getRequestTarget()
);
$status = 200;
$content = $result->getContent();
} catch (NotFoundException $exception) {
$status = 404;
$content = "not found";
} catch (MethodNotAllowedException $exception) {
$status = 405;
$content = "method not allowed";
}
$response = new Response($status);
$response = $response->withHeader(
"Content-Type", "text/html"
);
yield $response->getBody()->end($content);
yield $response;
}
);
We’ve pulled in League\Route
, and enabled the UriStrategy
. It’s one of four different methods for determining which route belongs to which request. League\Route
is often used alongside Symfony requests and responses. We’ll need to feed the request method and path/target to the dispatcher.
If a route is matched, we get a Symfony\HttpFoundation Response, so we get the body content with getContent
. If there isn’t a matching route, or an allowed method for a matching route, then we return the appropriate errors. So what does routes.php
look like?
$router->addRoute("GET", "/home", function() {
return "hello world";
});
Rendering Complex Views
Strings are fine for simple pages. But when we start to build more complex applications, we may need a better tool. How about we use League\Plates
? It’s a template engine that adds things like layouts and template inheritance on top of plain PHP.
composer require league/plates
Then we’ll create a layout template, for all the views in our site to inherit from:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>
<?php print $this->e($title); ?>
</title>
</head>
<body>
<?php print $this->section("content"); ?>
</body>
</html>
This is from templates/layout.php
.
The e
method escapes HTML entities. The section
method will be where the page content gets rendered:
<?php $this->layout("layout", ["title" => "Home"]); ?>
<p>
Hello, <?php print $this->e($name); ?>
</p>
The above is from templates/home.php
.
Finally, we change our /home
route to return a rendered template instead of a simple string:
$router->addRoute("GET", "/home", function() {
$engine = new League\Plates\Engine(
__DIR__ . "/templates"
);
return $engine->render("home", [
"name" => "Chris"
]);
});
The above is from routes.php
.
Of course, we could create a shortcut function, to save us having to create the engine each time:
function view($name, array $data = []) {
static $engine = null;
if ($engine === null) {
$engine = new League\Plates\Engine(
__DIR__ . "/templates"
);
}
return $engine->render($name, $data);
}
The above is from helpers.php
.
… and if we include that (or add it to the Composer autoload definition), then our /home
route becomes:
$router->addRoute("GET", "/home", function() {
return view("home", [
"name" => "Chris"
]);
});
Conclusion
We’ve managed to cobble together a reasonable application framework, using Icicle\Http
and a couple of League libraries. Hopefully this has shown you that life outside of Apache (or Nginx) is possible. And that’s just the beginning…
I was able to get the following stats (while running Chrome and iTunes, on a 13” Macbook Pro Retina 2014):
Concurrency Level: 100
Time taken for tests: 60.003 seconds
Complete requests: 11108
Failed requests: 0
Total transferred: 3810044 bytes
HTML transferred: 2243816 bytes
Requests per second: 185.12 [#/sec] (mean)
Time per request: 540.182 [ms] (mean)
Time per request: 5.402 [ms] (mean, across all concurrent requests)
Transfer rate: 62.01 [Kbytes/sec] received
I imagine those figures will fluctuate as you add more complexity, and they don’t mean anything when compared to popular frameworks. The point is that this little event-based HTTP server can serve 11.1k requests in a minute, without failures. If you’re careful to avoid memory leaks, you can create a stable server out of this!
That’s exciting, isn’t it?
What are your thoughts about this setup? Have you played with Icicle yet? Let us know!
Edit: Aaron Piotrowski, the author of Icicle chimed in with some extra info on why the benchmark above may have been flawed (also discussed in the comments). Here are his (much more impressive) results. He says:
“I was able to get the following stats (while running iTunes, Chrome, and several other programs on a 3.4 GHz i7 iMac) using the command ab -n 10000 -c 100 http://127.0.0.1:9001/home
”:
Concurrency Level: 100
Time taken for tests: 5.662 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 2650000 bytes
HTML transferred: 2020000 bytes
Requests per second: 1766.04 [#/sec] (mean)
Time per request: 56.624 [ms] (mean)
Time per request: 0.566 [ms] (mean, across all concurrent requests)
Transfer rate: 457.03 [Kbytes/sec] received
I imagine those figures will fluctuate as you add more complexity, and they don’t mean anything when compared to popular frameworks. The point is that this little event-based HTTP server could potentially serve over 100,000 requests in a minute, without failures. If you’re careful to avoid memory leaks, you can create a stable server out of this!
Thanks for chiming in, Aaron!