首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP cURL 实战:模拟 POST 文件上传(multipart/form-data)、接收端处理、进度条、断点续传、并发控制

     編輯:PHP 相關知識

核心概念:CURLFile 对象(PHP 5.5+)

旧版 @/path/to/file 语法已弃用(PHP 7.4 移除),必须使用 CURLFile 类构建上传对象,自动设置 Content-Type: multipart/form-data。

1. 客户端:上传单文件 + 表单字段

<?php
function uploadFile(string $url, string $filePath, array $fields = []): array
{
    if (!file_exists($filePath)) throw new \InvalidArgumentException("File not found: $filePath");

    $cFile = new CURLFile($filePath, mime_content_type($filePath), basename($filePath));
    // 字段名 'file' 对应服务端 $_FILES['file']
    $postData = array_merge(['file' => $cFile], $fields);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $postData,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_HTTPHEADER     => [
            'Expect:', // 禁用 100 Continue,避免某些代理/服务器卡顿
        ],
    ]);

    $response = curl_exec($ch);
    $errno = curl_errno($ch);
    $error = curl_error($ch);
    $info = curl_getinfo($ch);
    curl_close($ch);

    if ($errno) throw new \RuntimeException("cURL error ($errno): $error");
    return ['body' => $response, 'http_code' => $info['http_code']];
}

// 使用
$result = uploadFile(
    'https://api.example.com/upload',
    '/data/report.pdf',
    ['user_id' => 1001, 'category' => 'report']
);
var_dump($result);

2. 客户端:多文件上传

<?php
function uploadMultiple(string $url, array $filePaths, array $fields = []): array
{
    $postData = $fields;
    foreach ($filePaths as $i => $path) {
        if (!file_exists($path)) continue;
        $postData["file_$i"] = new CURLFile($path, mime_content_type($path), basename($path));
    }
    // 同单文件上传逻辑...
}

3. 服务端接收:标准 $_FILES 处理

<?php
// upload.php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);

$maxSize = 10 * 1024 * 1024; // 10MB
$allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];

$response = ['success' => false, 'message' => '', 'data' => null];

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    $response['message'] = 'Only POST allowed';
} elseif (empty($_FILES['file'])) {
    $response['message'] = 'No file uploaded';
} else {
    $file = $_FILES['file'];
    if ($file['error'] !== UPLOAD_ERR_OK) {
        $response['message'] = 'Upload error: ' . $file['error'];
    } elseif ($file['size'] > $maxSize) {
        $response['message'] = 'File too large';
    } elseif (!in_array($file['type'], $allowedTypes, true)) {
        $response['message'] = 'Invalid file type';
    } else {
        // 安全文件名:UUID + 原扩展名
        $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
        $newName = bin2hex(random_bytes(16)) . '.' . $ext;
        $dest = $uploadDir . $newName;

        if (move_uploaded_file($file['tmp_name'], $dest)) {
            $response['success'] = true;
            $response['data'] = ['filename' => $newName, 'size' => $file['size'], 'type' => $file['type']];
        } else {
            $response['message'] = 'Move failed';
        }
    }
}

header('Content-Type: application/json');
echo json_encode($response);

4. 进阶:上传进度条(客户端轮询 / XHR / WebSocket)

<?php
// 服务端:基于 session.upload_progress (php.ini 开启)
/*
session.upload_progress.enabled = On
session.upload_progress.cleanup = On
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
session.upload_progress.freq = "1%"
session.upload_progress.min_freq = "1"
*/

// 前端表单必须包含隐藏字段:
// 

// 进度查询接口 progress.php
session_start();
$key = "upload_progress_unique_form_id";
if (!empty($_SESSION[$key])) {
    $prog = $_SESSION[$key];
    echo json_encode([
        'start_time' => $prog['start_time'],
        'content_length' => $prog['content_length'],
        'bytes_processed' => $prog['bytes_processed'],
        'done' => $prog['done'],
    ]);
} else {
    echo json_encode(['done' => true]); // 未开始或已结束
}

5. 进阶:大文件分片上传 + 断点续传(Resumable.js / Uppy 原理)

<?php
// 服务端分片合并逻辑
$chunkDir = __DIR__ . '/chunks/';
$fileId = $_POST['file_id'] ?? ''; // 客户端生成 UUID
$chunkIndex = (int)($_POST['chunk_index'] ?? 0);
$totalChunks = (int)($_POST['total_chunks'] ?? 1);
$fileName = $_POST['file_name'] ?? '';

if ($fileId && $fileName) {
    $dir = $chunkDir . $fileId . '/';
    if (!is_dir($dir)) mkdir($dir, 0755, true);
    $chunkFile = $dir . $chunkIndex;
    move_uploaded_file($_FILES['file']['tmp_name'], $chunkFile);

    // 检查是否全部到齐
    $received = glob("$dir/*");
    if (count($received) === $totalChunks) {
        // 合并
        $finalPath = __DIR__ . '/uploads/' . $fileName;
        $fp = fopen($finalPath, 'wb');
        for ($i = 0; $i < $totalChunks; $i++) {
            fwrite($fp, file_get_contents("$dir/$i"));
        }
        fclose($fp);
        // 清理分片
        array_map('unlink', $received);
        rmdir($dir);
        echo json_encode(['done' => true, 'path' => $finalPath]);
    } else {
        echo json_encode(['done' => false, 'received' => count($received)]);
    }
}

6. 并发上传控制:cURL 多句柄

<?php
function uploadConcurrent(array $tasks, int $maxConcurrent = 5): array
{
    $mh = curl_multi_init();
    $handles = [];
    $results = [];

    // 初始化所有句柄
    foreach ($tasks as $i => $task) {
        $ch = curl_init($task['url']);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $task['data'],
            CURLOPT_RETURNTRANSFER => true,
        ]);
        curl_multi_add_handle($mh, $ch);
        $handles[$i] = $ch;
    }

    $running = null;
    do {
        // 添加新句柄直到达到并发上限
        while (count($handles) < $maxConcurrent && $next = array_shift($tasks)) {
            $ch = curl_init($next['url']);
            curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $next['data'], CURLOPT_RETURNTRANSFER => true]);
            curl_multi_add_handle($mh, $ch);
            $handles[] = $ch;
        }

        curl_multi_exec($mh, $running);
        // 读取完成的句柄
        while ($info = curl_multi_info_read($mh)) {
            $ch = $info['handle'];
            $idx = array_search($ch, $handles);
            $results[$idx] = [
                'body' => curl_multi_getcontent($ch),
                'http_code' => curl_getinfo($ch, CURLINFO_HTTP_CODE),
                'error' => curl_error($ch),
            ];
            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);
            unset($handles[$idx]);
        }
        if ($running) curl_multi_select($mh, 0.1);
    } while ($running || $handles);

    curl_multi_cleanup($mh);
    return $results;
}

安全清单

风险对策
任意文件上传白名单 MIME/扩展名、重命名存储、存储目录不可执行、病毒扫描
大文件撑爆内存/磁盘限制单文件/总大小、分片上传、流式处理、定时清理临时文件
CSRF上传接口验证 Token、SameSite Cookie
路径遍历basename() 只取文件名、存储路径拼接用 DIRECTORY_SEPARATOR
钓鱼/恶意内容内容安全策略、文件类型二次校验、用户举报机制

总结

PHP cURL 文件上传核心三要素:CURLFile 对象构建 multipart/form-data、服务端 $_FILES 规范处理 + 安全重命名、大文件分片/断点续传/并发控制。生产环境建议封装为 Uploader 服务类,统一错误码、日志、监控指标(上传成功率、平均耗时、带宽占用),并对接对象存储(S3/OSS/COS)实现直传,减轻应用服务器压力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved