[PHP]hash_update_stream関数の使い方と実例 – ストリーミングデータのハッシュ化をマスターする

PHP

リアルタイムデータ処理や大容量ストリーミングデータの処理において、効率的なハッシュ計算は重要な技術です。PHPのhash_update_stream関数を使用することで、ストリームからのデータを直接ハッシュコンテキストに追加でき、メモリ効率的でパフォーマンスに優れた処理が実現できます。この記事では、hash_update_stream関数の基本的な使い方から実践的な応用例まで、詳しく解説していきます。

hash_update_stream関数とは?

hash_update_stream関数は、ストリームリソースからデータを読み取り、そのデータを直接ハッシュコンテキストに追加する関数です。ファイルハンドル、ネットワークストリーム、メモリストリームなど、様々なストリームリソースに対応しています。

従来の方法との比較

従来の方法(手動読み取り):

php$context = hash_init('sha256');
$handle = fopen('data.txt', 'r');
while (($chunk = fread($handle, 8192)) !== false) {
    hash_update($context, $chunk);
}
$hash = hash_final($context);
fclose($handle);

hash_update_stream方式(推奨):

php$context = hash_init('sha256');
$handle = fopen('data.txt', 'r');
hash_update_stream($context, $handle);
$hash = hash_final($context);
fclose($handle);

hash_update_stream関数の基本構文

phphash_update_stream(
    HashContext $context,
    resource $handle,
    int $length = -1
): int

パラメータの詳細

$context(ハッシュコンテキスト) hash_init()関数で初期化されたハッシュコンテキストオブジェクトです。

$handle(ストリームハンドル) 読み取り可能なストリームリソース。ファイルハンドル、ソケット、メモリストリームなどが指定できます。

$length(読み取り長) 読み取る最大バイト数。-1(デフォルト)の場合、ストリームの終端まで読み取ります。

戻り値 実際に読み取られ、ハッシュコンテキストに追加されたバイト数を返します。

基本的な使用例

例1: ファイルストリームからのハッシュ計算

php<?php
/**
 * ファイルストリームを使用したハッシュ計算
 */
function calculateStreamHash($filename, $algorithm = 'sha256') {
    if (!file_exists($filename)) {
        throw new InvalidArgumentException("ファイルが存在しません: {$filename}");
    }
    
    // ファイルを開く
    $handle = fopen($filename, 'rb');
    if (!$handle) {
        throw new RuntimeException("ファイルを開けませんでした: {$filename}");
    }
    
    try {
        // ハッシュコンテキストを初期化
        $context = hash_init($algorithm);
        
        // ストリームからハッシュコンテキストに追加
        $bytesRead = hash_update_stream($context, $handle);
        
        // 最終ハッシュ値を取得
        $hash = hash_final($context);
        
        return [
            'hash' => $hash,
            'bytes_processed' => $bytesRead,
            'algorithm' => $algorithm
        ];
        
    } finally {
        fclose($handle);
    }
}

// 使用例
try {
    $filename = 'sample_data.txt';
    $result = calculateStreamHash($filename);
    
    echo "ファイル: {$filename}\n";
    echo "ハッシュ値: {$result['hash']}\n";
    echo "処理バイト数: " . number_format($result['bytes_processed']) . "\n";
    echo "アルゴリズム: {$result['algorithm']}\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例2: 部分的なストリーム読み取り

php<?php
/**
 * ストリームの一部分のみをハッシュ計算する
 */
function calculatePartialStreamHash($filename, $offset = 0, $length = 1024, $algorithm = 'sha256') {
    $handle = fopen($filename, 'rb');
    if (!$handle) {
        throw new RuntimeException("ファイルを開けませんでした: {$filename}");
    }
    
    try {
        // 指定位置にシークする
        if ($offset > 0) {
            fseek($handle, $offset);
        }
        
        $context = hash_init($algorithm);
        
        // 指定した長さだけ読み取り
        $bytesRead = hash_update_stream($context, $handle, $length);
        $hash = hash_final($context);
        
        return [
            'hash' => $hash,
            'offset' => $offset,
            'requested_length' => $length,
            'actual_bytes_read' => $bytesRead,
            'current_position' => ftell($handle)
        ];
        
    } finally {
        fclose($handle);
    }
}

// 使用例
try {
    $filename = 'large_file.dat';
    
    // ファイルの先頭1KBのハッシュ
    $headerHash = calculatePartialStreamHash($filename, 0, 1024);
    echo "ヘッダーハッシュ: {$headerHash['hash']}\n";
    
    // ファイルの中間部分のハッシュ
    $middleHash = calculatePartialStreamHash($filename, 1024, 2048);
    echo "中間部分ハッシュ: {$middleHash['hash']}\n";
    
    // 実際に読み取られたバイト数を確認
    echo "実際の読み取りバイト数: {$middleHash['actual_bytes_read']}\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

実践的な応用例

例3: ネットワークストリームのハッシュ計算

php<?php
/**
 * HTTP/FTPストリームからのハッシュ計算
 */
class NetworkStreamHasher {
    private $timeout;
    private $userAgent;
    
    public function __construct($timeout = 30, $userAgent = 'PHP Stream Hasher/1.0') {
        $this->timeout = $timeout;
        $this->userAgent = $userAgent;
    }
    
    /**
     * HTTPリソースのハッシュを計算
     */
    public function hashHttpResource($url, $algorithm = 'sha256') {
        // ストリームコンテキストを作成
        $context = stream_context_create([
            'http' => [
                'timeout' => $this->timeout,
                'user_agent' => $this->userAgent,
                'follow_location' => true,
                'max_redirects' => 5
            ]
        ]);
        
        // HTTPストリームを開く
        $handle = fopen($url, 'rb', false, $context);
        if (!$handle) {
            throw new RuntimeException("HTTPストリームを開けませんでした: {$url}");
        }
        
        try {
            // ストリームメタデータを取得
            $metadata = stream_get_meta_data($handle);
            $headers = $metadata['wrapper_data'] ?? [];
            
            // ハッシュ計算
            $hashContext = hash_init($algorithm);
            $startTime = microtime(true);
            $bytesRead = hash_update_stream($hashContext, $handle);
            $endTime = microtime(true);
            $hash = hash_final($hashContext);
            
            return [
                'url' => $url,
                'hash' => $hash,
                'bytes_downloaded' => $bytesRead,
                'download_time' => $endTime - $startTime,
                'download_speed' => $bytesRead > 0 ? $bytesRead / ($endTime - $startTime) : 0,
                'http_headers' => $headers,
                'algorithm' => $algorithm
            ];
            
        } finally {
            fclose($handle);
        }
    }
    
    /**
     * 複数のHTTPリソースを並列処理
     */
    public function hashMultipleResources($urls, $algorithm = 'sha256') {
        $results = [];
        
        foreach ($urls as $url) {
            try {
                echo "処理中: {$url}\n";
                $result = $this->hashHttpResource($url, $algorithm);
                $results[$url] = $result;
                
                echo "完了: " . number_format($result['bytes_downloaded']) . " バイト, ";
                echo number_format($result['download_speed'] / 1024, 2) . " KB/s\n";
                
            } catch (Exception $e) {
                $results[$url] = [
                    'error' => $e->getMessage(),
                    'url' => $url
                ];
                echo "エラー: {$url} - " . $e->getMessage() . "\n";
            }
        }
        
        return $results;
    }
}

// 使用例
try {
    $hasher = new NetworkStreamHasher(60); // 60秒タイムアウト
    
    $urls = [
        'https://httpbin.org/bytes/1024',      // 1KBのランダムデータ
        'https://httpbin.org/base64/SFRUUEJJTiBpcyBhd2Vzb21l', // Base64データ
    ];
    
    echo "=== ネットワークリソースのハッシュ計算 ===\n";
    $results = $hasher->hashMultipleResources($urls);
    
    echo "\n=== 結果サマリー ===\n";
    foreach ($results as $url => $result) {
        if (isset($result['error'])) {
            echo "❌ {$url}: {$result['error']}\n";
        } else {
            echo "✅ {$url}:\n";
            echo "   ハッシュ: {$result['hash']}\n";
            echo "   サイズ: " . number_format($result['bytes_downloaded']) . " バイト\n";
        }
    }
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例4: メモリストリームを使用した動的データ処理

php<?php
/**
 * メモリストリームを使用した動的データハッシュ計算
 */
class MemoryStreamHasher {
    
    /**
     * 動的に生成されるデータのハッシュを計算
     */
    public function hashGeneratedData($dataGenerator, $algorithm = 'sha256') {
        // メモリストリームを作成
        $memoryStream = fopen('php://memory', 'w+b');
        if (!$memoryStream) {
            throw new RuntimeException("メモリストリームを作成できませんでした");
        }
        
        try {
            // データを生成してメモリストリームに書き込み
            $totalBytes = 0;
            foreach ($dataGenerator as $chunk) {
                $written = fwrite($memoryStream, $chunk);
                $totalBytes += $written;
            }
            
            // ストリームの先頭に戻る
            rewind($memoryStream);
            
            // ハッシュ計算
            $context = hash_init($algorithm);
            $bytesRead = hash_update_stream($context, $memoryStream);
            $hash = hash_final($context);
            
            return [
                'hash' => $hash,
                'generated_bytes' => $totalBytes,
                'processed_bytes' => $bytesRead,
                'algorithm' => $algorithm
            ];
            
        } finally {
            fclose($memoryStream);
        }
    }
    
    /**
     * CSVデータのハッシュ計算
     */
    public function hashCsvData($data, $algorithm = 'sha256') {
        $memoryStream = fopen('php://memory', 'w+b');
        
        try {
            // CSVデータを生成
            foreach ($data as $row) {
                fputcsv($memoryStream, $row);
            }
            
            rewind($memoryStream);
            
            // ハッシュ計算
            $context = hash_init($algorithm);
            $bytesRead = hash_update_stream($context, $memoryStream);
            $hash = hash_final($context);
            
            return [
                'hash' => $hash,
                'rows_processed' => count($data),
                'csv_bytes' => $bytesRead,
                'algorithm' => $algorithm
            ];
            
        } finally {
            fclose($memoryStream);
        }
    }
    
    /**
     * JSONデータのストリーミングハッシュ
     */
    public function hashJsonStream($objects, $algorithm = 'sha256') {
        $memoryStream = fopen('php://memory', 'w+b');
        
        try {
            // JSON配列の開始
            fwrite($memoryStream, '[');
            
            $first = true;
            foreach ($objects as $object) {
                if (!$first) {
                    fwrite($memoryStream, ',');
                }
                fwrite($memoryStream, json_encode($object));
                $first = false;
            }
            
            // JSON配列の終了
            fwrite($memoryStream, ']');
            
            rewind($memoryStream);
            
            // ハッシュ計算
            $context = hash_init($algorithm);
            $bytesRead = hash_update_stream($context, $memoryStream);
            $hash = hash_final($context);
            
            return [
                'hash' => $hash,
                'objects_count' => iterator_count($objects),
                'json_bytes' => $bytesRead,
                'algorithm' => $algorithm
            ];
            
        } finally {
            fclose($memoryStream);
        }
    }
}

// 使用例
try {
    $hasher = new MemoryStreamHasher();
    
    // 1. 動的データ生成のハッシュ
    echo "=== 動的データ生成のハッシュ ===\n";
    $dataGenerator = function() {
        for ($i = 0; $i < 1000; $i++) {
            yield "データ行 {$i}: " . str_repeat('*', $i % 50) . "\n";
        }
    };
    
    $result1 = $hasher->hashGeneratedData($dataGenerator());
    echo "生成バイト数: " . number_format($result1['generated_bytes']) . "\n";
    echo "ハッシュ: {$result1['hash']}\n\n";
    
    // 2. CSVデータのハッシュ
    echo "=== CSVデータのハッシュ ===\n";
    $csvData = [
        ['名前', '年齢', '職業'],
        ['田中太郎', 30, 'エンジニア'],
        ['佐藤花子', 25, 'デザイナー'],
        ['鈴木一郎', 35, 'マネージャー']
    ];
    
    $result2 = $hasher->hashCsvData($csvData);
    echo "処理行数: {$result2['rows_processed']}\n";
    echo "CSVバイト数: {$result2['csv_bytes']}\n";
    echo "ハッシュ: {$result2['hash']}\n\n";
    
    // 3. JSONストリームのハッシュ
    echo "=== JSONストリームのハッシュ ===\n";
    $jsonObjects = [
        ['id' => 1, 'name' => 'Product A', 'price' => 1000],
        ['id' => 2, 'name' => 'Product B', 'price' => 2000],
        ['id' => 3, 'name' => 'Product C', 'price' => 1500]
    ];
    
    $result3 = $hasher->hashJsonStream($jsonObjects);
    echo "オブジェクト数: {$result3['objects_count']}\n";
    echo "JSONバイト数: {$result3['json_bytes']}\n";
    echo "ハッシュ: {$result3['hash']}\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例5: ストリーミング暗号化との組み合わせ

php<?php
/**
 * ストリーミング暗号化とハッシュ計算の同時実行
 */
class StreamEncryptionHasher {
    private $cipher;
    private $key;
    private $iv;
    
    public function __construct($cipher = 'aes-256-cbc', $key = null) {
        $this->cipher = $cipher;
        $this->key = $key ?: random_bytes(32);
        $this->iv = random_bytes(openssl_cipher_iv_length($cipher));
    }
    
    /**
     * ストリームを暗号化しながらハッシュ計算
     */
    public function encryptAndHashStream($inputHandle, $outputHandle, $hashAlgorithm = 'sha256') {
        $hashContext = hash_init($hashAlgorithm);
        $encryptedHashContext = hash_init($hashAlgorithm);
        
        $chunkSize = 8192;
        $totalPlainBytes = 0;
        $totalEncryptedBytes = 0;
        
        // IVを出力ファイルの先頭に書き込み
        fwrite($outputHandle, $this->iv);
        hash_update($encryptedHashContext, $this->iv);
        $totalEncryptedBytes += strlen($this->iv);
        
        while (($chunk = fread($inputHandle, $chunkSize)) !== false && $chunk !== '') {
            // 平文のハッシュ計算
            hash_update($hashContext, $chunk);
            $totalPlainBytes += strlen($chunk);
            
            // 暗号化
            $encryptedChunk = openssl_encrypt(
                $chunk, 
                $this->cipher, 
                $this->key, 
                OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
            );
            
            if ($encryptedChunk === false) {
                throw new RuntimeException("暗号化に失敗しました");
            }
            
            // 暗号化データの書き込みとハッシュ計算
            fwrite($outputHandle, $encryptedChunk);
            hash_update($encryptedHashContext, $encryptedChunk);
            $totalEncryptedBytes += strlen($encryptedChunk);
        }
        
        return [
            'plain_hash' => hash_final($hashContext),
            'encrypted_hash' => hash_final($encryptedHashContext),
            'plain_bytes' => $totalPlainBytes,
            'encrypted_bytes' => $totalEncryptedBytes,
            'cipher' => $this->cipher,
            'iv' => bin2hex($this->iv)
        ];
    }
    
    /**
     * 暗号化ファイルの検証とハッシュ計算
     */
    public function verifyEncryptedFile($encryptedFile, $expectedPlainHash) {
        $handle = fopen($encryptedFile, 'rb');
        if (!$handle) {
            throw new RuntimeException("暗号化ファイルを開けませんでした");
        }
        
        try {
            // IVを読み取り
            $iv = fread($handle, openssl_cipher_iv_length($this->cipher));
            
            // 残りのデータをハッシュ計算(復号化しながら)
            $hashContext = hash_init('sha256');
            $chunkSize = 8192;
            
            while (($encryptedChunk = fread($handle, $chunkSize)) !== false && $encryptedChunk !== '') {
                $decryptedChunk = openssl_decrypt(
                    $encryptedChunk,
                    $this->cipher,
                    $this->key,
                    OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
                    $iv
                );
                
                if ($decryptedChunk !== false) {
                    hash_update($hashContext, $decryptedChunk);
                }
            }
            
            $actualHash = hash_final($hashContext);
            
            return [
                'expected_hash' => $expectedPlainHash,
                'actual_hash' => $actualHash,
                'verification_passed' => hash_equals($expectedPlainHash, $actualHash),
                'iv_used' => bin2hex($iv)
            ];
            
        } finally {
            fclose($handle);
        }
    }
}

// 使用例
try {
    echo "=== ストリーミング暗号化とハッシュ計算 ===\n";
    
    $encrypter = new StreamEncryptionHasher();
    
    // テストファイルを作成
    $plainFile = 'test_plain.txt';
    $encryptedFile = 'test_encrypted.bin';
    
    file_put_contents($plainFile, "これは暗号化テスト用のサンプルデータです。\n" . str_repeat("テストデータ ", 1000));
    
    // 暗号化とハッシュ計算を同時実行
    $plainHandle = fopen($plainFile, 'rb');
    $encryptedHandle = fopen($encryptedFile, 'wb');
    
    $result = $encrypter->encryptAndHashStream($plainHandle, $encryptedHandle);
    
    fclose($plainHandle);
    fclose($encryptedHandle);
    
    echo "平文ハッシュ: {$result['plain_hash']}\n";
    echo "暗号化ファイルハッシュ: {$result['encrypted_hash']}\n";
    echo "平文サイズ: " . number_format($result['plain_bytes']) . " バイト\n";
    echo "暗号化サイズ: " . number_format($result['encrypted_bytes']) . " バイト\n";
    echo "使用暗号: {$result['cipher']}\n";
    echo "IV: {$result['iv']}\n\n";
    
    // 検証
    echo "=== 暗号化ファイルの検証 ===\n";
    $verification = $encrypter->verifyEncryptedFile($encryptedFile, $result['plain_hash']);
    echo "期待ハッシュ: {$verification['expected_hash']}\n";
    echo "実際ハッシュ: {$verification['actual_hash']}\n";
    echo "検証結果: " . ($verification['verification_passed'] ? '成功' : '失敗') . "\n";
    
    // クリーンアップ
    unlink($plainFile);
    unlink($encryptedFile);
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

パフォーマンス比較とベンチマーク

php<?php
/**
 * 各種ストリーム処理方法のパフォーマンス比較
 */
function benchmarkStreamMethods($filename, $chunkSize = 8192) {
    if (!file_exists($filename)) {
        // テストファイル作成
        $handle = fopen($filename, 'wb');
        for ($i = 0; $i < 1024; $i++) {
            fwrite($handle, str_repeat(chr(rand(65, 90)), 1024)); // 1MB
        }
        fclose($handle);
    }
    
    $fileSize = filesize($filename);
    echo "テストファイルサイズ: " . number_format($fileSize / 1024 / 1024, 2) . "MB\n\n";
    
    // 方法1: hash_update_stream
    $start = microtime(true);
    $handle1 = fopen($filename, 'rb');
    $context1 = hash_init('sha256');
    hash_update_stream($context1, $handle1);
    $hash1 = hash_final($context1);
    fclose($handle1);
    $time1 = microtime(true) - $start;
    
    // 方法2: 手動チャンク読み取り + hash_update
    $start = microtime(true);
    $handle2 = fopen($filename, 'rb');
    $context2 = hash_init('sha256');
    while (($chunk = fread($handle2, $chunkSize)) !== false && $chunk !== '') {
        hash_update($context2, $chunk);
    }
    $hash2 = hash_final($context2);
    fclose($handle2);
    $time2 = microtime(true) - $start;
    
    // 方法3: hash_file (参考)
    $start = microtime(true);
    $hash3 = hash_file('sha256', $filename);
    $time3 = microtime(true) - $start;
    
    echo "結果比較:\n";
    echo "hash_update_stream: " . number_format($time1 * 1000, 2) . "ms\n";
    echo "手動チャンク処理: " . number_format($time2 * 1000, 2) . "ms\n";
    echo "hash_file: " . number_format($time3 * 1000, 2) . "ms\n\n";
    
    echo "ハッシュ値一致確認:\n";
    echo "hash_update_stream vs 手動処理: " . ($hash1 === $hash2 ? '一致' : '不一致') . "\n";
    echo "hash_update_stream vs hash_file: " . ($hash1 === $hash3 ? '一致' : '不一致') . "\n";
    
    return [
        'stream_time' => $time1,
        'manual_time' => $time2,
        'file_time' => $time3,
        'file_size' => $fileSize
    ];
}

// ベンチマーク実行
try {
    $testFile = 'benchmark_test.dat';
    benchmarkStreamMethods($testFile);
    
    // クリーンアップ
    if (file_exists($testFile)) {
        unlink($testFile);
    }
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

よくある質問と回答

Q: hash_update_stream()とhash_update_file()の違いは何ですか? A: hash_update_file()はファイルパスを指定してファイル全体を処理しますが、hash_update_stream()はすでに開かれたストリームリソースを操作します。hash_update_stream()の方がより柔軟で、ネットワークストリームやメモリストリーム、部分的な読み取りなどに対応できます。

Q: どのような場面でhash_update_stream()が最も有効ですか? A: 以下の場面で特に有効です:

  • リアルタイムストリーミングデータの処理
  • ネットワーク経由でのデータ受信中のハッシュ計算
  • 大容量ファイルの部分的なハッシュ計算
  • 動的に生成されるデータのハッシュ化

Q: エラーハンドリングで注意すべき点は? A: ストリームの状態確認、読み取り権限、ネットワークエラー、メモリ不足などを適切に処理する必要があります。また、ストリームリソースの適切なクローズも重要です。

Q: パフォーマンスを最適化するには? A: 適切なチャンクサイズの選択、不要なメモリコピーの回避、ストリームのバッファリング設定の調整などが効果的です。

まとめ

hash_update_stream関数は、PHPでストリーミングデータのハッシュ計算を効率的に行うための非常に強力なツールです。ファイルからネットワークストリーム、メモリストリームまで幅広いストリームリソースに対応し、リアルタイム処理や大容量データ処理において優れたパフォーマンスを発揮します。

今回紹介した実例を参考に、データ整合性チェック、セキュリティ検証、ストリーミング処理など様々な場面でhash_update_stream関数を活用してみてください。適切な実装により、メモリ効率的で高性能なデータ処理システムを構築できるでしょう。

現代のWebアプリケーションでは、リアルタイム性とセキュリティの両立が求められます。

タイトルとURLをコピーしました