在程序开发中,我们经常需要处理临时文件,例如:
- 安全替换大文件:先将内容写入临时文件,成功后再替换目标文件,避免写入过程中断导致数据损坏。
- 进程间数据传递:临时文件作为中间媒介,实现不同进程之间的数据交换。
- Web文件下载:将动态生成的数据写入临时文件,并提供给用户下载。
本文将以 ASP.NET Core 中的文件下载 场景为例,带你一步步实现更优雅的临时文件处理方案。
一、理解核心概念:Stream(流)
文件操作离不开流(Stream)的概念。你可以把Stream想象成一根水管,数据就像水一样,可以从一端流入,从另一端流出。
Stream是C#中用来处理数据的一种方式。它就像是一个管道,数据可以通过这个管道流动。你可以把数据从一个地方(比如硬盘上的文件)读出来,也可以把数据写到另一个地方(比如内存或者网络)。
使用Stream的好处是,它提供了一种统一的方式来处理不同类型的数据传输。在这个过程中,数据就像水管里的水一样源源不断的流动着,处理完的数据咱们可以舍弃,继续接收后面的即可,这样就可以实现传输例如磁盘上的大文件的信息,解决了一次性加载时的内存占用问题。
二 简单基础的实现 lv.1 (入门参考)
了解了流的基本概念后,我们先来看一个在ASP.NET Core中实现文件下载接口的基础代码。这是一个最直接的实现。为了快速达成目标,我们在指定文件夹创建了一个随机名称的临时文件,写入数据后,返回文件流供下载。
如果这段代码能成功运行,恭喜你!你已经实现了一个基础的Web文件下载接口。用户将得到一个文本文件,内容为查询参数的值。- // 一个简单的控制器实现
- public class DownloadController:Controller
- {
- string path = "d:\\tmp";
-
- // 一个简单的带参数的Get实现
- [HttpGet("DownloadFileMemory")]
- public IResult DownloadFileMemory(string query)
- {
- try
- {
- //创建一个新的文件流,可写(写入文件数据)、可读(读取数据最终给用户下载)
- FileStream fs = new FileStream(path+"\"+Guid.NewGuid(), FileMode.CreateNew, FileAccess.ReadWrite);
- // 一个简单的流写入对象,以文本写入为例
- using (StreamWriter writer = new StreamWriter(fs, leaveOpen: true))
- {
- writer.Write(query);
- }
- fs.Position = 0;
- // 返回下载的文件,并将文件名重设为"sample.txt"
- return Results.File(fs, "application/octet-stream", "sample.txt"); // 流回自带关闭
- }
- catch (Exception ex)
- {
- throw new Exception(ex.Message);
- }
- }
- }
复制代码 这是一个最基础的实现,刚接触代码时,为了简单粗暴实现目标,通常直接在指定的文件夹创建一个随机名称的文件,然后写入信息,返回Stream。上面的代码如果您能跑通,那么可以恭喜了!咱们已经实现了一个基础的web文件下载接口,能下载得到一个txt文本文件,里面写着query取值的文本。
这里有几个需要注意的点:
- FileStream 不能加using,因为using将导致fs对象在方法结束时立刻触发Dispose()导致流关闭,但此时用户端请求的流还未开始执行传输;
- StreamWriter 必须加参数leveOpen:true, 否则流会随着writer对象提前关闭,导致后续传输出错,如果不想用这个参数也可以把leveOpen:true和using都去掉,加上writer.Flush(),托管对象自动回收;
- fs.Position=0必须有,因为writer.Write()执行完已将流的位置Position写到了末尾,如果不重置,那么返回的将是一个空的Stream。
- `Return Results.File(),ASP.NET Core 框架在文件传输完成后,会自动关闭流,无需我们手动处理。
这个实现有个明显问题:方法执行后,临时文件会一直留在磁盘上,随着调用次数增加,会造成大量垃圾文件, 得定期手动清理。那么,如何避免临时文件残留呢?
三 避免临时文件残留 lv.2 (优化进阶)
当确认临时文件不再需要时,我们应在操作完成后立即删除它。对于文件下载场景,难点在于必须保证文件内容已成功传输给用户后,才能删除文件。即:在文件传输完成之前,数据流不能被破坏。
3.1 思路A 使用FileShare.Delete
这种方法利用了操作系统的一个特性:允许在文件仍被打开时将其标记为删除。具体做法是,在创建文件流时,设置其共享模式为“允许删除”。这样,我们就可以立即调用删除命令。此时,文件并不会立刻从磁盘上消失,而是会等到最后一个打开它的程序(即我们的下载进程)关闭文件流后,才被系统真正清理。
具体操作:将FileStream的FileShare属性设为Delete,然后在后续操作中直接用File.Delete()删除掉文件即可,代码如下:- // 前面的其他代码不变,省略
- // codes ...
- // 由于文件可被即时删除了,所以路径的指定不再重要
- // 可以方便的用系统自带方法直接生成一个空白的临时文件,并返回该文件路径
- var tmppath = Path.GetTempFileName()
- FileStream fs = new FileStream(tmpPath,
- FileMode.OpenOrCreate, FileAccess.ReadWrite,
- FileShare.Delete); // 增加标志位参数,可供其他进程删除
- // writer 的代码,和之前一致, 省略
- // codes ...
- System.IO.File.Delete(tmppath); // 直接删除
- return Results.File(fs, "application/octet-stream", "sample.txt");
- // 后面面的其他代码不变,省略
- // codes ...
复制代码 可能一开始会觉得奇怪,为什么文件删除都执行了,还能继续读取数据? 这是因为 Windows 和 .NET 中,文件删除是一个“延迟删除”操作。也就是说:当你用 FileShare.Delete 打开一个文件流时,其他进程(或同一进程)可以“标记”该文件为删除。但实际上,文件并不会立即从磁盘上消失,直到最后一个打开该文件的句柄被关闭。所以,只要你还持有文件流(FileStream)未关闭,你就可以继续读取数据,即使文件已经被“删除”。
3.2 思路B 使用MemoryStream
如果待下载的数据量不大,使用内存流是更简单、更高效的方案。内存流将数据完全保存在内存中,不再涉及磁盘I/O操作。当下载完成、流被关闭后,所占用的内存会被垃圾回收器自动释放,从根本上杜绝了文件残留的问题。这是一种非常干净利落的解决方案,特别适合生成小型报表、文本内容或图片等场景。- [HttpGet("DownloadFileMemory")]
- public IResult DownloadFileMemory(string query)
- {
- try
- {
- MemoryStream ms = new MemoryStream();
- using (StreamWriter writer = new StreamWriter(ms, leaveOpen: true))
- {
- writer.Write(query);
- }
- ms.Position = 0;
- return Results.File(ms, "application/octet-stream", "sample.txt");
- }
- catch (Exception ex)
- {
- throw new Exception(ex.Message);
- }
- }
复制代码 四 走向优雅 lv.3 (设计一个通用方案)
虽然上述两种优化方案已经能解决特定问题,但在复杂的实际项目中,我们可能需要一个更统一、更强大的解决方案。例如:
- 流来源多样:数据可能来自磁盘文件、内存流,甚至是非托管内存,流本身自带的信息甚少。
- 生命周期管理复杂:某些流需要缓存复用,某些则需要立即销毁。
- 规避潜在风险:FileShare.Delete 模式可能使文件在预期之外被删除,增加调试难度。
因此,一个理想的设计是创建一个通用的 TempDataStream 类。这个类旨在:
统一接口:无论底层是文件流还是内存流,对使用者来说都是同一个流类型。
自动化管理:在流关闭时,能根据预设策略自动执行清理工作(如删除临时文件、释放非托管内存等)。
灵活可控:允许明确指定某个临时流是否需要被销毁。
我们可以通过封装(Decorator Pattern)来实现它。让 TempDataStream 类继承 Stream 基类,并在内部持有一个真正的流实例(如 FileStream 或 MemoryStream)。TempDataStream 重写所有流操作方法(如 Read, Write, Seek 等),将其转发给内部持有的流实例。最关键的是,在其 Dispose 方法中,除了关闭内部流,还执行我们自定义的清理逻辑。- public class DownloadController:Controller
- {
- [HttpGet("DownloadFile")]
- public IResult DownloadFile()
- {
- TempDataStream tempStream = new TempDataStream(destoryOnDispose:true);
- tempStream.Write([1, 2, 3, 3]);
- return Results.File(tempStream, "application/octet-stream", "sample.txt");
- }
- }
复制代码 还希望他能兼容其他类型的流,作为统一的临时数据对象:- public Stream GetStream()
- {
- Stream stream; // 定义包装的内部Stream
- if(data.lengthM<1024){
- // 使用 MemoryStream 暂存到内存
- // stream=...
- }else{
- // stream 使用 FileStream 暂存到文件
- // stream=...
- }
- //else{
- // 非托管内存的stream
- // stream=...
- //}
- return new TempDataStream(stream,destoryOnDispose:true);
- }
复制代码 六、最后
感谢您的耐心阅读,希望各位从零开始的新朋友和老朋友有所收获!如果你对这篇文章的内容有任何建议或想法,欢迎随时交流!所有实现的代码以在上述章节完整提供。如果你觉得有用,欢迎去浏览一些本公众号的其他其他项目,点个 Star ⭐️支持一下! https://github.com/LdotJdot
P.S. 虽然现在AI是强大的工具,但那种为一个方案苦思冥想、最终灵光一现的顿悟感,以及亲手将代码调试成功的巨大成就感,是任何提示词都无法直接给予的。这恰恰是编程中最迷人的部分,是真正属于我们自己的成长。希望大家能享受不断实践、深入原理的过程,那才是通往前方之路的坚实阶梯。
欢迎关注公众号“萤火初芒“,更多分享等你来看:
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |