You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

暂停浏览器文件下载时如何终止后端流以解决内存泄漏问题

大文件下载暂停后后端循环内存泄漏的解决方案

问题分析

下载10GB以上大文件时,正常流程无异常,但前端暂停下载后,后端仍卡在while循环中持续读取文件并尝试写入响应,导致内存占用飙升引发泄漏。核心原因:

  • 后端同步处理流写入,未响应前端的暂停/取消信号
  • 前端暂停时未主动终止请求,后端无法感知连接状态变化
  • 后端每次循环重复分配缓冲区,加剧内存压力

后端C#代码修复方案

关键修改点

  • 改为异步方法,避免阻塞线程
  • 复用缓冲区,减少内存分配
  • 响应RequestAborted取消信号,及时终止循环
  • 使用异步写入方法,让操作能感知取消事件

修复后的代码

public async Task TransmitFileAsync()
{
    string fullPath = "D:\\backend-fileupload\\test.rar";
    string outFileName = "test.rar";
    // 复用缓冲区,避免每次循环新建
    byte[] buffer = new Byte[10485760];
    long dataToRead;
    var cancellationToken = HttpContext.RequestAborted;

    try
    {
        using var iStream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
        dataToRead = iStream.Length;

        Response.Clear();
        Response.ContentType = "application/octet-stream";
        Response.Headers.Add("Content-Disposition", $"attachment; filename={outFileName}");
        Response.Headers.Add("Content-Length", iStream.Length.ToString());
        Response.Headers.AcceptRanges = "bytes";

        // 提前检查取消信号
        cancellationToken.ThrowIfCancellationRequested();

        while (dataToRead > 0 && !cancellationToken.IsCancellationRequested)
        {
            // 异步读取文件,响应取消
            int length = await iStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
            if (length == 0) break;

            // 异步写入响应,传入取消令牌
            await Response.Body.WriteAsync(buffer, 0, length, cancellationToken);
            await Response.Body.FlushAsync(cancellationToken);

            dataToRead -= length;
        }
    }
    catch (OperationCanceledException)
    {
        // 捕获取消异常,正常终止流程无需抛出
    }
    catch (Exception ex)
    {
        throw new ApplicationException(ex.Message);
    }
    finally
    {
        await Response.Body.CloseAsync();
    }
}

额外优化:支持断点续传(可选)

若要实现真正的暂停/继续,后端需支持Range请求头,前端暂停后重新请求时带上已下载字节范围,后端从对应位置读取:

// 在方法开头添加Range处理逻辑
var rangeHeader = Request.Headers.Range.ToString();
long start = 0;
long end = dataToRead - 1;

if (!string.IsNullOrEmpty(rangeHeader))
{
    // 解析Range头,格式示例: bytes=1000-
    var rangeParts = rangeHeader.Replace("bytes=", "").Split('-');
    if (long.TryParse(rangeParts[0], out start))
    {
        end = rangeParts.Length > 1 && long.TryParse(rangeParts[1], out var parsedEnd) ? parsedEnd : dataToRead - 1;
        end = Math.Min(end, dataToRead - 1);
        Response.StatusCode = StatusCodes.Status206PartialContent;
        Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{dataToRead}");
        dataToRead = end - start + 1;
        iStream.Seek(start, SeekOrigin.Begin);
    }
}

前端JavaScript代码修复方案

关键修改点

  • 使用AbortController控制fetch请求,暂停时主动终止请求
  • 记录已下载字节,续传时发送Range头
  • 完善流的取消/清理逻辑

修复后的代码

let abortController;
let downloadedBytes = 0;
const requestUrl = "你的后端接口地址";
const fileName = "test.rar";
let fileStream;
let writer;
let reader;

// 开始/继续下载
function startDownload() {
    abortController = new AbortController();
    const headers = {
        'Content-Type': "application/octet-stream"
    };

    // 续传时添加Range头
    if (downloadedBytes > 0) {
        headers['Range'] = `bytes=${downloadedBytes}-`;
    }

    fetch(requestUrl, {
        method: 'POST',
        headers: headers,
        signal: abortController.signal
    }).then(res => {
        const readableStream = res.body;
        const chunkSize = Number(res.headers.get("Content-Length"));
        const totalSize = downloadedBytes + chunkSize;

        fileStream = streamSaver.createWriteStream(fileName, {
            size: totalSize
        });
        writer = fileStream.getWriter();

        if (window.WritableStream && readableStream?.pipeTo) {
            return readableStream.pipeTo(fileStream, { signal: abortController.signal }).then(() => {
                console.log('下载完成');
                downloadedBytes = 0;
            });
        }

        reader = readableStream?.getReader();
        const pump = () => {
            if (!reader) return Promise.resolve();
            return reader.read()
                .then(res => {
                    if (res.done) {
                        writer.close();
                        console.log('下载完成');
                        downloadedBytes = 0;
                        return;
                    }
                    downloadedBytes += res.value.length;
                    return writer.write(res.value).then(pump);
                })
                .catch(err => {
                    console.log('下载暂停/取消');
                    writer.abort();
                });
        };
        pump();
    }).catch(err => {
        console.log('请求终止');
    });
}

// 暂停下载
function pauseDownload() {
    if (abortController) abortController.abort();
    if (reader) reader.cancel();
    if (writer) writer.abort();
}

// 页面卸载时清理资源
window.onunload = pauseDownload;

内容的提问来源于stack exchange,提问作者wangwang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 17:40:33