Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Created March 3, 2026 22:27
Show Gist options
  • Select an option

  • Save masakielastic/275547119591aa6c92e7d854b21b710a to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/275547119591aa6c92e7d854b21b710a to your computer and use it in GitHub Desktop.
PHP のストリーム関数で簡易 HTTP/2 クライアント (SETTINGS まで)

PHP のストリーム関数で簡易 HTTP/2 クライアント (SETTINGS まで)

実行結果

target: tls://142.250.192.164:443 (SNI/peer_name=www.google.com)
alpn_protocols(configured): h2,http/1.1
ALPN (negotiated): h2
>>> sending client preface (24 bytes)
>>> sending SETTINGS (empty payload)
FRAME #0 len=18 type=0x04(SETTINGS) flags=0x00 sid=0
0000  00 03 00 00 00 64 00 04 00 10 00 00 00 06 00 01  .....d..........
0010  00 00                                            ..

>>> sending SETTINGS ACK
SETTINGS exchange done; exiting demo.
done
<?php
declare(strict_types=1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
function logln(string $s): void {
fwrite(STDOUT, $s . PHP_EOL);
fflush(STDOUT);
}
function hexdump(string $bin, int $max = 256): string {
$len = strlen($bin);
$n = min($len, $max);
$out = '';
for ($i = 0; $i < $n; $i += 16) {
$chunk = substr($bin, $i, 16);
$hex = implode(' ', str_split(bin2hex($chunk), 2));
$ascii = preg_replace('/[^\x20-\x7e]/', '.', $chunk);
$out .= sprintf("%04x %-47s %s\n", $i, $hex, $ascii);
}
if ($len > $max) {
$out .= sprintf("... (%d bytes total)\n", $len);
}
return $out;
}
/**
* read exactly $n bytes; returns null on timeout/EOF/error
*/
function readN($fp, int $n): ?string {
$buf = '';
while (strlen($buf) < $n) {
$r = fread($fp, $n - strlen($buf));
if ($r === false) {
return null;
}
if ($r === '') {
$m = stream_get_meta_data($fp);
if (!empty($m['timed_out']) || feof($fp)) {
return null;
}
usleep(10_000);
continue;
}
$buf .= $r;
}
return $buf;
}
/**
* HTTP/2 frame:
* length(24) type(8) flags(8) r(1)+stream_id(31) payload(length)
*/
function readFrame($fp): ?array {
$h = readN($fp, 9);
if ($h === null || strlen($h) < 9) return null;
$len = (ord($h[0]) << 16) | (ord($h[1]) << 8) | ord($h[2]);
$type = ord($h[3]);
$flags = ord($h[4]);
$sid = (unpack('N', substr($h, 5, 4))[1]) & 0x7FFFFFFF;
$payload = $len ? readN($fp, $len) : '';
if ($len && ($payload === null || strlen($payload) < $len)) return null;
return [$len, $type, $flags, $sid, $payload];
}
function packFrame(int $type, int $flags, int $sid, string $payload): string {
$len = strlen($payload);
$h = chr(($len >> 16) & 0xff) . chr(($len >> 8) & 0xff) . chr($len & 0xff);
$h .= chr($type & 0xff) . chr($flags & 0xff);
$h .= pack('N', $sid & 0x7FFFFFFF);
return $h . $payload;
}
function nameFrameType(int $t): string {
return match ($t) {
0x00 => 'DATA',
0x01 => 'HEADERS',
0x02 => 'PRIORITY',
0x03 => 'RST_STREAM',
0x04 => 'SETTINGS',
0x05 => 'PUSH_PROMISE',
0x06 => 'PING',
0x07 => 'GOAWAY',
0x08 => 'WINDOW_UPDATE',
0x09 => 'CONTINUATION',
default => 'UNKNOWN',
};
}
function connectTlsWithAlpn(string $host, int $port, string $alpn, int $timeoutSec = 3) {
// IPv6 でハングしがちな環境向け:IPv4を優先(取れないならホスト名のまま)
$ip4 = gethostbyname($host);
$targetHost = ($ip4 !== $host) ? $ip4 : $host;
$target = "tls://{$targetHost}:{$port}";
$ctx = stream_context_create([
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
'SNI_enabled' => true,
'peer_name' => $host, // SNI はホスト名で
'alpn_protocols' => $alpn,
],
]);
logln("target: {$target} (SNI/peer_name={$host})");
logln("alpn_protocols(configured): {$alpn}");
$errno = 0; $errstr = '';
$fp = @stream_socket_client($target, $errno, $errstr, $timeoutSec, STREAM_CLIENT_CONNECT, $ctx);
if (!$fp) {
throw new RuntimeException("connect failed: ($errno) $errstr");
}
// 重要:タイムアウト・バッファを明示
stream_set_timeout($fp, $timeoutSec);
stream_set_read_buffer($fp, 0);
stream_set_write_buffer($fp, 0);
$meta = stream_get_meta_data($fp);
$neg = null;
if (isset($meta['crypto']) && is_array($meta['crypto'])) {
foreach (['alpn_protocol','alpn_selected','ssl_alpn_protocol','negotiated_protocol','protocol'] as $k) {
if (isset($meta['crypto'][$k]) && is_string($meta['crypto'][$k]) && $meta['crypto'][$k] !== '') {
$neg = $meta['crypto'][$k];
break;
}
}
}
logln("ALPN (negotiated): " . ($neg ?? "(not found; check meta dump)"));
return [$fp, $meta, $neg];
}
/* ========= main ========= */
$host = $argv[1] ?? 'www.google.com';
$port = (int)($argv[2] ?? 443);
// h2 が選ばれるかを見る。順序は h2 優先。
$alpn = "h2,http/1.1";
// ここは用途に応じて。デモなので短め。
$timeoutSec = 2;
try {
[$fp, $meta, $neg] = connectTlsWithAlpn($host, $port, $alpn, $timeoutSec);
} catch (Throwable $e) {
logln("[!] " . $e->getMessage());
exit(1);
}
if ($neg !== 'h2') {
logln("[!] negotiated ALPN is not h2. Exiting (this demo expects HTTP/2).");
// 必要なら meta を見たいときだけ出す
// var_export($meta); echo PHP_EOL;
fclose($fp);
exit(2);
}
// 1) client connection preface(必須)
$preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
logln(">>> sending client preface (" . strlen($preface) . " bytes)");
fwrite($fp, $preface);
// 2) クライアント SETTINGS(最小:空payloadでもOK)
$settings = packFrame(0x04, 0x00, 0, '');
logln(">>> sending SETTINGS (empty payload)");
fwrite($fp, $settings);
// 3) サーバからの SETTINGS 等を読む
$gotServerSettings = false;
$ackedServerSettings = false;
$maxFrames = 50;
for ($i = 0; $i < $maxFrames; $i++) {
$fr = readFrame($fp);
if ($fr === null) {
$m = stream_get_meta_data($fp);
logln("no more frames / timeout=" . (!empty($m['timed_out']) ? 'yes' : 'no') . " eof=" . (feof($fp) ? 'yes' : 'no'));
break;
}
[$len, $type, $flags, $sid, $payload] = $fr;
$tname = nameFrameType($type);
logln(sprintf("FRAME #%d len=%d type=0x%02x(%s) flags=0x%02x sid=%d", $i, $len, $type, $tname, $flags, $sid));
if ($len > 0) {
// SETTINGSのpayloadは6バイト単位のID/Valueだが、デモではダンプだけ
logln(hexdump($payload, 256));
}
// サーバ SETTINGS(ACKではない)を受けたら ACK を返す
if ($type === 0x04 /*SETTINGS*/ && ($flags & 0x01) === 0) {
$gotServerSettings = true;
if (!$ackedServerSettings) {
$ack = packFrame(0x04, 0x01, 0, '');
logln(">>> sending SETTINGS ACK");
fwrite($fp, $ack);
$ackedServerSettings = true;
// 疎通確認としてはここで終わるのが現実的
logln("SETTINGS exchange done; exiting demo.");
break;
}
}
// 念のため:GOAWAY が来たら終わり
if ($type === 0x07 /*GOAWAY*/) {
logln("GOAWAY received; exiting.");
break;
}
}
fclose($fp);
logln("done");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment