AMPHP — это коллекция управляемых событиями библиотек для PHP, разработанных с учетом волокон и параллелизма. Этот пакет предоставляет неблокирующий одновременный сервер приложений HTTP/1.1 и HTTP/2 для PHP на основе Revolt. Некоторые функции предоставляются в отдельных пакетах, например компонент WebSocket.
Этот пакет можно установить как зависимость Composer.
composer require amphp/http-server Кроме того, вы можете установить библиотеку nghttp2 , чтобы воспользоваться преимуществами FFI для ускорения и сокращения использования памяти.
Эта библиотека обеспечивает доступ к вашему приложению через протокол HTTP, принимая клиентские запросы и перенаправляя эти запросы обработчикам, определенным вашим приложением, которые возвращают ответ.
Входящие запросы представлены объектами Request . Запрос передается разработчику RequestHandler , который определяет метод handleRequest() возвращающий экземпляр Response .
public function handleRequest( Request $ request ): Response Обработчики запросов более подробно описаны в разделе RequestHandler .
Этот HTTP-сервер построен на основе цикла событий Revolt и платформы неблокируемого параллелизма Amp. Таким образом, он наследует полную поддержку всех своих примитивов и можно использовать все неблокирующие библиотеки, построенные на базе Revolt.
Примечание. В общем, вам следует ознакомиться с концепцией
Future, сопрограммами и знать несколько функций комбинатора, чтобы действительно преуспеть в использовании HTTP-сервера.
Почти каждая встроенная функция PHP блокирует ввод-вывод. Это означает, что исполняемый поток (в основном эквивалентный процессу в случае PHP) будет фактически остановлен до тех пор, пока не будет получен ответ. Несколько примеров таких функций: mysqli_query , file_get_contents , usleep и многие другие.
Хорошее эмпирическое правило: каждая встроенная функция PHP, выполняющая ввод-вывод, делает это блокирующим способом, если только вы точно не уверены, что это не так.
Существуют библиотеки, предоставляющие реализации, использующие неблокирующий ввод-вывод. Вы должны использовать их вместо встроенных функций.
Мы охватываем наиболее распространенные потребности ввода-вывода, такие как сетевые сокеты, доступ к файлам, HTTP-запросы и веб-сокеты, клиенты баз данных MySQL и Postgres, а также Redis. Если для выполнения запроса необходимо использовать блокирующий ввод-вывод или длительные вычисления, рассмотрите возможность использования параллельной библиотеки для запуска этого кода в отдельном процессе или потоке.
Предупреждение. Не используйте какие-либо функции блокировки ввода-вывода на HTTP-сервере.
// Here's a bad example, DO NOT do something like the following!
$ handler = new ClosureRequestHandler ( function () {
sleep ( 5 ); // Equivalent to a blocking I/O function with a 5 second timeout
return new Response ;
});
// Start a server with this handler and hit it twice.
// You'll have to wait until the 5 seconds are over until the second request is handled. Ваше приложение будет обслуживаться экземпляром HttpServer . Эта библиотека предоставляет SocketHttpServer , который подойдет для большинства приложений, построенных на компонентах, найденных в этой библиотеке и в amphp/socket .
Чтобы создать экземпляр SocketHttpServer и прослушивать запросы, требуется минимум четыре вещи:
RequestHandler для ответа на входящие запросы,ErrorHander для предоставления ответов на недействительные запросы,PsrLogLoggerInterface и <?php
use Amp ByteStream ;
use Amp Http HttpStatus ;
use Amp Http Server DefaultErrorHandler ;
use Amp Http Server Request ;
use Amp Http Server RequestHandler ;
use Amp Http Server Response ;
use Amp Http Server SocketHttpServer ;
use Amp Log ConsoleFormatter ;
use Amp Log StreamHandler ;
use Monolog Logger ;
use Monolog Processor PsrLogMessageProcessor ;
require __DIR__ . ' /vendor/autoload.php ' ;
// Note any PSR-3 logger may be used, Monolog is only an example.
$ logHandler = new StreamHandler ( ByteStream getStdout ());
$ logHandler -> pushProcessor ( new PsrLogMessageProcessor ());
$ logHandler -> setFormatter ( new ConsoleFormatter ());
$ logger = new Logger ( ' server ' );
$ logger -> pushHandler ( $ logHandler );
$ requestHandler = new class () implements RequestHandler {
public function handleRequest ( Request $ request ) : Response
{
return new Response (
status: HttpStatus:: OK ,
headers: [ ' Content-Type ' => ' text/plain ' ],
body: ' Hello, world! ' ,
);
}
};
$ errorHandler = new DefaultErrorHandler ();
$ server = SocketHttpServer:: createForDirectAccess ( $ logger );
$ server -> expose ( ' 127.0.0.1:1337 ' );
$ server -> start ( $ requestHandler , $ errorHandler );
// Serve requests until SIGINT or SIGTERM is received by the process.
Amp trapSignal ([ SIGINT , SIGTERM ]);
$ server -> stop ();В приведенном выше примере создается простой сервер, который отправляет текстовый ответ на каждый полученный запрос.
SocketHttpServer предоставляет два статических конструктора для распространенных случаев использования в дополнение к обычному конструктору для более сложных и пользовательских применений.
SocketHttpServer::createForDirectAccess() : используется в приведенном выше примере и создает сервер приложений HTTP, подходящий для прямого доступа к сети. Настраиваемые ограничения накладываются на количество подключений по IP, общее количество подключений и одновременные запросы (10, 1000 и 1000 по умолчанию соответственно). Сжатие ответов можно включать и выключать (включено по умолчанию), а методы запроса по умолчанию ограничены известным набором команд HTTP.SocketHttpServer::createForBehindProxy() : Создает сервер, подходящий для использования за прокси-службой, такой как nginx. Для этого статического конструктора требуется список доверенных IP-адресов прокси-серверов (с дополнительными масками подсети) и случай перечисления ForwardedHeaderType (соответствующий Forwarded или X-Forwarded-For ) для анализа исходного IP-адреса клиента из заголовков запросов. На количество подключений к серверу ограничений не налагается, однако количество одновременных запросов ограничено (по умолчанию 1000, настраивается или может быть удалено). Сжатие ответа можно включить или выключить (включено по умолчанию). По умолчанию методы запроса ограничены известным набором команд HTTP. Если ни один из этих методов не соответствует потребностям вашего приложения, конструктор SocketHttpServer можно использовать напрямую. Это обеспечивает огромную гибкость в создании и обработке входящих клиентских соединений, но для создания потребуется больше кода. Конструктор требует, чтобы пользователь передал экземпляр SocketServerFactory , используемый для создания клиентских экземпляров Socket (оба компонента библиотеки amphp/socket ), и экземпляр ClientFactory , который соответствующим образом создает экземпляры Client , которые прикрепляются к каждому Request , сделанному клиентом. .
RequestHandler Входящие запросы представлены объектами Request . Запрос передается разработчику RequestHandler , который определяет метод handleRequest() возвращающий экземпляр Response .
public function handleRequest( Request $ request ): Response Каждый клиентский запрос (т. е. вызов RequestHandler::handleRequest() ) выполняется в отдельной сопрограмме, поэтому запросы автоматически обрабатываются совместно в рамках серверного процесса. Когда обработчик запросов ожидает неблокирующего ввода-вывода, другие клиентские запросы обрабатываются в параллельных сопрограммах. Ваш обработчик запросов может сам создавать другие сопрограммы, используя Ampasync() для выполнения нескольких задач для одного запроса.
Обычно RequestHandler напрямую генерирует ответ, но он также может делегировать его другому RequestHandler . Примером такого делегирующего RequestHandler является Router .
Интерфейс RequestHandler предназначен для реализации с помощью пользовательских классов. Для очень простых случаев использования или быстрого макетирования вы можете использовать CallableRequestHandler , который может обернуть любой callable , принять Request и вернуть Response .
Промежуточное программное обеспечение позволяет осуществлять предварительную обработку запросов и постобработку ответов. Кроме того, промежуточное программное обеспечение также может перехватывать обработку запроса и возвращать ответ, не делегируя полномочия обработчику переданного запроса. Для этого классы должны реализовать интерфейс Middleware .
Примечание. Промежуточное программное обеспечение обычно следует за другими словами, такими как «программное обеспечение» и «аппаратное обеспечение», во множественном числе. Однако мы используем термин «промежуточное программное обеспечение» для обозначения нескольких объектов, реализующих интерфейс
Middleware.
public function handleRequest( Request $ request , RequestHandler $ next ): Response handleRequest — единственный метод интерфейса Middleware . Если Middleware не обрабатывает запрос само по себе, оно должно делегировать создание ответа полученному RequestHandler .
function stackMiddleware( RequestHandler $ handler , Middleware ... $ middleware ): RequestHandler Несколько промежуточных программ можно объединить с помощью AmpHttpServerMiddlewarestackMiddleware() , который принимает RequestHandler в качестве первого аргумента и переменное количество экземпляров Middleware . Возвращенный RequestHandler будет вызывать каждое промежуточное программное обеспечение в указанном порядке.
$ requestHandler = new class implements RequestHandler {
public function handleRequest ( Request $ request ): Response
{
return new Response (
status: HttpStatus:: OK ,
headers: [ " content-type " => " text/plain; charset=utf-8 " ],
body: " Hello, World! " ,
);
}
}
$ middleware = new class implements Middleware {
public function handleRequest ( Request $ request , RequestHandler $ next ): Response
{
$ requestTime = microtime ( true );
$ response = $ next -> handleRequest ( $ request );
$ response -> setHeader ( " x-request-time " , microtime ( true ) - $ requestTime );
return $ response ;
}
};
$ stackedHandler = Middleware stackMiddleware ( $ requestHandler , $ middleware );
$ errorHandler = new DefaultErrorHandler ();
// $logger is a PSR-3 logger instance.
$ server = SocketHttpServer:: createForDirectAccess ( $ logger );
$ server -> expose ( ' 127.0.0.1:1337 ' );
$ server -> start ( $ stackedHandler , $ errorHandler );ErrorHandler ErrorHander используется HTTP-сервером при получении некорректного или недействительного запроса. Объект Request предоставляется, если он создан на основе входящих данных, но не всегда может быть установлен.
public function handleError(
int $ status ,
? string $ reason = null ,
? Request $ request = null ,
): Response Эта библиотека предоставляет DefaultErrorHandler , который возвращает стилизованную HTML-страницу в качестве тела ответа. Возможно, вы захотите предоставить другую реализацию для своего приложения, возможно, используя несколько в сочетании с маршрутизатором.
Request Редко вам придется создавать объект Request самостоятельно, поскольку они обычно предоставляются RequestHandler::handleRequest() сервером.
/**
* @param string $method The HTTP method verb.
* @param array<string>|array<string, array<string>> $headers An array of strings or an array of string arrays.
*/
public function __construct(
private readonly Client $ client ,
string $ method ,
Psr Http Message UriInterface $ uri ,
array $ headers = [],
Amp ByteStream ReadableStream | string $ body = '' ,
private string $ protocol = ' 1.1 ' ,
? Trailers $ trailers = null ,
) public function getClient(): Client Возвращает Сlient отправившего запрос
public function getMethod(): string Возвращает метод HTTP, использованный для выполнения этого запроса, например "GET" .
public function setMethod( string $ method ): voidУстанавливает метод HTTP запроса.
public function getUri(): Psr Http Message UriInterface Возвращает URI запроса.
public function setUri( Psr Http Message UriInterface $ uri ): void Устанавливает новый URI для запроса.
public function getProtocolVersion(): stringВозвращает версию протокола HTTP в виде строки (например, «1.0», «1.1», «2»).
public function setProtocolVersion( string $ protocol )Устанавливает новый номер версии протокола для запроса.
/** @return array<non-empty-string, list<string>> */
public function getHeaders(): arrayВозвращает заголовки в виде индексированного по строкам массива массивов строк или пустого массива, если заголовки не заданы.
public function hasHeader( string $ name ): boolПроверяет, существует ли данный заголовок.
/** @return list<string> */
public function getHeaderArray( string $ name ): arrayВозвращает массив значений для данного заголовка или пустой массив, если заголовок не существует.
public function getHeader( string $ name ): ? string Возвращает значение данного заголовка. Если для именованного заголовка присутствует несколько заголовков, будет возвращено только первое значение заголовка. Используйте getHeaderArray() , чтобы вернуть массив всех значений для конкретного заголовка. Возвращает null , если заголовок не существует.
public function setHeaders( array $ headers ): voidУстанавливает заголовки из данного массива.
/** @param array<string>|string $value */
public function setHeader( string $ name , array | string $ value ): voidУстанавливает заголовок в заданные значения. Все предыдущие строки заголовка с указанным именем будут заменены.
/** @param array<string>|string $value */
public function addHeader( string $ name , array | string $ value ): voidДобавляет дополнительную строку заголовка с заданным именем.
public function removeHeader( string $ name ): voidУдаляет данный заголовок, если он существует. Если существует несколько строк заголовка с одинаковым именем, все они удаляются.
public function getBody(): RequestBody Возвращает тело запроса. RequestBody обеспечивает потоковый и буферизованный доступ к InputStream .
public function setBody( ReadableStream | string $ body )Устанавливает поток для тела сообщения
Примечание. Использование строки автоматически установит для заголовка
Content-Lengthдлину данной строки. УстановкаReadableStreamприведет к удалению заголовкаContent-Length. Если вы знаете точную длину контента вашего потока, вы можете добавить заголовокcontent-lengthпосле вызоваsetBody().
/** @return array<non-empty-string, RequestCookie> */
public function getCookies(): array Возвращает все файлы cookie в ассоциативной карте имен файлов cookie в RequestCookie .
public function getCookie( string $ name ): ? RequestCookie Получает значение файла cookie по имени или null .
public function setCookie( RequestCookie $ cookie ): void Добавляет Cookie в запрос.
public function removeCookie( string $ name ): voidУдаляет куки из запроса.
public function getAttributes(): arrayВозвращает массив всех атрибутов, хранящихся в изменяемом локальном хранилище запроса.
public function removeAttributes(): arrayУдаляет все атрибуты запроса из изменяемого локального хранилища запроса.
public function hasAttribute( string $ name ): boolПроверьте, существует ли атрибут с данным именем в изменяемом локальном хранилище запроса.
public function getAttribute( string $ name ): mixedПолучите переменную из изменяемого локального хранилища запроса.
Примечание. Имя атрибута должно находиться в пространстве имен поставщика и пространства имен пакета, например классов.
public function setAttribute( string $ name , mixed $ value ): voidНазначьте переменную изменяемому локальному хранилищу запроса.
Примечание. Имя атрибута должно находиться в пространстве имен поставщика и пространства имен пакета, например классов.
public function removeAttribute( string $ name ): voidУдаляет переменную из изменяемого локального хранилища запроса.
public function getTrailers(): Trailers Разрешает доступ к Trailers запроса.
public function setTrailers( Trailers $ trailers ): void Назначает объект Trailers , который будет использоваться в запросе.
Сведения, связанные с клиентом, объединяются в объекты AmpHttpServerDriverClient возвращаемые из Request::getClient() . Client интерфейс предоставляет методы для получения адресов удаленных и локальных сокетов и информации TLS (если применимо).
Response Класс Response представляет ответ HTTP. Response возвращается обработчиками запросов и промежуточным программным обеспечением.
/**
* @param int $code The HTTP response status code.
* @param array<string>|array<string, array<string>> $headers An array of strings or an array of string arrays.
*/
public function __construct(
int $ code = HttpStatus:: OK ,
array $ headers = [],
Amp ByteStream ReadableStream | string $ body = '' ,
? Trailers $ trailers = null ,
) Вызывает обработчики удаления (т.е. функции, зарегистрированные с помощью метода onDispose() ).
Примечание. Неперехваченные исключения из обработчиков удаления будут перенаправлены в обработчик ошибок цикла событий.
public function getBody(): Amp ByteStream ReadableStreamВозвращает поток для тела сообщения.
public function setBody( Amp ByteStream ReadableStream | string $ body )Устанавливает поток для тела сообщения.
Примечание. Использование строки автоматически установит для заголовка
Content-Lengthдлину данной строки. УстановкаReadableStreamприведет к удалению заголовкаContent-Length. Если вы знаете точную длину контента вашего потока, вы можете добавить заголовокcontent-lengthпосле вызоваsetBody().
/** @return array<non-empty-string, list<string>> */
public function getHeaders(): arrayВозвращает заголовки в виде индексированного по строкам массива массивов строк или пустого массива, если заголовки не заданы.
public function hasHeader( string $ name ): boolПроверяет, существует ли данный заголовок.
/** @return list<string> */
public function getHeaderArray( string $ name ): arrayВозвращает массив значений для данного заголовка или пустой массив, если заголовок не существует.
public function getHeader( string $ name ): ? string Возвращает значение данного заголовка. Если для именованного заголовка присутствует несколько заголовков, будет возвращено только первое значение заголовка. Используйте getHeaderArray() , чтобы вернуть массив всех значений для конкретного заголовка. Возвращает null , если заголовок не существует.
public function setHeaders( array $ headers ): voidУстанавливает заголовки из данного массива.
/** @param array<string>|string $value */
public function setHeader( string $ name , array | string $ value ): voidУстанавливает заголовок в заданные значения. Все предыдущие строки заголовка с указанным именем будут заменены.
/** @param array<string>|string $value */
public function addHeader( string $ name , array | string $ value ): voidДобавляет дополнительную строку заголовка с заданным именем.
public function removeHeader( string $ name ): voidУдаляет данный заголовок, если он существует. Если существует несколько строк заголовка с одинаковым именем, все они удаляются.
public function getStatus(): intВозвращает код состояния ответа.
public function getReason(): stringВозвращает фразу причины, описывающую код состояния.
public function setStatus( int $ code , string | null $ reason ): voidУстанавливает числовой код состояния HTTP (от 100 до 599) и фразу причины. Используйте значение null в качестве фразы причины, чтобы использовать фразу по умолчанию, связанную с кодом состояния.
/** @return array<non-empty-string, ResponseCookie> */
public function getCookies(): array Возвращает все файлы cookie в ассоциативной карте имен файлов cookie в ResponseCookie .
public function getCookie( string $ name ): ? ResponseCookie Получает значение файла cookie по имени или null , если файл cookie с таким именем отсутствует.
public function setCookie( ResponseCookie $ cookie ): voidДобавляет файл cookie в ответ.
public function removeCookie( string $ name ): voidУдаляет файл cookie из ответа.
/** @return array<string, Push> Map of URL strings to Push objects. */
public function getPushes(): array Возвращает список ресурсов push в ассоциативной карте строк URL-адресов для объектов Push .
/** @param array<string>|array<string, array<string>> $headers */
public function push( string $ url , array $ headers ): void Укажите ресурсы, которые, вероятно, потребуется клиенту получить. (например, Link: preload или HTTP/2 Server Push).
public function isUpgraded(): bool Возвращает true если установлен обратный вызов отсоединения, false если его нет.
/** @param Closure(DriverUpgradedSocket, Request, Response): void $upgrade */
public function upgrade( Closure $ upgrade ): void Устанавливает обратный вызов, который будет вызываться после записи ответа клиенту, и меняет статус ответа на 101 Switching Protocols . Обратный вызов получает экземпляр DriverUpgradedSocket , Request , который инициировал обновление, и этот Response .
Обратный вызов можно удалить, изменив статус на значение, отличное от 101.
public function getUpgradeCallable(): ? ClosureВозвращает функцию обновления, если она присутствует.
/** @param Closure():void $onDispose */
public function onDispose( Closure $ onDispose ): voidРегистрирует функцию, которая вызывается при отмене ответа. Ответ отбрасывается либо после того, как он был записан клиенту, либо в случае его замены в цепочке промежуточного программного обеспечения.
public function getTrailers(): Trailers Разрешает доступ к Trailers ответа.
public function setTrailers( Trailers $ trailers ): void Назначает объект Trailers , который будет использоваться в ответе. Трейлеры отправляются после того, как клиенту будет передано все тело ответа.
RequestBody , возвращаемый из Request::getBody() , обеспечивает буферизованный и потоковый доступ к телу запроса. Используйте потоковый доступ для обработки больших сообщений, что особенно важно, если у вас большие ограничения на сообщения (например, десятки мегабайт) и вы не хотите буферизовать все это в памяти. Если несколько человек одновременно загружают большие тела, память может быстро исчерпаться.
Следовательно, важна инкрементная обработка, доступная через API read() AmpByteStreamReadableStream .
В случае отключения клиента read() завершается с ошибкой AmpHttpServerClientException . Это исключение генерируется как для API read() так и для buffer() .
Примечание.
ClientExceptionне обязательно перехватывать. Вы можете поймать их, если хотите продолжить, но это не обязательно. Сервер молча завершит цикл запроса и затем отменит это исключение.
Вместо того, чтобы устанавливать высокий общий предел тела, вам следует рассмотреть возможность увеличения предела тела только там, где это необходимо, что динамически возможно с помощью метода increaseSizeLimit() в RequestBody .
Примечание. Сам
RequestBodyне обеспечивает анализ данных формы. Если вам это нужно, вы можете использоватьamphp/http-server-form-parser.
Как и в случае с Request , создание экземпляра RequestBody требуется редко, поскольку он будет предоставлен как часть Request .
public function __construct(
ReadableStream | string $ stream ,
? Closure $ upgradeSize = null ,
) public function increaseSizeLimit( int $ limit ): voidДинамически увеличивает предел размера тела, чтобы позволить отдельным обработчикам запросов обрабатывать тела запросов большего размера, чем набор по умолчанию для HTTP-сервера.
Класс Trailers обеспечивает доступ к трейлерам HTTP-запроса, доступным через Request::getTrailers() . null возвращается, если по запросу не ожидаются трейлеры. Trailers::await() возвращает Future , которое разрешается с помощью объекта HttpMessage предоставляющего методы для доступа к заголовкам трейлера.
$ trailers = $ request -> getTrailers ();
$ message = $ trailers ?->await();HTTP-сервер не будет узким местом. Неправильная конфигурация, использование блокировки ввода-вывода или неэффективные приложения.
Сервер хорошо оптимизирован и может обрабатывать десятки тысяч запросов в секунду на типичном оборудовании, сохраняя при этом высокий уровень одновременного выполнения тысяч клиентов.
Но эта производительность резко снизится при использовании неэффективных приложений. Сервер имеет то преимущество, что классы и обработчики загружаются всегда, поэтому на компиляцию и инициализацию не тратится время.
Распространенной ловушкой является начало работы с большими данными с помощью простых строковых операций, требующих множества неэффективных больших копий. Вместо этого, где это возможно, следует использовать потоковую передачу для более крупных тел запросов и ответов.
Проблема действительно в стоимости процессора. Неэффективное управление вводом-выводом (пока оно неблокируется!) просто задерживает отдельные запросы. Рекомендуется отправлять одновременно и в конечном итоге объединять несколько независимых запросов ввода-вывода через комбинаторы Amp, но медленный обработчик также замедлит каждый другой запрос. Пока один обработчик выполняет вычисления, все остальные обработчики не могут продолжать работу. Таким образом, крайне важно сократить время вычислений обработчиков до минимума.
Несколько примеров можно найти в каталоге ./examples репозитория. Их можно выполнять как обычные сценарии PHP в командной строке.
php examples/hello-world.php Затем вы можете получить доступ к примеру сервера по адресу http://localhost:1337/ в своем браузере.
Если вы обнаружите какие-либо проблемы, связанные с безопасностью, используйте частное средство сообщения о проблемах безопасности вместо общедоступного средства отслеживания проблем.
Лицензия MIT (MIT). Пожалуйста, смотрите ЛИЦЕНЗИЮ для получения дополнительной информации.