暂停浏览器文件下载时如何终止后端流以解决内存泄漏问题
大文件下载暂停后后端循环内存泄漏的解决方案
问题分析
下载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
相关产品推荐
相关产品推荐

