Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Created March 3, 2026 21:59
Show Gist options
  • Select an option

  • Save masakielastic/0eb47e4572ca709e6cbf57ebc20da6cd to your computer and use it in GitHub Desktop.

Select an option

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

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

通信結果

=== stream_get_meta_data() ===
crypto: array (
  'protocol' => 'TLSv1.3',
  'cipher_name' => 'TLS_AES_256_GCM_SHA384',
  'cipher_bits' => 256,
  'cipher_version' => 'TLSv1.3',
  'alpn_protocol' => 'h2',
)
stream_type: 'tcp_socket/ssl'
timed_out: false
blocked: true
eof: false
unread_bytes: 0
meta keys: crypto, timed_out, blocked, eof, stream_type, mode, unread_bytes, seekable, uri
ALPN negotiated: (meta['alpn_protocol'] not present on this build)
>>> sending 70 bytes
0000  50 52 49 20 2a 20 48 54 54 50 2f 32 2e 30 0d 0a  PRI * HTTP/2.0..
0010  0d 0a 53 4d 0d 0a 0d 0a 00 00 0c 04 00 00 00 00  ..SM............
0020  00 00 02 00 00 00 00 00 03 00 00 00 64 00 00 10  ............d...
0030  01 05 00 00 00 01 82 84 87 01 0b 65 78 61 6d 70  ...........examp
0040  6c 65 2e 63 6f 6d                                le.com
<<< reading frames...
FRAME len=18 type=0x04 flags=0x00 sid=0
0000  00 03 00 00 00 64 00 04 00 01 00 00 00 05 00 ff  .....d..........
0010  ff ff                                            ..
>>> sending SETTINGS ACK
FRAME len=4 type=0x08 flags=0x00 sid=0
0000  7f ff 00 00                                      ....
FRAME len=0 type=0x04 flags=0x01 sid=0
FRAME len=127 type=0x01 flags=0x04 sid=1
0000  88 61 96 df 69 7e 94 03 2a 68 1d 8a 08 02 71 41  .a..i~..*h....qA
0010  06 e3 6d dc 13 2a 62 d1 bf 5f 87 49 7c a5 89 d3  ..m..*b.._.I|...
0020  4d 1f 40 85 24 ab 58 3f 5f 8f 7e 47 23 28 9f 78  M.@.$.X?_.~G#(.x
0030  84 78 0d 88 12 2d a7 6e ff 6c 96 e4 59 3e 94 13  .x...-.n.l..Y>..
0040  6a 61 2c 6a 08 02 71 40 3b 70 40 b8 db 4a 62 d1  ja,j..q@;p@..Jb.
0050  bf 56 88 c5 83 7f d2 98 f0 43 7f 52 84 8f d2 4a  .V.......C.R...J
0060  8f 55 83 7d f1 37 40 8a 24 ab 10 64 9c ab 21 23  .U.}.7@.$..d..!#
0070  4d a8 03 48 49 54 76 87 25 07 b6 49 68 1d 85     M..HITv.%..Ih..
FRAME len=528 type=0x00 flags=0x00 sid=1
0000  3c 21 64 6f 63 74 79 70 65 20 68 74 6d 6c 3e 3c  <!doctype html><
0010  68 74 6d 6c 20 6c 61 6e 67 3d 22 65 6e 22 3e 3c  html lang="en"><
0020  68 65 61 64 3e 3c 74 69 74 6c 65 3e 45 78 61 6d  head><title>Exam
0030  70 6c 65 20 44 6f 6d 61 69 6e 3c 2f 74 69 74 6c  ple Domain</titl
0040  65 3e 3c 6d 65 74 61 20 6e 61 6d 65 3d 22 76 69  e><meta name="vi
0050  65 77 70 6f 72 74 22 20 63 6f 6e 74 65 6e 74 3d  ewport" content=
0060  22 77 69 64 74 68 3d 64 65 76 69 63 65 2d 77 69  "width=device-wi
0070  64 74 68 2c 20 69 6e 69 74 69 61 6c 2d 73 63 61  dth, initial-sca
0080  6c 65 3d 31 22 3e 3c 73 74 79 6c 65 3e 62 6f 64  le=1"><style>bod
0090  79 7b 62 61 63 6b 67 72 6f 75 6e 64 3a 23 65 65  y{background:#ee
00a0  65 3b 77 69 64 74 68 3a 36 30 76 77 3b 6d 61 72  e;width:60vw;mar
00b0  67 69 6e 3a 31 35 76 68 20 61 75 74 6f 3b 66 6f  gin:15vh auto;fo
00c0  6e 74 2d 66 61 6d 69 6c 79 3a 73 79 73 74 65 6d  nt-family:system
00d0  2d 75 69 2c 73 61 6e 73 2d 73 65 72 69 66 7d 68  -ui,sans-serif}h
00e0  31 7b 66 6f 6e 74 2d 73 69 7a 65 3a 31 2e 35 65  1{font-size:1.5e
00f0  6d 7d 64 69 76 7b 6f 70 61 63 69 74 79 3a 30 2e  m}div{opacity:0.
0100  38 7d 61 3a 6c 69 6e 6b 2c 61 3a 76 69 73 69 74  8}a:link,a:visit
0110  65 64 7b 63 6f 6c 6f 72 3a 23 33 34 38 7d 3c 2f  ed{color:#348}</
0120  73 74 79 6c 65 3e 3c 2f 68 65 61 64 3e 3c 62 6f  style></head><bo
0130  64 79 3e 3c 64 69 76 3e 3c 68 31 3e 45 78 61 6d  dy><div><h1>Exam
0140  70 6c 65 20 44 6f 6d 61 69 6e 3c 2f 68 31 3e 3c  ple Domain</h1><
0150  70 3e 54 68 69 73 20 64 6f 6d 61 69 6e 20 69 73  p>This domain is
0160  20 66 6f 72 20 75 73 65 20 69 6e 20 64 6f 63 75   for use in docu
0170  6d 65 6e 74 61 74 69 6f 6e 20 65 78 61 6d 70 6c  mentation exampl
0180  65 73 20 77 69 74 68 6f 75 74 20 6e 65 65 64 69  es without needi
0190  6e 67 20 70 65 72 6d 69 73 73 69 6f 6e 2e 20 41  ng permission. A
01a0  76 6f 69 64 20 75 73 65 20 69 6e 20 6f 70 65 72  void use in oper
01b0  61 74 69 6f 6e 73 2e 3c 2f 70 3e 3c 70 3e 3c 61  ations.</p><p><a
01c0  20 68 72 65 66 3d 22 68 74 74 70 73 3a 2f 2f 69   href="https://i
01d0  61 6e 61 2e 6f 72 67 2f 64 6f 6d 61 69 6e 73 2f  ana.org/domains/
01e0  65 78 61 6d 70 6c 65 22 3e 4c 65 61 72 6e 20 6d  example">Learn m
01f0  6f 72 65 3c 2f 61 3e 3c 2f 70 3e 3c 2f 64 69 76  ore</a></p></div
0200  3e 3c 2f 62 6f 64 79 3e 3c 2f 68 74 6d 6c 3e 0a  ></body></html>.
FRAME len=0 type=0x00 flags=0x01 sid=1
<?php
declare(strict_types=1);
/**
* Minimal HTTP/2 over TLS client using PHP streams + ALPN (h2).
* Sends: client preface, SETTINGS, HEADERS (HPACK minimal), then reads frames.
*
* Usage:
* php h2_stream_client.php example.com 443 /
*/
function hexdump(string $bin, int $max = 512): string {
$bin = substr($bin, 0, $max);
$out = '';
$len = strlen($bin);
for ($i = 0; $i < $len; $i += 16) {
$chunk = substr($bin, $i, 16);
$hex = implode(' ', str_split(bin2hex($chunk), 2));
$asc = preg_replace('/[^\x20-\x7E]/', '.', $chunk);
$out .= sprintf("%04x %-47s %s\n", $i, $hex, $asc);
}
return $out;
}
function packFrame(int $type, int $flags, int $streamId, string $payload): string {
$len = strlen($payload);
if ($len > 0xFFFFFF) throw new RuntimeException("Frame too large: $len");
// 24-bit length + type + flags + 31-bit stream id (reserved bit must be 0)
$hdr = chr(($len >> 16) & 0xFF) . chr(($len >> 8) & 0xFF) . chr($len & 0xFF);
$hdr .= chr($type & 0xFF);
$hdr .= chr($flags & 0xFF);
$hdr .= pack('N', $streamId & 0x7FFFFFFF);
return $hdr . $payload;
}
function readN($fp, int $n): string {
$buf = '';
while (strlen($buf) < $n) {
$r = fread($fp, $n - strlen($buf));
if ($r === false || $r === '') {
if (feof($fp)) break;
usleep(10_000);
continue;
}
$buf .= $r;
}
return $buf;
}
function readFrame($fp): ?array {
$h = readN($fp, 9);
if (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 (strlen($payload) < $len) return null;
return [$len, $type, $flags, $sid, $payload];
}
/**
* HPACK minimal header block:
* :method: GET -> static indexed #2 => 0x82
* :path: <path> -> if "/" then static indexed #4 => 0x84 else literal
* :scheme: https -> static indexed #7 => 0x87
* :authority: <host> -> literal without indexing, indexed name #1
*
* NOTE: For simplicity, path is literal if not "/".
*/
function hpackStringLiteral(string $s): string {
// H=0 (no Huffman), length in 7-bit prefix (assume <= 127 for demo)
$len = strlen($s);
if ($len > 127) throw new RuntimeException("String too long for demo: $len");
return chr($len) . $s;
}
function buildHpackHeaders(string $host, string $path): string {
$block = '';
$block .= "\x82"; // :method GET
if ($path === '/') {
$block .= "\x84"; // :path /
} else {
// Literal Header Field without Indexing — Indexed Name
// :path is static table #4 => index=4 with 4-bit prefix => 0x04
$block .= "\x04" . hpackStringLiteral($path);
}
$block .= "\x87"; // :scheme https (static #7)
// :authority (indexed name = 1), literal value without indexing
$block .= "\x01" . hpackStringLiteral($host);
// 追加で User-Agent を入れたい場合(static table #58 is "user-agent", but value differs)
// -> literal without indexing, indexed name (#58) requires base-128 integer encoding; demoでは省略
return $block;
}
// ---- main ----
$host = $argv[1] ?? 'example.com';
$port = (int)($argv[2] ?? 443);
$path = $argv[3] ?? '/';
$ssl = [
// ALPN で h2 を要求(これがないと HTTP/2 にならない)
'alpn_protocols' => 'h2',
// SNI
'SNI_enabled' => true,
'peer_name' => $host,
// 検証(実験でまず動かす目的なら verify_peer=false も可能だが推奨しない)
'verify_peer' => true,
'verify_peer_name' => true,
// Debian 系の典型。環境に応じて調整
'cafile' => '/etc/ssl/certs/ca-certificates.crt',
// 失敗時の情報を取りたい場合
'capture_peer_cert' => true,
];
$ctx = stream_context_create(['ssl' => $ssl]);
$fp = stream_socket_client(
"tls://{$host}:{$port}",
$errno,
$errstr,
8.0,
STREAM_CLIENT_CONNECT,
$ctx
);
if (!$fp) {
fwrite(STDERR, "connect failed: {$errno} {$errstr}\n");
exit(1);
}
stream_set_timeout($fp, 5);
stream_set_blocking($fp, true);
// ALPN / crypto の状態を目視
$meta = stream_get_meta_data($fp);
fwrite(STDOUT, "=== stream_get_meta_data() ===\n");
foreach (['crypto', 'stream_type', 'timed_out', 'blocked', 'eof', 'unread_bytes'] as $k) {
if (array_key_exists($k, $meta)) {
fwrite(STDOUT, sprintf("%s: %s\n", $k, var_export($meta[$k], true)));
}
}
// PHP の実装・バージョンでキー名が微妙に違うことがあるので dump
fwrite(STDOUT, "meta keys: " . implode(', ', array_keys($meta)) . "\n");
// ここが重要:ALPN の結果が取れる環境なら "alpn_protocol" が見える
if (isset($meta['alpn_protocol'])) {
fwrite(STDOUT, "ALPN negotiated: " . $meta['alpn_protocol'] . "\n");
} else {
fwrite(STDOUT, "ALPN negotiated: (meta['alpn_protocol'] not present on this build)\n");
}
// 交渉結果チェック(h2 じゃないなら HTTP/2 フレーム送っても意味がない)
if (isset($meta['alpn_protocol']) && $meta['alpn_protocol'] !== 'h2') {
fwrite(STDERR, "ERROR: negotiated ALPN is not h2\n");
fclose($fp);
exit(1);
}
// 1) client connection preface
$preface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
// 2) SETTINGS (例: ENABLE_PUSH=0, MAX_CONCURRENT_STREAMS=100)
$settingsPayload =
"\x00\x02" . "\x00\x00\x00\x00" . // SETTINGS_ENABLE_PUSH = 0
"\x00\x03" . "\x00\x00\x00\x64"; // SETTINGS_MAX_CONCURRENT_STREAMS = 100
$settingsFrame = packFrame(0x04 /*SETTINGS*/, 0x00, 0, $settingsPayload);
// 3) HEADERS on stream 1: END_STREAM|END_HEADERS
$hpack = buildHpackHeaders($host, $path);
$headersFrame = packFrame(0x01 /*HEADERS*/, 0x05 /*END_STREAM|END_HEADERS*/, 1, $hpack);
// send bytes
$toSend = $preface . $settingsFrame . $headersFrame;
fwrite(STDOUT, ">>> sending " . strlen($toSend) . " bytes\n");
fwrite(STDOUT, hexdump($toSend, 2048));
$w = fwrite($fp, $toSend);
if ($w === false) {
fwrite(STDERR, "write failed\n");
fclose($fp);
exit(1);
}
fwrite(STDOUT, "<<< reading frames...\n");
// read frames (demo)
for ($i = 0; $i < 40; $i++) {
$fr = readFrame($fp);
if ($fr === null) {
$m = stream_get_meta_data($fp);
fwrite(STDOUT, "no more frames / timeout=" . ($m['timed_out'] ? 'yes' : 'no') . "\n");
break;
}
[$len, $type, $flags, $sid, $payload] = $fr;
fwrite(STDOUT, sprintf("FRAME len=%d type=0x%02x flags=0x%02x sid=%d\n", $len, $type, $flags, $sid));
if ($len > 0) {
fwrite(STDOUT, hexdump($payload, 1024));
}
// SETTINGS を受けたら ACK
if ($type === 0x04 /*SETTINGS*/ && $len > 0 && ($flags & 0x01) === 0) {
$ack = packFrame(0x04, 0x01, 0, '');
fwrite(STDOUT, ">>> sending SETTINGS ACK\n");
fwrite($fp, $ack);
}
// GOAWAY (0x07) が来たら終わり
if ($type === 0x07) {
fwrite(STDOUT, "GOAWAY received\n");
break;
}
}
fclose($fp);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment