Node.js 下载大文件卡住?read ETIMEDOUT 错误解决方案
2024-09-30 10:18:52
在使用 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
或者手动发送文件。如果无法修改后端代码,可以使用流式下载来解决问题。
常见问题及其解答:
-
问:为什么浏览器下载同样的文件没有问题?
答:浏览器通常会自动处理Transfer-Encoding: chunked
响应,并且可能使用不同的下载机制,例如多线程下载,因此不太容易出现超时问题。 -
问:流式下载和普通下载有什么区别?
答:普通下载会将整个文件下载到内存后再保存到磁盘,而流式下载则会将数据块逐个写入磁盘,减少内存占用。 -
问:如何判断我的服务器是否启用了
Transfer-Encoding: chunked
?
答:可以使用浏览器的开发者工具查看网络请求的响应头,或者使用curl -I
命令查看响应头信息。 -
问:
Content-Length
头部有什么作用?
答:Content-Length
头部告诉客户端文件的大小,方便客户端显示下载进度,并且可以避免一些潜在的错误。 -
问:如果我无法修改后端代码,还有什么其他解决方法?
答:可以尝试增加 Axios 的超时时间,或者使用其他支持流式下载的 HTTP 客户端库。