为 PHP 5.5+ 发出 POST 正文中包含大文件的请求

Make POST request with large file in body for PHP 5.5+

有一个 API 我需要从我的 PHP 应用程序中使用。一个端点收到要上传的文件作为 POST 请求的正文。上传的文件可能相当大(最多 25GB)。端点 returns 一个简单的 JSON 内容,带有 200 OK 或不同的其他状态代码。

示例请求可能如下所示:

POST /api/upload HTTP/1.1
Host: <hostname>
Content-Type: application/octet-stream
Content-Length: 26843545600
Connection: close

<raw file data up to 25 GB>

基本上,我需要编写一个方法来执行此类请求而不会终止服务器。

我试图找到任何合理的实现,但据我所知,cURL 和非 cURL (stream_context_create) 方法都需要 string 请求主体,这可能会耗尽服务器内存。

有没有不用写a separate socket transport layer就能实现的简单方法?

由于没有找到更好的选择,我选择了 fsockopen 的默认解决方案。

这是实用程序函数的完整源代码,它将以低内存消耗执行 HTTP 请求。作为 data 参数,它可以接受 stringarraySplFileInfo 对象。

/**
 * Performs memory-safe HTTP request.
 *
 * @param string $url       Request URL, e.g. "https://example.com:23986/api/upload".
 * @param string $method    Request method, e.g. "GET", "POST", "PATCH", etc.
 * @param mixed $data       [optional] Data to pass with the request.
 * @param array $headers    [optional] Additional headers.
 *
 * @return string           Response body.
 *
 * @throws Exception
 */
function request($url, $method, $data = null, array &$headers = []) {
    static $schemes = [
        'https' => ['ssl://', 443],
        'http'  => ['', 80],
    ];

    $u = parse_url($url);
    if (!isset($u['host']) || !isset($u['scheme']) || !isset($schemes[$u['scheme']])) {
        throw new Exception('URL parameter must be a valid URL.');
    }

    $scheme = $schemes[$u['scheme']];
    if (isset($u['port'])) {
        $scheme[1] = $u['port'];
    }

    $fp = @fsockopen($scheme[0] . $u['host'], $scheme[1], $errno, $errstr);
    if ($fp === false) {
        throw new Exception($errstr, $errno);
    }

    $uri = isset($u['path']) ? $u['path'] : '/';
    if (isset($u['query'])) {
        $uri .= '?' . $u['query'];
    }

    if (is_array($data)) {
        $data = http_build_query($data);
        $headers['Content-Type'] = 'application/x-www-form-urlencoded';
        $headers['Content-Length'] = strlen($data);
    } elseif ($data instanceof SplFileInfo) {
        $headers['Content-Length'] = $data->getSize();
    }

    $headers['Host'] = $this->host;
    $headers['Connection'] = 'close';

    fwrite($fp, sprintf("%s /api%s HTTP/1.1\r\n", $method, $uri));
    foreach ($headers as $header => $value) {
        fwrite($fp, $header . ': ' . $value . "\r\n");
    }
    fwrite($fp, "\r\n");

    if ($data instanceof SplFileInfo) {
        $fh = fopen($data->getPathname(), 'rb');
        while ($chunk = fread($fh, 4096)) {
            fwrite($fp, $chunk);
        }
        fclose($fh);
    } else {
        fwrite($fp, $data);
    }

    $response = '';
    while (!feof($fp)) {
        $response .= fread($fp, 1024);
    }
    fclose($fp);

    if (false === $pos = strpos($response, "\r\n\r\n")) {
        throw new Exception('Bad server response body.');
    }

    $headers = explode("\r\n", substr($response, 0, $pos));
    if (!isset($headers[0]) || strpos($headers[0], 'HTTP/1.1 ')) {
        throw new Exception('Bad server response headers.');
    }

    return substr($response, $pos + 4);
}

用法示例:

$file = new SplFileObject('/path/to/file', 'rb');
$contents = request('https://example.com/api/upload', 'POST', $file, $headers);
if ($headers[0] == 'HTTP/1.1 200 OK') {
    print $contents;
}