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). Пожалуйста, смотрите ЛИЦЕНЗИЮ для получения дополнительной информации.