返回

Node.js 下载大文件卡住?read ETIMEDOUT 错误解决方案

php

在使用 Node.js 下载文件,特别是大文件时,你可能会碰到下载过程突然卡住,最后抛出一个 read ETIMEDOUT 错误的情况。更奇怪的是,在浏览器里下载同样的文件却一切正常。这其中的原因是什么呢?让我们来仔细分析一下,找出解决方法。

首先,我们需要了解你的 PHP 后端代码是如何处理文件下载的。你使用了 readfile() 函数直接输出文件内容。这种做法虽然简单直接,但在处理大文件时却容易出问题。因为它会尝试将整个文件一次性加载到内存,然后再输出。如果文件体积过大,就可能导致内存不足或超时。

其次,你的 HTTP 响应头里包含了 Transfer-Encoding: chunked。这意味着服务器会把文件分成多个数据块(chunk)发送,每个数据块都带有自己的长度信息。这种方式适合传输长度不确定的数据,比如动态生成的内容。但是,当你使用 Transfer-Encoding: chunked 时,就不能再设置 Content-Length 头部,因为数据长度是动态变化的。

Axios 在处理 Transfer-Encoding: chunked 响应时,默认会等待所有数据块都接收完毕,然后把它们拼接起来,再触发 res.data。这就会导致 Node.js 进程必须等到所有数据块都接收完成才能继续执行。如果网络连接不稳定或者文件太大,就很容易发生超时。

那么,我们该如何解决这个问题呢?

以下提供几种可行的解决方案:

1. 使用流式下载:

与其把整个文件加载到内存,不如采用流式下载的方式,将数据块逐个写入文件。这样既可以减少内存占用,又能更快地开始下载过程。

const axios = require('axios').default;
const path = require('path');
const fs = require('fs');

const filePath = path.join(__dirname, `./file.zip`);
const writer = fs.createWriteStream(filePath);

axios({
  method: 'get',
  url: 'https://example.org/endpoint.php',
  responseType: 'stream'
})
.then(response => {
  response.data.pipe(writer);
  return new Promise((resolve, reject) => {
    writer.on('finish', resolve);
    writer.on('error', reject);
  });
})
.then(() => {
  console.log('Download completed!');
})
.catch(error => {
  console.error('Download failed:', error);
});

这段代码使用了 Axios 的 responseType: 'stream' 选项,将响应数据作为流来处理。接着,我们使用 pipe() 方法将数据流连接到文件写入流,实现流式下载。

2. 修改 PHP 后端代码:

如果你可以修改 PHP 后端代码,可以尝试以下两种方法:

  • 禁用 Transfer-Encoding: chunked:

    找到 Apache 配置文件(例如 httpd.conf 或 .htaccess),添加以下指令:

    SetOutputFilter DEFLATE
    SetEnvIfNoCase Request_URI \.zip$ no-gzip dont-vary
    

    这会禁用针对 .zip 文件的压缩和分块传输,让你可以设置 Content-Length 头部。

  • 使用 fopen()fread() 手动发送文件:

    $zipPath = "/path/to/zip";
    $filesize = filesize($zipPath);
    
    header('Content-Description: File Transfer');
    header('Content-Type: application/zip');
    header('Content-Disposition: attachment; filename="file.zip"');
    header('Content-Transfer-Encoding: binary');
    header('Connection: Keep-Alive');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header('Pragma: public');
    header('Content-Length: ' . $filesize);
    
    $fp = fopen($zipPath, 'rb');
    while (!feof($fp)) {
        echo fread($fp, 8192);
        flush();
    }
    fclose($fp);
    exit();
    

    这段代码使用 fopen() 打开文件,然后使用 fread() 逐块读取文件内容并输出。这样可以避免将整个文件加载到内存。

通过以上方法,你应该可以解决 Node.js 下载文件卡住的问题。选择哪种方法取决于你的具体情况和需求。如果可以修改后端代码,建议优先考虑禁用 Transfer-Encoding: chunked 或者手动发送文件。如果无法修改后端代码,可以使用流式下载来解决问题。

常见问题及其解答:

  1. 问:为什么浏览器下载同样的文件没有问题?
    答:浏览器通常会自动处理 Transfer-Encoding: chunked 响应,并且可能使用不同的下载机制,例如多线程下载,因此不太容易出现超时问题。

  2. 问:流式下载和普通下载有什么区别?
    答:普通下载会将整个文件下载到内存后再保存到磁盘,而流式下载则会将数据块逐个写入磁盘,减少内存占用。

  3. 问:如何判断我的服务器是否启用了 Transfer-Encoding: chunked
    答:可以使用浏览器的开发者工具查看网络请求的响应头,或者使用 curl -I 命令查看响应头信息。

  4. 问:Content-Length 头部有什么作用?
    答:Content-Length 头部告诉客户端文件的大小,方便客户端显示下载进度,并且可以避免一些潜在的错误。

  5. 问:如果我无法修改后端代码,还有什么其他解决方法?
    答:可以尝试增加 Axios 的超时时间,或者使用其他支持流式下载的 HTTP 客户端库。