PHP fpassthru()函数

fpassthru()函数用于输出文件指针处的所有剩余数据,通常用于输出二进制文件(如图片、PDF、音频等),并将文件指针移动到文件末尾。

语法

int fpassthru ( resource $handle )

参数说明

参数 描述
handle 文件指针资源,通常由fopen()函数打开,并且必须以二进制模式(如'rb')打开

返回值

  • 成功时返回读取并输出的字节数
  • 失败时返回false
  • 调用后,文件指针将移动到文件末尾(或到达EOF)

示例代码

示例1:输出图片文件

<?php
// 输出图片文件
$imagePath = 'photo.jpg';

// 检查文件是否存在
if (!file_exists($imagePath)) {
    die('图片文件不存在');
}

// 以二进制只读模式打开文件
$fp = fopen($imagePath, 'rb');
if ($fp) {
    // 设置正确的Content-Type头部
    header('Content-Type: image/jpeg');
    // 输出文件内容
    fpassthru($fp);
    // 关闭文件指针
    fclose($fp);
    exit; // 终止脚本执行,避免输出额外内容
} else {
    echo '无法打开图片文件';
}
?>

示例2:输出PDF文档

<?php
// 输出PDF文件
function outputPdfFile($filename) {
    if (!file_exists($filename)) {
        header('HTTP/1.1 404 Not Found');
        echo '文件不存在';
        return false;
    }

    // 获取文件信息
    $fileSize = filesize($filename);
    $modifiedTime = filemtime($filename);

    // 以二进制模式打开文件
    $fp = fopen($filename, 'rb');
    if (!$fp) {
        header('HTTP/1.1 500 Internal Server Error');
        echo '无法打开文件';
        return false;
    }

    // 设置PDF文件的HTTP头部
    header('Content-Type: application/pdf');
    header('Content-Disposition: inline; filename="' . basename($filename) . '"');
    header('Content-Length: ' . $fileSize);
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $modifiedTime) . ' GMT');
    header('Cache-Control: public, must-revalidate, max-age=0');
    header('Pragma: public');

    // 输出文件内容
    $bytes = fpassthru($fp);
    fclose($fp);

    return $bytes;
}

// 使用示例
if (isset($_GET['pdf'])) {
    outputPdfFile('document.pdf');
    exit;
}
?>

示例3:大文件下载功能

<?php
// 安全的大文件下载函数
function downloadFile($filePath, $downloadName = null) {
    // 安全检查
    if (!file_exists($filePath)) {
        return '文件不存在';
    }

    if (!is_readable($filePath)) {
        return '文件不可读';
    }

    // 防止目录遍历攻击
    $realPath = realpath($filePath);
    $baseDir = realpath('./downloads'); // 只允许下载downloads目录下的文件

    if (strpos($realPath, $baseDir) !== 0) {
        return '非法文件路径';
    }

    // 设置下载文件名
    if ($downloadName === null) {
        $downloadName = basename($filePath);
    }

    // 获取文件信息
    $fileSize = filesize($filePath);
    $fileExt = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));

    // 根据文件类型设置Content-Type
    $mimeTypes = [
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png' => 'image/png',
        'gif' => 'image/gif',
        'pdf' => 'application/pdf',
        'zip' => 'application/zip',
        'txt' => 'text/plain',
        'csv' => 'text/csv',
    ];

    $contentType = $mimeTypes[$fileExt] ?? 'application/octet-stream';

    // 设置HTTP头部
    header('Content-Type: ' . $contentType);
    header('Content-Disposition: attachment; filename="' . rawurlencode($downloadName) . '"');
    header('Content-Length: ' . $fileSize);
    header('Cache-Control: no-cache, must-revalidate');
    header('Expires: 0');

    // 打开文件并输出
    $fp = fopen($filePath, 'rb');
    if ($fp) {
        fpassthru($fp);
        fclose($fp);
        return true;
    }

    return false;
}

// 使用示例
if (isset($_GET['file'])) {
    $file = $_GET['file'];
    downloadFile('downloads/' . $file);
    exit;
}
?>

示例4:与readfile()函数的对比

<?php
// fpassthru()和readfile()的功能对比

$filename = 'example.txt';

echo "

使用fpassthru()输出文件内容:

"; // 使用fpassthru()需要先打开文件 $fp = fopen($filename, 'rb'); if ($fp) { // 获取当前输出缓冲内容 ob_start(); $bytes1 = fpassthru($fp); $output1 = ob_get_clean(); fclose($fp); echo "输出字节数: " . $bytes1 . "<br>"; echo "内容预览: " . htmlspecialchars(substr($output1, 0, 100)) . "...<br><br>"; } echo "

使用readfile()输出文件内容:

"; // readfile()更简单,不需要显式打开文件 $bytes2 = readfile($filename); echo "<br>输出字节数: " . $bytes2 . "<br>"; echo "

性能对比测试:

"; $testFile = 'large_file.bin'; // 假设有一个大文件 if (file_exists($testFile)) { // 测试fpassthru() $start1 = microtime(true); $fp = fopen($testFile, 'rb'); fpassthru($fp); fclose($fp); $time1 = microtime(true) - $start1; // 测试readfile() $start2 = microtime(true); readfile($testFile); $time2 = microtime(true) - $start2; echo "fpassthru()耗时: " . number_format($time1, 4) . " 秒<br>"; echo "readfile()耗时: " . number_format($time2, 4) . " 秒<br>"; } ?>

示例5:输出视频文件(支持断点续传)

<?php
// 支持HTTP范围请求(断点续传)的视频输出
function streamVideo($filePath) {
    if (!file_exists($filePath)) {
        header('HTTP/1.1 404 Not Found');
        return;
    }

    $fileSize = filesize($filePath);
    $fileName = basename($filePath);
    $fileExt = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));

    // 视频文件的MIME类型
    $videoTypes = [
        'mp4' => 'video/mp4',
        'webm' => 'video/webm',
        'ogg' => 'video/ogg',
        'avi' => 'video/x-msvideo',
        'mov' => 'video/quicktime',
    ];

    $contentType = $videoTypes[$fileExt] ?? 'application/octet-stream';

    // 处理HTTP范围请求
    $range = null;
    if (isset($_SERVER['HTTP_RANGE'])) {
        preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
        $start = (int)$matches[1];
        $end = isset($matches[2]) ? (int)$matches[2] : $fileSize - 1;
        $range = [$start, $end];
    }

    // 打开文件
    $fp = fopen($filePath, 'rb');
    if (!$fp) {
        header('HTTP/1.1 500 Internal Server Error');
        return;
    }

    if ($range) {
        // 部分内容响应
        header('HTTP/1.1 206 Partial Content');
        header('Content-Type: ' . $contentType);
        header('Content-Range: bytes ' . $range[0] . '-' . $range[1] . '/' . $fileSize);
        header('Content-Length: ' . ($range[1] - $range[0] + 1));
        header('Accept-Ranges: bytes');

        // 跳转到指定位置
        fseek($fp, $range[0]);

        // 输出指定范围的内容
        $length = $range[1] - $range[0] + 1;
        $bytes = 0;
        while ($bytes < $length && !feof($fp)) {
            $chunk = min($length - $bytes, 8192); // 8KB块
            echo fread($fp, $chunk);
            $bytes += $chunk;
            flush(); // 刷新输出缓冲
        }
    } else {
        // 完整文件响应
        header('Content-Type: ' . $contentType);
        header('Content-Length: ' . $fileSize);
        header('Accept-Ranges: bytes');

        // 使用fpassthru输出整个文件
        fpassthru($fp);
    }

    fclose($fp);
}

// 使用示例
if (isset($_GET['video'])) {
    streamVideo('videos/sample.mp4');
    exit;
}
?>

注意事项

重要提示:
  • 二进制模式:必须使用二进制模式(如'rb')打开文件,否则在Windows系统上可能无法正确处理二进制文件
  • HTTP头部:在调用fpassthru()之前,必须先发送正确的Content-Type头部,否则浏览器可能无法正确解析内容
  • 输出缓冲:如果启用了输出缓冲,fpassthru()可能无法立即输出内容,需要使用ob_end_flush()flush()
  • 内存效率:fpassthru()非常适合输出大文件,因为它不会将整个文件加载到内存中
  • 文件指针位置:调用fpassthru()后,文件指针会移动到文件末尾,后续读取将返回EOF
  • 错误处理:fpassthru()失败时返回false,但通常会在失败时输出部分数据
  • readfile()替代:对于简单的文件输出,readfile()是更简单的选择,不需要显式打开和关闭文件
  • 安全性:确保用户无法通过路径遍历访问系统文件,需要对文件路径进行严格验证

fpassthru() vs readfile()

特性 fpassthru() readfile()
用法 需要先使用fopen()打开文件 直接传入文件名
灵活性 可以在输出前对文件指针进行操作(如fseek() 直接输出整个文件
性能 稍慢(需要两次函数调用) 稍快(单次函数调用)
内存使用 流式输出,内存效率高 流式输出,内存效率高
适用场景 需要操作文件指针的场景(如断点续传) 简单的文件下载/输出

相关函数

  • readfile() - 输出文件内容(不需要显式打开文件)
  • fopen() - 打开文件或URL
  • fread() - 读取文件内容
  • fclose() - 关闭一个已打开的文件指针
  • fseek() - 在文件指针中定位
  • ftell() - 返回文件指针读/写的位置
  • header() - 发送原始HTTP头部
  • filesize() - 获取文件大小

典型应用场景

  1. 图片输出:动态输出图片文件(如验证码、用户上传的图片)
  2. 文件下载:实现文件下载功能,支持大文件
  3. PDF预览:在浏览器中直接显示PDF文档
  4. 视频流:输出视频文件,支持断点续传
  5. 音频播放:输出音频文件供在线播放
  6. 二进制文件:输出ZIP、EXE等二进制文件
  7. 动态内容生成:结合GD库生成动态图片并输出