Hard📖Теория13 min

Async экосистема

Revolt, AMPHP v3, ReactPHP — event loop, promises, async HTTP, WebSockets

Async экосистема PHP

Проблема: блокирующий I/O

В традиционном PHP каждая операция ввода-вывода блокирует процесс. Пока ответ от базы данных, HTTP-сервиса или файловой системы не получен, PHP просто ждёт.

Синхронный PHP (типичный):
────── fetch(api1) ██████████░░░░░░░░░░░ fetch(api2) ██████████░░░░░░░░░░░ render
       200ms waiting                      200ms waiting                     = 400ms total

Асинхронный PHP:
────── fetch(api1) ██████████
────── fetch(api2) ██████████
                              ░ render
                              = 200ms total (оба запроса параллельно)

Ключевой принцип: Async PHP не делает код быстрее вычислительно. Он устраняет время ожидания I/O, выполняя несколько операций ввода-вывода одновременно в одном потоке.

Event Loop — сердце async

Event Loop (цикл событий) — бесконечный цикл, который отслеживает готовность I/O-операций (сокеты, таймеры, сигналы) и вызывает соответствующие callback-и или возобновляет Fibers.

┌─────────────────────────────────┐
│          Event Loop             │
│                                 │
│  1. Проверить таймеры           │
│  2. Проверить I/O (poll/epoll)  │
│  3. Выполнить готовые callback  │
│  4. Повторить                   │
│                                 │
│  ┌──────┐ ┌──────┐ ┌──────┐    │
│  │Timer │ │Socket│ │Signal│    │
│  │Queue │ │Watch │ │Watch │    │
│  └──────┘ └──────┘ └──────┘    │
└─────────────────────────────────┘

Revolt — стандартный Event Loop

Revolt — это стандартный event loop для PHP, созданный разработчиками AMPHP. Все библиотеки AMPHP v3 используют Revolt. Это единый event loop, который работает с Fibers.

Установка

composer require revolt/event-loop

Базовые операции

<?php
declare(strict_types=1);

use Revolt\EventLoop;

// defer() — выполнить на следующей итерации цикла
EventLoop::defer(function (): void {
    echo "Deferred callback executed\n";
});

// delay() — выполнить через N секунд
EventLoop::delay(1.5, function (): void {
    echo "Executed after 1.5 seconds\n";
});

// repeat() — выполнять каждые N секунд
$callbackId = EventLoop::repeat(0.5, function (string $callbackId): void {
    static $count = 0;
    echo "Tick " . (++$count) . "\n";

    if ($count >= 5) {
        EventLoop::cancel($callbackId);  // Stop repeating
    }
});

// Run the event loop
EventLoop::run();

I/O наблюдатели (watchers)

<?php
declare(strict_types=1);

use Revolt\EventLoop;

// onReadable() — вызвать callback когда поток готов к чтению
$socket = stream_socket_client('tcp://example.com:80');
stream_set_blocking($socket, false);

// Send HTTP request
fwrite($socket, "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n");

EventLoop::onReadable($socket, function (string $callbackId, $stream): void {
    $data = fread($stream, 8192);

    if ($data === '' || $data === false || feof($stream)) {
        EventLoop::cancel($callbackId);
        fclose($stream);
        echo "Connection closed. Received data.\n";
        return;
    }

    echo "Received " . strlen($data) . " bytes\n";
});

// onWritable() — вызвать когда поток готов к записи
// Useful for non-blocking writes to sockets
$server = stream_socket_server('tcp://127.0.0.1:8080');
stream_set_blocking($server, false);

EventLoop::onReadable($server, function (string $callbackId, $server): void {
    $client = @stream_socket_accept($server, 0);
    if ($client === false) {
        return;
    }

    stream_set_blocking($client, false);

    EventLoop::onReadable($client, function (string $cbId, $client): void {
        $data = fread($client, 8192);
        if ($data === '' || $data === false) {
            EventLoop::cancel($cbId);
            fclose($client);
            return;
        }
        // Echo server: send back what we received
        fwrite($client, $data);
    });
});

// onSignal() — реакция на POSIX сигналы
EventLoop::onSignal(SIGINT, function (): void {
    echo "\nReceived SIGINT, shutting down...\n";
    EventLoop::getDriver()->stop();
});

EventLoop::run();

Suspension API

<?php
declare(strict_types=1);

use Revolt\EventLoop;
use Revolt\EventLoop\Suspension;

// Suspension — мост между event loop и Fibers
// Позволяет приостановить Fiber до наступления события

function asyncDelay(float $seconds): void
{
    $suspension = EventLoop::getSuspension();

    EventLoop::delay($seconds, function () use ($suspension): void {
        $suspension->resume();  // Wake up the fiber
    });

    $suspension->suspend();  // Pause the fiber
}

function asyncReadLine(string $prompt): string
{
    echo $prompt;

    $suspension = EventLoop::getSuspension();

    EventLoop::onReadable(STDIN, function (string $id) use ($suspension): void {
        EventLoop::cancel($id);
        $line = trim((string) fgets(STDIN));
        $suspension->resume($line);
    });

    return $suspension->suspend();
}

// Usage inside a fiber (Revolt runs code in fibers automatically)
EventLoop::queue(function (): void {
    echo "Starting...\n";
    asyncDelay(1.0);
    echo "1 second passed\n";
    asyncDelay(0.5);
    echo "0.5 more seconds\n";
});

EventLoop::run();

Отмена (Cancellation)

<?php
declare(strict_types=1);

use Revolt\EventLoop;

// Cancel a callback by its ID
$id = EventLoop::delay(5.0, function (): void {
    echo "This will not execute\n";
});

EventLoop::delay(1.0, function () use ($id): void {
    EventLoop::cancel($id);
    echo "Cancelled the 5-second timer\n";
});

// Enable/Disable callbacks (pause/unpause)
$repeatId = EventLoop::repeat(0.5, function (): void {
    echo "Tick\n";
});

EventLoop::delay(2.0, function () use ($repeatId): void {
    EventLoop::disable($repeatId);
    echo "Paused ticking\n";
});

EventLoop::delay(4.0, function () use ($repeatId): void {
    EventLoop::enable($repeatId);
    echo "Resumed ticking\n";
});

EventLoop::delay(6.0, function () use ($repeatId): void {
    EventLoop::cancel($repeatId);
    echo "Stopped\n";
});

EventLoop::run();

AMPHP v3

AMPHP v3 — набор библиотек для асинхронного PHP, построенных на Revolt event loop и Fibers. Ключевое отличие от v2: вместо Promise используется Future, а благодаря Fibers async код выглядит как синхронный.

Ключевые концепции

<?php
declare(strict_types=1);

use Amp\Future;
use function Amp\async;
use function Amp\delay;

// async() — запустить функцию конкурентно (в отдельном Fiber)
$future1 = async(function (): string {
    delay(1);  // Non-blocking sleep (1 second)
    return 'Result from task 1';
});

$future2 = async(function (): string {
    delay(0.5);
    return 'Result from task 2';
});

// await() — дождаться результата (блокирует текущий Fiber, не поток!)
$result1 = $future1->await();  // Waits for future to complete
$result2 = $future2->await();

echo "{$result1}\n{$result2}\n";
// Total time: ~1 second (not 1.5), because tasks run concurrently

Amp\Future

<?php
declare(strict_types=1);

use Amp\Future;
use function Amp\async;
use function Amp\delay;

// Future::await() — получить результат
$future = async(fn () => 42);
$value = $future->await();  // 42

// Future::await() with timeout
use Amp\TimeoutCancellation;

$future = async(function (): string {
    delay(10);  // 10 seconds
    return 'done';
});

try {
    // Wait max 2 seconds
    $result = $future->await(new TimeoutCancellation(2));
} catch (\Amp\CancelledException $e) {
    echo "Timed out!\n";
}

// Future::map() — transform the result
$future = async(fn () => 42);
$doubled = $future->map(fn (int $v) => $v * 2);
echo $doubled->await();  // 84

// Future::catch() — handle errors
$future = async(function (): never {
    throw new RuntimeException('Oops');
});
$safe = $future->catch(fn (\Throwable $e) => "Error: {$e->getMessage()}");
echo $safe->await();  // "Error: Oops"

// Await multiple futures
$futures = [];
for ($i = 0; $i < 5; $i++) {
    $futures[] = async(function () use ($i): int {
        delay(0.1 * $i);
        return $i * 10;
    });
}

// Wait for ALL futures
$results = Future\await($futures);
// [0, 10, 20, 30, 40]

// Wait for FIRST completed future
$firstResult = Future\awaitFirst($futures);

// Wait for ANY N futures
$anyTwo = Future\awaitAny($futures, 2);

Async HTTP клиент (amphp/http-client)

composer require amphp/http-client
<?php
declare(strict_types=1);

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future;

// Create HTTP client
$client = HttpClientBuilder::buildDefault();

// Concurrent requests
$urls = [
    'https://httpbin.org/delay/1',
    'https://httpbin.org/delay/2',
    'https://httpbin.org/delay/1',
    'https://httpbin.org/get',
    'https://httpbin.org/ip',
];

$startTime = microtime(true);

$futures = array_map(
    fn (string $url) => async(function () use ($client, $url): string {
        $request = new Request($url);
        $response = $client->request($request);
        $body = $response->getBody()->buffer();
        return "URL: {$url} — Status: {$response->getStatus()}, Size: " . strlen($body);
    }),
    $urls,
);

// Wait for all responses
$results = Future\await($futures);

$elapsed = microtime(true) - $startTime;
echo "Completed in " . number_format($elapsed, 2) . " seconds\n";
// ~2 seconds (not 5), because requests run concurrently

foreach ($results as $result) {
    echo "{$result}\n";
}

POST запрос с телом

<?php
declare(strict_types=1);

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use Amp\Http\Client\Body\FormBody;
use Amp\Http\Client\Body\JsonBody;

$client = HttpClientBuilder::buildDefault();

// JSON POST
$request = new Request('https://httpbin.org/post', 'POST');
$request->setHeader('Content-Type', 'application/json');
$request->setBody(json_encode(['name' => 'John', 'age' => 30]));

$response = $client->request($request);
echo $response->getBody()->buffer();

// Form POST
$body = new FormBody();
$body->addField('username', 'admin');
$body->addField('password', 'secret');

$request = new Request('https://httpbin.org/post', 'POST');
$request->setBody($body);

$response = $client->request($request);
echo $response->getBody()->buffer();

Параллельные вычисления (amphp/parallel)

composer require amphp/parallel
<?php
declare(strict_types=1);

use Amp\Parallel\Worker;
use function Amp\async;
use function Amp\Future;

// Run CPU-intensive tasks in separate processes/threads
$futures = [];
for ($i = 0; $i < 4; $i++) {
    $futures[] = async(function () use ($i): string {
        // This runs in a SEPARATE worker process
        $result = Worker\submit(new class($i) implements Worker\Task {
            public function __construct(
                private readonly int $id,
            ) {}

            public function run(Worker\Sync $sync): string
            {
                // Heavy computation in a worker
                $hash = hash('sha256', str_repeat((string) $this->id, 1_000_000));
                return "Worker {$this->id}: {$hash}";
            }
        });

        return $result->await();
    });
}

$results = Future\await($futures);
foreach ($results as $result) {
    echo "{$result}\n";
}

Async TCP-сокет (amphp/socket)

composer require amphp/socket
<?php
declare(strict_types=1);

use Amp\Socket;
use function Amp\async;

// Simple TCP echo server
$server = Socket\listen('127.0.0.1:1337');
echo "Server listening on 127.0.0.1:1337\n";

while ($client = $server->accept()) {
    // Each client handled in its own fiber
    async(function () use ($client): void {
        $address = $client->getRemoteAddress()->toString();
        echo "Client connected: {$address}\n";

        while (($chunk = $client->read()) !== null) {
            echo "Received from {$address}: {$chunk}";
            $client->write("Echo: {$chunk}");
        }

        echo "Client disconnected: {$address}\n";
        $client->close();
    });
}

ReactPHP

ReactPHP — первая и самая зрелая async-библиотека для PHP. Появилась задолго до Fibers и основана на callback/promise подходе. С PHP 8.1 также поддерживает Fibers через адаптеры.

Event Loop

<?php
declare(strict_types=1);

// ReactPHP v3 uses event loop implicitly through the Loop class
use React\EventLoop\Loop;

// Timers
Loop::addTimer(1.0, function (): void {
    echo "Executed once after 1 second\n";
});

$counter = 0;
Loop::addPeriodicTimer(0.5, function ($timer) use (&$counter): void {
    echo "Tick " . (++$counter) . "\n";
    if ($counter >= 5) {
        Loop::cancelTimer($timer);
    }
});

// Deferred execution
Loop::futureTick(function (): void {
    echo "Runs on next tick\n";
});

// The loop runs automatically when the script has pending events

Promises

<?php
declare(strict_types=1);

use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use function React\Promise\resolve;
use function React\Promise\reject;
use function React\Promise\all;
use function React\Promise\race;
use function React\Promise\any;

// Creating a promise
function asyncOperation(): PromiseInterface
{
    $deferred = new Deferred();

    // Simulate async work
    Loop::addTimer(1.0, function () use ($deferred): void {
        $deferred->resolve('Operation completed');
    });

    return $deferred->promise();
}

// Using promises
asyncOperation()->then(
    function (string $result): void {
        echo "Success: {$result}\n";
    },
    function (\Throwable $error): void {
        echo "Error: {$error->getMessage()}\n";
    }
);

// Chaining promises
asyncOperation()
    ->then(fn (string $result) => strtoupper($result))
    ->then(fn (string $upper) => "Processed: {$upper}")
    ->then(fn (string $final) => echo "{$final}\n");

// Immediate values
$resolved = resolve(42);
$rejected = reject(new RuntimeException('Failure'));

// Combining promises
$promise1 = resolve('A');
$promise2 = resolve('B');
$promise3 = resolve('C');

// all() — wait for ALL (fail on first error)
all([$promise1, $promise2, $promise3])
    ->then(fn (array $results) => print_r($results));
// ['A', 'B', 'C']

// race() — first to settle (resolve or reject)
race([$promise1, $promise2])
    ->then(fn ($first) => echo "First: {$first}\n");

// any() — first to RESOLVE (ignores rejections)
any([$rejected, $promise1, $promise2])
    ->then(fn ($first) => echo "First success: {$first}\n");

Async HTTP-сервер (reactphp/http)

composer require react/http
<?php
declare(strict_types=1);

use React\Http\HttpServer;
use React\Http\Message\Response;
use React\Socket\SocketServer;
use Psr\Http\Message\ServerRequestInterface;

// Create HTTP server
$http = new HttpServer(function (ServerRequestInterface $request): Response {
    $method = $request->getMethod();
    $path = $request->getUri()->getPath();

    return match (true) {
        $path === '/' => new Response(
            status: 200,
            headers: ['Content-Type' => 'text/plain'],
            body: "Hello from ReactPHP!\n",
        ),
        $path === '/json' => new Response(
            status: 200,
            headers: ['Content-Type' => 'application/json'],
            body: json_encode(['time' => time(), 'method' => $method]),
        ),
        $path === '/stream' => new Response(
            status: 200,
            headers: ['Content-Type' => 'text/plain'],
            body: generateStream(),  // Returns a ReadableStreamInterface
        ),
        default => new Response(
            status: 404,
            body: "Not Found\n",
        ),
    };
});

$socket = new SocketServer('0.0.0.0:8080');
$http->listen($socket);

echo "Server running at http://127.0.0.1:8080\n";

Async HTTP-клиент (reactphp/http)

<?php
declare(strict_types=1);

use React\Http\Browser;
use React\EventLoop\Loop;
use function React\Promise\all;

$browser = new Browser();

// GET request
$browser->get('https://httpbin.org/get')
    ->then(function (Psr\Http\Message\ResponseInterface $response): void {
        echo "Status: {$response->getStatusCode()}\n";
        echo "Body: {$response->getBody()}\n";
    });

// POST request with JSON
$browser->post(
    'https://httpbin.org/post',
    ['Content-Type' => 'application/json'],
    json_encode(['key' => 'value']),
)->then(function ($response): void {
    echo $response->getBody() . "\n";
});

// Concurrent requests
$urls = [
    'https://httpbin.org/delay/1',
    'https://httpbin.org/delay/2',
    'https://httpbin.org/delay/1',
];

$promises = array_map(
    fn (string $url) => $browser->get($url),
    $urls,
);

$start = microtime(true);

all($promises)->then(function (array $responses) use ($start): void {
    $elapsed = microtime(true) - $start;
    echo "All " . count($responses) . " requests completed in "
        . number_format($elapsed, 2) . " seconds\n";
    // ~2 seconds (not 4)
});

TCP-сервер (reactphp/socket)

<?php
declare(strict_types=1);

use React\Socket\SocketServer;
use React\Socket\ConnectionInterface;

$server = new SocketServer('127.0.0.1:4000');

$server->on('connection', function (ConnectionInterface $conn): void {
    $addr = $conn->getRemoteAddress();
    echo "New connection: {$addr}\n";

    $conn->write("Welcome! Type something:\n");

    $conn->on('data', function (string $data) use ($conn, $addr): void {
        $data = trim($data);
        echo "[{$addr}] {$data}\n";
        $conn->write("Echo: {$data}\n");

        if ($data === 'quit') {
            $conn->end("Bye!\n");
        }
    });

    $conn->on('close', function () use ($addr): void {
        echo "Connection closed: {$addr}\n";
    });
});

echo "TCP server listening on 127.0.0.1:4000\n";

Дочерние процессы (reactphp/child-process)

composer require react/child-process
<?php
declare(strict_types=1);

use React\ChildProcess\Process;
use React\EventLoop\Loop;

// Run external command
$process = new Process('ls -la /tmp');

$process->start();

$process->stdout->on('data', function (string $chunk): void {
    echo "STDOUT: {$chunk}";
});

$process->stderr->on('data', function (string $chunk): void {
    echo "STDERR: {$chunk}";
});

$process->on('exit', function (int $exitCode, ?int $termSignal): void {
    echo "Process exited with code {$exitCode}\n";
});

// Run PHP script in background
$worker = new Process('php worker.php');
$worker->start();

// Send data to worker via stdin
$worker->stdin->write("process this data\n");
$worker->stdin->end();

Сравнение: AMPHP v3 vs ReactPHP

Аспект AMPHP v3 ReactPHP
Event Loop Revolt (стандарт) Собственный (React\EventLoop)
Стиль кода Синхронный (Fibers) Callback/Promise
Минимальный PHP 8.1+ (Fibers обязательны) 8.1+ (v3)
Future/Promise Amp\Future React\Promise
HTTP-клиент amphp/http-client react/http (Browser)
HTTP-сервер amphp/http-server react/http (HttpServer)
Параллелизм amphp/parallel (workers) react/child-process
WebSocket amphp/websocket ratchet/pawl
Подход Современный, Fiber-first Зрелый, широкая экосистема
Код $result = $client->request($req) $client->get($url)->then(...)

Стиль кода — ключевое отличие

<?php
declare(strict_types=1);

// AMPHP v3 — выглядит как синхронный код
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;

function amphpStyle(): void
{
    $client = HttpClientBuilder::buildDefault();

    // Looks synchronous, but is non-blocking!
    $response = $client->request(new Request('https://example.com'));
    $body = $response->getBody()->buffer();

    echo "Got: " . strlen($body) . " bytes\n";
}

// ReactPHP — callback/promise chain
use React\Http\Browser;

function reactStyle(): void
{
    $browser = new Browser();

    $browser->get('https://example.com')
        ->then(function ($response) {
            $body = (string) $response->getBody();
            echo "Got: " . strlen($body) . " bytes\n";
        })
        ->catch(function (\Throwable $e) {
            echo "Error: {$e->getMessage()}\n";
        });
}

Рекомендация: Для новых проектов предпочитайте AMPHP v3 — код выглядит как обычный PHP, легче читается и отлаживается. ReactPHP отлично подходит, если вы уже знакомы с его экосистемой или используете библиотеки, которые есть только для ReactPHP.

Когда использовать Async PHP

Подходит отлично

  • WebSocket-сервер — постоянные соединения, двусторонний обмен
  • Real-time чат — множество одновременных подключений
  • API Gateway — агрегация данных из нескольких сервисов
  • Long Polling — держать соединение открытым
  • Streaming — передача данных по частям (Server-Sent Events)
  • Очереди задач — обработка сообщений из RabbitMQ/Redis
  • Микросервисы — легковесные HTTP-серверы
  • Веб-скрапинг — параллельный парсинг множества URL
  • CLI-инструменты — параллельное выполнение задач

НЕ подходит

  • Типичный CRUD API — один запрос = один ответ, PHP-FPM справляется отлично
  • Рендеринг HTML-страниц — нет выигрыша от async
  • CPU-интенсивные задачи — async не добавляет параллелизм (один поток)
  • Простые скрипты — оверхед не оправдан
Правило большого пальца:
├── Много I/O ожидания?          → Async поможет
├── Много одновременных клиентов? → Async поможет
├── CPU-bound вычисления?         → Async НЕ поможет (используй amphp/parallel)
└── Простой request/response?     → PHP-FPM достаточно

Практический пример: Async HTTP-клиент

10 параллельных запросов с AMPHP

<?php
declare(strict_types=1);

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future;

require __DIR__ . '/vendor/autoload.php';

$client = HttpClientBuilder::buildDefault();

$urls = [
    'https://jsonplaceholder.typicode.com/posts/1',
    'https://jsonplaceholder.typicode.com/posts/2',
    'https://jsonplaceholder.typicode.com/posts/3',
    'https://jsonplaceholder.typicode.com/users/1',
    'https://jsonplaceholder.typicode.com/users/2',
    'https://jsonplaceholder.typicode.com/comments/1',
    'https://jsonplaceholder.typicode.com/comments/2',
    'https://jsonplaceholder.typicode.com/todos/1',
    'https://jsonplaceholder.typicode.com/albums/1',
    'https://jsonplaceholder.typicode.com/photos/1',
];

$start = hrtime(true);

$futures = array_map(
    fn (string $url) => async(function () use ($client, $url): array {
        $request = new Request($url);
        $response = $client->request($request);
        $body = $response->getBody()->buffer();

        return [
            'url' => $url,
            'status' => $response->getStatus(),
            'size' => strlen($body),
            'data' => json_decode($body, true),
        ];
    }),
    $urls,
);

// Await all — returns when ALL are done
$results = Future\await($futures);

$elapsed = (hrtime(true) - $start) / 1_000_000;  // Convert to ms

echo "Fetched " . count($results) . " URLs in " . number_format($elapsed, 1) . " ms\n\n";

foreach ($results as $result) {
    echo sprintf(
        "  %s — %d (%d bytes)\n",
        $result['url'],
        $result['status'],
        $result['size'],
    );
}

// Sequential comparison:
// 10 requests × ~100ms each = ~1000ms
// Concurrent: ~100-200ms (all in parallel)

WebSocket Echo Server с AMPHP

composer require amphp/websocket-server amphp/http-server
<?php
declare(strict_types=1);

use Amp\Http\HttpStatus;
use Amp\Http\Server\DefaultErrorHandler;
use Amp\Http\Server\Request;
use Amp\Http\Server\Response;
use Amp\Http\Server\Router;
use Amp\Http\Server\SocketHttpServer;
use Amp\Socket;
use Amp\Websocket\Server\Websocket;
use Amp\Websocket\Server\WebsocketClientHandler;
use Amp\Websocket\Server\WebsocketGateway;
use Amp\Websocket\Server\WebsocketAcceptHandler;
use Amp\Websocket\WebsocketClient;
use Psr\Log\NullLogger;

require __DIR__ . '/vendor/autoload.php';

// WebSocket handler
$handler = new class implements WebsocketClientHandler {
    public function handleClient(
        WebsocketClient $client,
        Request $request,
        Response $response,
    ): void {
        $addr = $request->getClient()->getRemoteAddress()->toString();
        echo "Client connected: {$addr}\n";

        foreach ($client as $message) {
            $payload = $message->buffer();
            echo "[{$addr}] {$payload}\n";

            // Echo back with timestamp
            $client->sendText(sprintf(
                '[%s] Echo: %s',
                date('H:i:s'),
                $payload,
            ));
        }

        echo "Client disconnected: {$addr}\n";
    }
};

$server = SocketHttpServer::createForDirectAccess(new NullLogger());

$server->expose(new Socket\InternetAddress('127.0.0.1', 9001));

$websocket = new Websocket($server, new NullLogger(), new WebsocketAcceptHandler(), $handler);

$router = new Router($server, new NullLogger(), new DefaultErrorHandler());
$router->addRoute('GET', '/ws', $websocket);

$server->start($router, new DefaultErrorHandler());

echo "WebSocket server running at ws://127.0.0.1:9001/ws\n";

// Keep running until signal
Amp\trapSignal([SIGINT, SIGTERM]);

$server->stop();

Итоговая таблица API

Revolt AMPHP v3 ReactPHP Назначение
EventLoop::defer() Amp\async() Loop::futureTick() Немедленное выполнение
EventLoop::delay() Amp\delay() Loop::addTimer() Одноразовый таймер
EventLoop::repeat() Loop::addPeriodicTimer() Повторяющийся таймер
EventLoop::onReadable() (через socket) Loop::addReadStream() I/O read watcher
EventLoop::onWritable() (через socket) Loop::addWriteStream() I/O write watcher
EventLoop::onSignal() Amp\trapSignal() Loop::addSignal() POSIX-сигнал
EventLoop::cancel() Loop::cancelTimer() Отмена
Suspension::suspend() Future::await() ->then() Ожидание результата

Проверь себя

5 из 9
🧪

Когда async PHP НЕ даёт выигрыша?

🧪

Какой минимальный PHP требует AMPHP v3?

🧪

Как Revolt EventLoop::onReadable() помогает в async I/O?

🧪

Главное стилистическое отличие AMPHP v3 от ReactPHP?

🧪

Чем async PHP ускоряет выполнение?