Программирование на основе событий — странная тема для разработчиков PHP. На языке как процедурном; события немного больше, чем вызовы функций. Между событиями ничего не происходит, и весь значимый код все еще блокируется.
Такие языки, как JavaScript, показывают нам, каким может быть PHP, если бы в центре были циклы событий. Некоторые люди взяли эти идеи и закодировали их в циклы событий и HTTP-серверы. Сегодня мы собираемся создать HTTP-сервер на PHP. Мы подключим его к Apache, чтобы быстро обслуживать статические файлы. Все остальное пройдет через наш PHP HTTP сервер, основанный на Icicle.
Вы можете найти пример кода по адресу https://github.com/sitepoint-editors/icicle-http-server
Настройка Apache
Когда браузеры запрашивают существующие файлы, лучше всего обслуживать их без привлечения интерпретатора PHP. Apache быстр и эффективен в обслуживании этих файлов, поэтому давайте настроим его для обработки всех запросов статических файлов:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*) http://%{SERVER_NAME}:9001%{REQUEST_URI} [P]
Вы можете разместить этот код внутри записи виртуального хоста.
Эти директивы mod_rewrite
Другими словами: когда браузер запрашивает example.com/robots.txt
Если это так, Apache вернет его, не раскручивая интерпретатор PHP. Если нет, Apache отправит запрос на http://example.com:9001/robots.txt
Простой HTTP-сервер
Сосулька поставляется с петлей событий. Мы можем обернуть вокруг него HTTP-сервер, поэтому новые запросы приходят к нам в виде событий. Большая часть этого процесса абстрагирована, но в любом случае давайте рассмотрим пример. Для начала давайте icicleio/http
composer require icicleio/http
Это установленная версия 0.1.0
Если у вас возникли проблемы с работой моих примеров, возможно, у вас более новая версия. Попробуйте установить эту конкретную версию.
Это позволит вам запустить следующий код:
// 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();
Обработка разных маршрутов
Это самый простой HTTP-сервер, который можно создать. Он получает все запросы и ответы «Здравствуй, мир». Чтобы сделать его более полезным, нам нужно включить какой-то маршрутизатор. League\Route
composer require league/route
Теперь мы можем разделить отдельные запросы и отправить более значимые ответы:
// 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;
}
);
Мы включили League\Route
UriStrategy
Это один из четырех различных методов определения того, какой маршрут принадлежит какому запросу. League\Route
Нам нужно будет передать метод запроса и путь / цель диспетчеру.
Если маршрут совпадает, мы получаем ответ Symfony \ HttpFoundation, поэтому мы получаем содержимое тела с помощью getContent
Если нет соответствующего маршрута или разрешенного метода для соответствующего маршрута, мы возвращаем соответствующие ошибки. Итак, как же выглядит routes.php
$router->addRoute("GET", "/home", function() {
return "hello world";
});
Визуализация сложных видов
Строки хороши для простых страниц. Но когда мы начинаем создавать более сложные приложения, нам может понадобиться лучший инструмент. Как насчет использования League\Plates
Это механизм шаблонов, который добавляет такие вещи, как макеты и наследование шаблонов поверх простого PHP.
composer require league/plates
Затем мы создадим шаблон макета, чтобы все виды на нашем сайте могли наследовать:
<!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>
Это из templates/layout.php
Метод e
Метод section
<?php $this->layout("layout", ["title" => "Home"]); ?>
<p>
Hello, <?php print $this->e($name); ?>
</p>
Выше это из templates/home.php
Наконец, мы изменим наш /home
$router->addRoute("GET", "/home", function() {
$engine = new League\Plates\Engine(
__DIR__ . "/templates"
);
return $engine->render("home", [
"name" => "Chris"
]);
});
Выше от routes.php
Конечно, мы могли бы создать функцию быстрого доступа, чтобы избавить нас от необходимости каждый раз создавать движок:
function view($name, array $data = []) {
static $engine = null;
if ($engine === null) {
$engine = new League\Plates\Engine(
__DIR__ . "/templates"
);
}
return $engine->render($name, $data);
}
Выше от helpers.php
… И если мы включим это (или добавим в определение автозагрузки Composer), наш маршрут /home
$router->addRoute("GET", "/home", function() {
return view("home", [
"name" => "Chris"
]);
});
Вывод
Нам удалось собрать разумную прикладную среду, используя Icicle\Http
Надеюсь, это показало вам, что жизнь вне Apache (или Nginx) возможна. И это только начало …
Мне удалось получить следующую статистику (во время работы Chrome и iTunes на 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
Я полагаю, что эти цифры будут колебаться, когда вы добавляете больше сложности, и они ничего не значат по сравнению с популярными фреймворками. Дело в том, что этот маленький HTTP-сервер на основе событий может обслужить 11,1 тыс. Запросов за минуту без сбоев. Если вы будете осторожны, чтобы избежать утечек памяти, вы можете создать стабильный сервер из этого!
Это интересно, не правда ли?
Что вы думаете об этой настройке? Ты уже играл с Icicle? Дайте нам знать!
Редактировать: Аарон Пиотровски, автор Icicle, добавил некоторую дополнительную информацию о том, почему вышеупомянутый тест, возможно, был ошибочным (также обсужденный в комментариях). Вот его (гораздо более впечатляющие) результаты. Он говорит:
«Мне удалось получить следующую статистику (при запуске iTunes, Chrome и нескольких других программ на i7ac i7 3,4 ГГц) с помощью команды 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
Я полагаю, что эти цифры будут колебаться, когда вы добавляете больше сложности, и они ничего не значат по сравнению с популярными фреймворками. Дело в том, что этот маленький HTTP-сервер, основанный на событиях, потенциально может обслуживать более 100 000 запросов в минуту без сбоев. Если вы будете осторожны, чтобы избежать утечек памяти, вы можете создать стабильный сервер из этого!
Спасибо, что поделились, Аарон!