😺

【PHP】ReactPHP の EventLoop で HTTP/2 フレームを送信する

2024/07/18に公開

ブロッキングなストリームで HTTP/2 フレームを送信して正常にレスポンスを取得することができた。

> php client.php
00000604000000000000030000006400000004010000000000005c010400000001887690aa69d29ae452a9a74a6b13015db12e0f5889a47e561cc58197000f6197df3dbf4a05e532db4282009a5022b8cbb71b6d4c5a37ff0f0d01366c96dc34fd280714cb6d0a08026940bf702fdc6db53168df5f87497ca589d34d1f00000600010000000148656c6c6f0a
client.php
<?php

use React\EventLoop\Loop;

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

// https://github.com/reactphp/event-loop/blob/f2fb5a22568c4d29737d69208d1394ac47d77a38/examples/13-http-client-blocking.php

// https://gist.github.com/masakielastic/8840124754b38663373f8f74d6f162d4
require 'h2frames.php';

// TLS なしの h2c サーバー
// https://zenn.dev/masakielastic/articles/618f9f44f327f4

$stream = stream_socket_client('tcp://0.0.0.0:8000');
if (!$stream) {
    exit(1);
}
stream_set_blocking($stream, false);

fwrite($stream, h2frames('pri'));
fwrite($stream, h2frames('settings'));
fwrite($stream, h2frames('ack'));
fwrite($stream, h2frames('headers'));

Loop::addReadStream($stream, function ($stream) {
    $chunk = fread($stream, 64 * 1024);

    echo bin2hex($chunk), PHP_EOL;

    Loop::removeReadStream($stream);
    fclose($stream);

});

追記。ノンブロッキングなコードでも正常にレスポンスを取得できたので記載する。Loop::addReadStream のなかでのループの処理は手抜きをしている。本来はレスポンスを DATA (0x00) フレームまで解析しなければならないが、著者の環境では ACK (0x04) を取得した時点ですべてのレスポンスが読み込まれるので、それを確認して終了している。

php client.php
Connecting[connected]
type
04
flag
00
stream
000006040000000000000300000064

type
04
flag
01
stream
00000004010000000000005b010400000001887690aa69d29ae452a9a74a6b13015db12e0f5889a47e561cc58197000f6196df3dbf4a05e532db4282009a502cdc102e09f53168df0f0d01366c96dc34fd280714cb6d0a08026940bf702fdc6db53168df5f87497ca589d34d1f00000600010000000148656c6c6f0a

[END]
client.php
<?php

// https://github.com/reactphp/event-loop/blob/v1.5.0/examples/14-http-client-async.php

use React\EventLoop\Factory;
use React\EventLoop\Loop;

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

$ip = gethostbyname('localhost');
if (ip2long($ip) === false) {
    echo 'Unable to resolve hostname' . PHP_EOL;
    exit(1);
}

$stream = stream_socket_client('tcp://' . $ip . ':8000', $errno, $errstr, null, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_C>
if (!$stream) {
    exit(1);
}
stream_set_blocking($stream, false);

echo 'Connecting';
$timer = Loop::addPeriodicTimer(0.01, function () {
    echo '.', PHP_EOL;
});

Loop::addWriteStream($stream, function ($stream) use ($timer) {
    Loop::removeWriteStream($stream);
    Loop::cancelTimer($timer);

    if (stream_socket_get_name($stream, true) === false) {
        echo '[unable to connect]' . PHP_EOL;
        exit(1);
    } else {
        echo '[connected]' . PHP_EOL;
    }

    fwrite($stream, h2frames('pri'));
    fwrite($stream, h2frames('settings'));
    fwrite($stream, h2frames('ack'));
    fwrite($stream, h2frames('headers'));

    Loop::addReadStream($stream, function ($stream) {
        $chunk = fread($stream, 64 * 1024);
        $type = substr($chunk, 3, 1);
        $flag = substr($chunk, 4, 1);

        if ($chunk === '') {
            echo '[END]' . PHP_EOL;
            Loop::removeReadStream($stream);
            fclose($stream);
            return;
        }

        echo "type", PHP_EOL;
        echo bin2hex($type), PHP_EOL;
        echo "flag", PHP_EOL;
        echo bin2hex($flag), PHP_EOL;
        echo "stream", PHP_EOL;
        echo bin2hex($chunk), PHP_EOL;
        echo PHP_EOL;

        if ($type === "\x04" && $flag === "\x01" && strlen($chunk) > 9) {
            echo '[END]' . PHP_EOL;
            Loop::removeReadStream($stream);
            fclose($stream);
            return;
        }

    });
});

Discussion