はじめに
PHPでネットワーク通信やソケット処理を行う際、相手サーバーからの応答がいつまでも来ないと、スクリプトが永遠に待ち続けてしまいます。本番環境でこれが起きると、プロセスが詰まりサービス全体に影響を及ぼします。
stream_set_timeout は、ストリームの読み書き操作に対して タイムアウト時間を設定する関数です。指定時間内に応答がなければ処理を打ち切り、後続のエラーハンドリングに移ることができます。
この記事では、基本的な使い方から実践的なクラス実装まで、丁寧に解説します。
stream_set_timeout とは
| 項目 | 内容 |
|---|---|
| 関数名 | stream_set_timeout |
| PHPバージョン | PHP 4.3.0以降 |
| カテゴリ | ストリーム関数 |
| 返り値 | bool(成功時 true、失敗時 false) |
構文
stream_set_timeout(resource $stream, int $seconds, int $microseconds = 0): bool
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$stream | resource | 対象のストリームリソース |
$seconds | int | タイムアウトの秒数部分 |
$microseconds | int | タイムアウトのマイクロ秒部分(省略可、デフォルト 0) |
秒とマイクロ秒を組み合わせる例:
- 2秒500ミリ秒 →
stream_set_timeout($s, 2, 500000)- 500ミリ秒のみ →
stream_set_timeout($s, 0, 500000)- 5秒ちょうど →
stream_set_timeout($s, 5)
返り値
- 成功時:
true - 失敗時:
false
タイムアウトの検知方法
重要: タイムアウトが発生しても、fread や fgets は即座に例外を投げません。処理が終わった後、stream_get_meta_data で timed_out フラグを確認する必要があります。
<?php
$stream = fsockopen('example.com', 80, $errno, $errstr, 5);
stream_set_timeout($stream, 3); // 3秒でタイムアウト
$response = fgets($stream);
// タイムアウトの確認
$meta = stream_get_meta_data($stream);
if ($meta['timed_out']) {
echo "タイムアウトが発生しました";
}
stream_get_meta_data が返す主なキー:
| キー | 型 | 説明 |
|---|---|---|
timed_out | bool | タイムアウトが発生した場合 true |
blocked | bool | ブロッキングモードの場合 true |
eof | bool | ストリームがEOFに達した場合 true |
stream_type | string | ストリームの種類 |
基本的な使い方
<?php
// TCP接続して3秒タイムアウトを設定
$stream = fsockopen('example.com', 80, $errno, $errstr, 5);
if (!$stream) {
die("接続失敗: [{$errno}] {$errstr}");
}
// 読み書きのタイムアウトを3秒に設定
stream_set_timeout($stream, 3);
// HTTPリクエスト送信
fwrite($stream, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
// レスポンス受信
$response = '';
while (!feof($stream)) {
$response .= fgets($stream, 4096);
$meta = stream_get_meta_data($stream);
if ($meta['timed_out']) {
echo "タイムアウト発生" . PHP_EOL;
break;
}
}
fclose($stream);
echo "受信バイト数: " . strlen($response) . PHP_EOL;
実践例(クラスを使った実装)
例1:タイムアウト付きTCPクライアント
接続・送信・受信のすべての段階でタイムアウトを管理する基本クラスです。
<?php
class TimeoutTcpClient
{
private $socket = null;
private int $connectTimeout;
private int $readTimeout;
private int $readTimeoutUsec;
public function __construct(
int $connectTimeoutSec = 5,
int $readTimeoutSec = 3,
int $readTimeoutUsec = 0
) {
$this->connectTimeout = $connectTimeoutSec;
$this->readTimeout = $readTimeoutSec;
$this->readTimeoutUsec = $readTimeoutUsec;
}
public function connect(string $host, int $port): void
{
$this->socket = fsockopen(
$host, $port,
$errno, $errstr,
$this->connectTimeout // 接続タイムアウト
);
if (!$this->socket) {
throw new RuntimeException("接続失敗 [{$errno}]: {$errstr}");
}
// 読み書きのタイムアウトを設定
$result = stream_set_timeout(
$this->socket,
$this->readTimeout,
$this->readTimeoutUsec
);
if (!$result) {
throw new RuntimeException("タイムアウト設定に失敗しました");
}
}
public function send(string $data): int
{
$this->assertConnected();
$written = fwrite($this->socket, $data);
if ($written === false) {
throw new RuntimeException("送信失敗");
}
return $written;
}
public function receive(int $bufferSize = 4096): string
{
$this->assertConnected();
$response = '';
while (!feof($this->socket)) {
$chunk = fread($this->socket, $bufferSize);
if ($chunk === false) {
break;
}
$response .= $chunk;
$meta = stream_get_meta_data($this->socket);
if ($meta['timed_out']) {
throw new RuntimeException(
"受信タイムアウト({$this->readTimeout}秒 + {$this->readTimeoutUsec}μs)"
);
}
}
return $response;
}
public function close(): void
{
if (is_resource($this->socket)) {
fclose($this->socket);
$this->socket = null;
}
}
private function assertConnected(): void
{
if (!is_resource($this->socket)) {
throw new RuntimeException("接続されていません。connect() を先に呼び出してください");
}
}
}
// 使用例
$client = new TimeoutTcpClient(connectTimeoutSec: 5, readTimeoutSec: 3);
try {
$client->connect('example.com', 80);
$client->send("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
$response = $client->receive();
echo "受信完了: " . strlen($response) . " バイト" . PHP_EOL;
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . PHP_EOL;
} finally {
$client->close();
}
出力例:
受信完了: 1256 バイト
例2:タイムアウトを動的に変更しながらフェーズ別に通信する
接続・認証・データ受信など、フェーズごとに異なるタイムアウトを設定するパターンです。
<?php
class PhaseAwareStreamHandler
{
private $stream;
private array $phaseLog = [];
public function __construct(resource $stream)
{
$this->stream = $stream;
}
/**
* フェーズ名と秒数を指定してタイムアウトを変更する
*/
public function enterPhase(string $phaseName, int $seconds, int $microseconds = 0): void
{
$result = stream_set_timeout($this->stream, $seconds, $microseconds);
$timeoutStr = $seconds . '秒';
if ($microseconds > 0) {
$timeoutStr .= ' + ' . number_format($microseconds) . 'μs';
}
$this->phaseLog[] = [
'phase' => $phaseName,
'timeout' => $timeoutStr,
'success' => $result,
'time' => date('H:i:s'),
];
if (!$result) {
throw new RuntimeException("フェーズ [{$phaseName}] のタイムアウト設定に失敗");
}
}
public function readLine(): string
{
$line = fgets($this->stream);
$meta = stream_get_meta_data($this->stream);
if ($meta['timed_out']) {
$current = end($this->phaseLog);
throw new RuntimeException(
"フェーズ [{$current['phase']}] でタイムアウト({$current['timeout']})"
);
}
return $line !== false ? rtrim($line) : '';
}
public function write(string $data): void
{
if (fwrite($this->stream, $data) === false) {
throw new RuntimeException("書き込みに失敗しました");
}
}
public function printLog(): void
{
echo "=== フェーズログ ===" . PHP_EOL;
foreach ($this->phaseLog as $entry) {
$status = $entry['success'] ? '✓' : '✗';
echo " [{$entry['time']}] {$status} {$entry['phase']}: タイムアウト {$entry['timeout']}" . PHP_EOL;
}
}
public function getStream(): resource
{
return $this->stream;
}
}
// 使用例(SMTPライクなプロトコルシミュレーション)
try {
$socket = fsockopen('smtp.example.com', 25, $errno, $errstr, 5);
if (!$socket) {
throw new RuntimeException("接続失敗: [{$errno}] {$errstr}");
}
$handler = new PhaseAwareStreamHandler($socket);
// 接続確認:短めのタイムアウト
$handler->enterPhase('CONNECT', 2);
$banner = $handler->readLine();
// EHLO:中程度のタイムアウト
$handler->enterPhase('EHLO', 5);
$handler->write("EHLO localhost\r\n");
$ehloResp = $handler->readLine();
// DATA転送:長めのタイムアウト
$handler->enterPhase('DATA', 30);
// ... データ転送処理 ...
$handler->printLog();
fclose($socket);
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . PHP_EOL;
}
出力例(接続できた場合):
=== フェーズログ ===
[12:00:01] ✓ CONNECT: タイムアウト 2秒
[12:00:01] ✓ EHLO: タイムアウト 5秒
[12:00:01] ✓ DATA: タイムアウト 30秒
例3:タイムアウトを検知してリトライするHTTPクライアント
タイムアウト発生時に自動リトライするパターンです。指数バックオフで待機時間を伸ばします。
<?php
class RetryableHttpClient
{
private int $maxRetries;
private int $baseTimeoutSec;
private float $backoffMultiplier;
public function __construct(
int $maxRetries = 3,
int $baseTimeoutSec = 3,
float $backoffMultiplier = 2.0
) {
$this->maxRetries = $maxRetries;
$this->baseTimeoutSec = $baseTimeoutSec;
$this->backoffMultiplier = $backoffMultiplier;
}
public function get(string $host, string $path = '/'): array
{
$attempt = 0;
$lastError = null;
while ($attempt < $this->maxRetries) {
$attempt++;
// リトライごとにタイムアウトを延長
$timeout = (int) ($this->baseTimeoutSec * ($this->backoffMultiplier ** ($attempt - 1)));
try {
$result = $this->doRequest($host, $path, $timeout);
$result['attempts'] = $attempt;
return $result;
} catch (RuntimeException $e) {
$lastError = $e->getMessage();
echo "試行 {$attempt}/{$this->maxRetries} 失敗: {$lastError}(タイムアウト: {$timeout}秒)" . PHP_EOL;
if ($attempt < $this->maxRetries) {
sleep(1); // 1秒待ってリトライ
}
}
}
throw new RuntimeException("最大リトライ回数到達: {$lastError}");
}
private function doRequest(string $host, string $path, int $timeoutSec): array
{
$socket = fsockopen($host, 80, $errno, $errstr, $timeoutSec);
if (!$socket) {
throw new RuntimeException("接続失敗 [{$errno}]: {$errstr}");
}
stream_set_timeout($socket, $timeoutSec);
fwrite($socket, "GET {$path} HTTP/1.0\r\nHost: {$host}\r\n\r\n");
$response = '';
while (!feof($socket)) {
$chunk = fgets($socket, 4096);
$meta = stream_get_meta_data($socket);
if ($meta['timed_out']) {
fclose($socket);
throw new RuntimeException("タイムアウト発生({$timeoutSec}秒)");
}
if ($chunk !== false) {
$response .= $chunk;
}
}
fclose($socket);
return [
'status' => $this->parseStatus($response),
'body_bytes' => strlen($response),
'timeout_used' => $timeoutSec,
];
}
private function parseStatus(string $response): string
{
if (preg_match('/^HTTP\/\S+\s+(\d{3}[^\r\n]*)/', $response, $m)) {
return $m[1];
}
return 'unknown';
}
}
// 使用例
$client = new RetryableHttpClient(maxRetries: 3, baseTimeoutSec: 2);
try {
$result = $client->get('example.com', '/');
echo "成功!試行回数: {$result['attempts']}, ステータス: {$result['status']}, バイト数: {$result['body_bytes']}" . PHP_EOL;
} catch (RuntimeException $e) {
echo "最終エラー: " . $e->getMessage() . PHP_EOL;
}
出力例(1回目で成功した場合):
成功!試行回数: 1, ステータス: 200 OK, バイト数: 1256
出力例(タイムアウトが続いた場合):
試行 1/3 失敗: タイムアウト発生(2秒)(タイムアウト: 2秒)
試行 2/3 失敗: タイムアウト発生(4秒)(タイムアウト: 4秒)
試行 3/3 失敗: タイムアウト発生(8秒)(タイムアウト: 8秒)
最終エラー: 最大リトライ回数到達: タイムアウト発生(8秒)
例4:マイクロ秒単位のタイムアウトで高精度な通信制御
$microseconds パラメータを活用した、ミリ秒・マイクロ秒単位の細かい制御です。
<?php
class PrecisionTimeoutStream
{
private $stream;
private array $timeoutHistory = [];
public function __construct(resource $stream)
{
$this->stream = $stream;
}
/**
* 浮動小数点で秒数を指定する(例:1.5 → 1秒500ミリ秒)
*/
public function setTimeoutFloat(float $seconds): bool
{
$sec = (int) floor($seconds);
$usec = (int) (($seconds - $sec) * 1_000_000);
$result = stream_set_timeout($this->stream, $sec, $usec);
$this->timeoutHistory[] = [
'seconds' => $sec,
'microseconds' => $usec,
'display' => $this->formatTimeout($sec, $usec),
'success' => $result,
];
return $result;
}
/**
* ミリ秒で指定する
*/
public function setTimeoutMs(int $milliseconds): bool
{
$sec = (int) floor($milliseconds / 1000);
$usec = ($milliseconds % 1000) * 1000;
return $this->setTimeoutRaw($sec, $usec);
}
/**
* 秒・マイクロ秒を直接指定する
*/
public function setTimeoutRaw(int $seconds, int $microseconds = 0): bool
{
$result = stream_set_timeout($this->stream, $seconds, $microseconds);
$this->timeoutHistory[] = [
'seconds' => $seconds,
'microseconds' => $microseconds,
'display' => $this->formatTimeout($seconds, $microseconds),
'success' => $result,
];
return $result;
}
public function isTimedOut(): bool
{
$meta = stream_get_meta_data($this->stream);
return $meta['timed_out'];
}
public function getMeta(): array
{
return stream_get_meta_data($this->stream);
}
public function printHistory(): void
{
echo "=== タイムアウト設定履歴 ===" . PHP_EOL;
foreach ($this->timeoutHistory as $i => $entry) {
$status = $entry['success'] ? '✓' : '✗';
echo " [{$i}] {$status} {$entry['display']}" . PHP_EOL;
}
}
private function formatTimeout(int $sec, int $usec): string
{
$parts = [];
if ($sec > 0) {
$parts[] = "{$sec}秒";
}
if ($usec >= 1000) {
$parts[] = ($usec / 1000) . "ミリ秒";
} elseif ($usec > 0) {
$parts[] = "{$usec}マイクロ秒";
}
return implode(' + ', $parts) ?: '0秒';
}
}
// 使用例
$socket = fsockopen('example.com', 80, $errno, $errstr, 5);
if ($socket) {
$pts = new PrecisionTimeoutStream($socket);
$pts->setTimeoutFloat(1.5); // 1.5秒
$pts->setTimeoutMs(750); // 750ミリ秒
$pts->setTimeoutRaw(0, 250000); // 250ミリ秒
$pts->printHistory();
// 最後に設定した250msで通信
fwrite($socket, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
$line = fgets($socket);
echo PHP_EOL . "タイムアウト発生: " . ($pts->isTimedOut() ? 'はい' : 'いいえ') . PHP_EOL;
echo "レスポンス先頭: " . trim($line) . PHP_EOL;
fclose($socket);
}
出力例:
=== タイムアウト設定履歴 ===
[0] ✓ 1秒 + 500ミリ秒
[1] ✓ 750ミリ秒
[2] ✓ 250ミリ秒
タイムアウト発生: いいえ
レスポンス先頭: HTTP/1.0 200 OK
例5:stream_get_meta_data でタイムアウト状態を詳細モニタリングする
ストリームの状態を継続的に記録し、タイムアウト・EOF・ブロッキング状態を一元管理します。
<?php
class StreamHealthMonitor
{
private $stream;
private string $name;
private array $snapshots = [];
public function __construct(resource $stream, string $name = 'stream')
{
$this->stream = $stream;
$this->name = $name;
}
public function setTimeout(int $seconds, int $microseconds = 0): bool
{
return stream_set_timeout($this->stream, $seconds, $microseconds);
}
/**
* 現在のストリーム状態をスナップショットとして記録する
*/
public function snapshot(string $label = ''): array
{
$meta = stream_get_meta_data($this->stream);
$snap = [
'label' => $label,
'time' => date('H:i:s'),
'timed_out' => $meta['timed_out'],
'blocked' => $meta['blocked'],
'eof' => $meta['eof'],
'stream_type' => $meta['stream_type'],
'mode' => $meta['mode'],
];
$this->snapshots[] = $snap;
return $snap;
}
public function readWithMonitoring(int $bufferSize = 4096): string
{
$data = '';
while (!feof($this->stream)) {
$chunk = fread($this->stream, $bufferSize);
$snap = $this->snapshot("fread後");
if ($snap['timed_out']) {
throw new RuntimeException("[{$this->name}] タイムアウト検知({$snap['time']})");
}
if ($chunk !== false && $chunk !== '') {
$data .= $chunk;
}
}
$this->snapshot("EOF到達");
return $data;
}
public function printReport(): void
{
echo "=== ストリーム [{$this->name}] 状態レポート ===" . PHP_EOL;
echo str_pad("ラベル", 16)
. str_pad("時刻", 10)
. str_pad("TimedOut", 12)
. str_pad("Blocked", 10)
. "EOF" . PHP_EOL;
echo str_repeat('-', 54) . PHP_EOL;
foreach ($this->snapshots as $s) {
echo str_pad($s['label'], 16)
. str_pad($s['time'], 10)
. str_pad($s['timed_out'] ? 'true' : 'false', 12)
. str_pad($s['blocked'] ? 'true' : 'false', 10)
. ($s['eof'] ? 'true' : 'false') . PHP_EOL;
}
}
}
// 使用例
$socket = fsockopen('example.com', 80, $errno, $errstr, 5);
if ($socket) {
$monitor = new StreamHealthMonitor($socket, 'example.com:80');
$monitor->setTimeout(5);
fwrite($socket, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
try {
$body = $monitor->readWithMonitoring();
echo "受信: " . strlen($body) . " バイト" . PHP_EOL . PHP_EOL;
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . PHP_EOL . PHP_EOL;
}
$monitor->printReport();
fclose($socket);
}
出力例:
受信: 1256 バイト
=== ストリーム [example.com:80] 状態レポート ===
ラベル 時刻 TimedOut Blocked EOF
------------------------------------------------------
fread後 12:00:01 false true false
EOF到達 12:00:01 false true true
例6:ファイルストリームへの適用と注意点を示すデモ
stream_set_timeout はネットワークストリームに対して効果的ですが、ローカルファイルでの動作の違いも把握しておくことが重要です。
<?php
class StreamTimeoutDemo
{
/**
* 各ストリームタイプでタイムアウト設定を試みる
*/
public static function runAll(): void
{
$cases = [
'ローカルファイル' => fn() => fopen('php://temp', 'r+'),
'メモリストリーム' => fn() => fopen('php://memory', 'r+'),
'stdin(標準入力)' => fn() => fopen('php://stdin', 'r'),
'HTTPストリーム' => fn() => @fopen('https://example.com', 'r'),
];
echo str_pad("ストリーム種別", 24) . str_pad("設定結果", 12) . "timed_out初期値" . PHP_EOL;
echo str_repeat('-', 52) . PHP_EOL;
foreach ($cases as $label => $factory) {
$stream = $factory();
if (!is_resource($stream)) {
echo str_pad($label, 24) . "開けませんでした" . PHP_EOL;
continue;
}
$result = stream_set_timeout($stream, 1);
$meta = stream_get_meta_data($stream);
$status = $result ? '✓ 成功' : '✗ 失敗';
echo str_pad($label, 24)
. str_pad($status, 12)
. ($meta['timed_out'] ? 'true' : 'false') . PHP_EOL;
fclose($stream);
}
echo PHP_EOL;
echo "【注意】ローカルファイル・メモリストリームでは" . PHP_EOL;
echo " set_timeout は成功しますが、実際にタイムアウトは" . PHP_EOL;
echo " 発生しません(常にデータが即座に読めるため)。" . PHP_EOL;
echo " タイムアウト制御はネットワーク・ソケット系ストリームで使ってください。" . PHP_EOL;
}
}
StreamTimeoutDemo::runAll();
出力例:
ストリーム種別 設定結果 timed_out初期値
----------------------------------------------------
ローカルファイル ✓ 成功 false
メモリストリーム ✓ 成功 false
stdin(標準入力) ✓ 成功 false
HTTPストリーム ✓ 成功 false
【注意】ローカルファイル・メモリストリームでは
set_timeout は成功しますが、実際にタイムアウトは
発生しません(常にデータが即座に読めるため)。
タイムアウト制御はネットワーク・ソケット系ストリームで使ってください。
関連する関数との比較
| 関数 | 役割 | 返り値 |
|---|---|---|
stream_set_timeout | 読み書きのタイムアウトを設定 | bool |
stream_get_meta_data | タイムアウト発生を検知(timed_out キー) | array |
stream_set_blocking | ブロッキング/ノンブロッキングの切り替え | bool |
stream_set_read_buffer | 読み取りバッファサイズを設定 | int(0=成功) |
fsockopen | ソケット接続(第5引数が接続タイムアウト) | resource|false |
stream_context_create | http ラッパーのタイムアウトを timeout オプションで設定 | resource |
fsockopen の接続タイムアウトとの違い
// fsockopen の第5引数 → 接続確立までのタイムアウト
$socket = fsockopen('example.com', 80, $errno, $errstr, 5.0);
// ↑ 接続タイムアウト(秒)
// stream_set_timeout → 接続後の読み書きのタイムアウト
stream_set_timeout($socket, 3);
// ↑ 読み書きタイムアウト(秒)
| 観点 | fsockopen 第5引数 | stream_set_timeout |
|---|---|---|
| 適用タイミング | TCP接続確立まで | 接続後の読み書き |
| 設定後の変更 | 不可 | いつでも変更可能 |
| タイムアウト検知 | 戻り値 false | stream_get_meta_data |
よくある注意点・落とし穴
1. タイムアウトは stream_get_meta_data で確認する
タイムアウト発生時に例外は投げられません。必ずメタデータを確認します。
// NG:タイムアウトに気づかずループが終わる
while (!feof($stream)) {
$data .= fread($stream, 4096);
}
// OK:タイムアウトを毎回チェックする
while (!feof($stream)) {
$chunk = fread($stream, 4096);
$meta = stream_get_meta_data($stream);
if ($meta['timed_out']) {
throw new RuntimeException("タイムアウト");
}
$data .= $chunk;
}
2. ローカルファイルへの効果は限定的
ローカルファイルやメモリストリームは即座にデータが読めるため、タイムアウトが実際に発生することはありません。主にネットワークソケットや標準入力で利用します。
3. $microseconds には 999999 以下の値を指定する
$microseconds は0〜999999の範囲(1秒未満)です。1,000,000以上を渡した場合の動作は未定義です。
// NG
stream_set_timeout($s, 0, 1_500_000); // 1,500,000μs は未定義
// OK:秒とマイクロ秒に分解する
stream_set_timeout($s, 1, 500_000); // 1秒500ミリ秒
4. stream_set_timeout は socket_set_timeout の別名
socket_set_timeout は stream_set_timeout の古い別名です。現在は stream_set_timeout を使うことが推奨されています。
// 古い書き方(PHP 8でも動くが非推奨)
socket_set_timeout($stream, 3);
// 推奨
stream_set_timeout($stream, 3);
まとめ
| 項目 | 内容 |
|---|---|
| 関数名 | stream_set_timeout(resource $stream, int $seconds, int $microseconds = 0): bool |
| 主な用途 | ネットワーク・ソケットストリームの読み書きタイムアウト設定 |
| タイムアウト検知 | stream_get_meta_data($stream)['timed_out'] で確認 |
| 返り値 | true = 成功、false = 失敗 |
| 注意点 | タイムアウト発生は自動検知されない、ローカルファイルは非効果 |
| PHP バージョン | PHP 4.3.0 以上 |
stream_set_timeout は、ネットワーク通信の安全性を高めるために欠かせない関数です。設定するだけでなく stream_get_meta_data でタイムアウトを明示的に検知する実装が重要です。
fsockopen の接続タイムアウトと組み合わせ、さらにリトライ処理も加えることで、現実の不安定なネットワーク環境にも耐えられる堅牢な通信処理を構築できます。
