[C#][ASP.NET] 将 Zip 写入 OutputStream 的几种方法比较

为什么要使用 OutputStream

产生 Large Size 的 Zip 压缩档时,如果将档案写入 MemoryStream,容易超出记忆体限制,引发 System.OutOfMemoryException 例外状况。
这时可以将写入目标,由记忆体改成网路串流 OutputStream,就能避免此问题,由于 .NET 版本众多,这边做个统整,纪录一下自己的实验心得。


# 版本1 - MVC5

新增 PushStreamResult

首先是 MVC5 的写法,新增 PushStreamResult 包装 FileResult。

public class PushStreamResult : FileResult{    private readonly Action<Stream> stream;    public PushStreamResult(string fileName, string contentType, Action<Stream> stream)        : base(contentType)    {        this.stream = stream;        this.FileDownloadName = fileName;    }        protected override void WriteFile(HttpResponseBase response)    {        response.Buffer = false;        stream(response.OutputStream);    }}

参考文章:
https://stackoverflow.com/questions/943122/writing-to-output-stream-from-action

用法

public ActionResult Zip(){    var funcZIP = (Action<Stream>)((stream) =>    {        using (var archive = new ZipArchive(stream, ZipArchiveMode.Create))        {            for (var i = 0; i < 1000; i++)            {                var zipEntry = archive.CreateEntry($@"aaa{i}.pdf", CompressionLevel.Fastest);                using (var zipStream = zipEntry.Open())                {                    var bytes = System.IO.File.ReadAllBytes(@"D:\专案\aaa.pdf");                    zipStream.Write(bytes, 0, bytes.Length);                }            }        }    });    return new PushStreamResult("aaa.zip", "application/zip",        stream => funcZIP(new ZipWrapStream(stream)));}

※ ZipWrapStream
由于 OutputStream 的 Position 属性是不可读的,所以需要重新包装才能让 ZipArchive 使用。(程式放在文章下方)

结果

成功压缩 1000 个 PDF 档案。

http://img2.58codes.com/2024/20106865rU4b1BBBUu.jpg

不过如果中间发生例外状况,虽然档案可以成功下载,但里面的 PDF 是缺少的,且档案会被加入错误讯息。

http://img2.58codes.com/2024/20106865N3UqUW3EwR.jpg

http://img2.58codes.com/2024/201068650ylb3phiuL.jpg


# 版本2 - Web API

想到 Web API 有现成的 PushStreamContent 可以使用,不知道能不能解决 PDF 档案缺失的问题。

用法

public HttpResponseMessage Zip(){    var funcZIP = (Action<Stream>)((stream) =>    {        using (var archive = new ZipArchive(stream, ZipArchiveMode.Create))        {            for (var i = 0; i < 1000; i++)            {                if (i == 100)                    throw new Exception();                var zipEntry = archive.CreateEntry($@"aaa{i}.pdf", CompressionLevel.Fastest);                using (var zipStream = zipEntry.Open())                {                    var bytes = System.IO.File.ReadAllBytes(@"D:\专案\aaa.pdf");                    zipStream.Write(bytes, 0, bytes.Length);                }            }        }    });        var response = new HttpResponseMessage(HttpStatusCode.OK);    response.Content = new PushStreamContent((stream, content, transport) =>    {        funcZIP(new ZipWrapStream(stream));        stream.Close();    });    response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zip");    response.Content.Headers.ContentDisposition.FileName = "aaa.zip";    return response;}

结果

使用 Web API 就不会缺档了,会直接出现网路错误。 http://img2.58codes.com/2024/emoticon42.gif

http://img2.58codes.com/2024/20106865g8PVJM6TlH.jpg


# 版本3 - .NET Core 3.1

新增 FileCallbackResult

.NET Core 和 MVC5 一样没有内建功能可以用,需要自己包装 FileResult。

public class FileCallbackResult : FileResult{    private readonly Func<Stream, ActionContext, Task> callback;    public FileCallbackResult(string fileName, string contentType, Func<Stream, ActionContext, Task> callback)        : base(contentType)    {        this.callback = callback;        this.FileDownloadName = fileName;    }    public override Task ExecuteResultAsync(ActionContext context)    {        var executor = new FileCallbackResultExecutor(context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>());        return executor.ExecuteAsync(context, this);    }    private sealed class FileCallbackResultExecutor : FileResultExecutorBase    {        public FileCallbackResultExecutor(ILoggerFactory loggerFactory)            : base(CreateLogger<FileCallbackResultExecutor>(loggerFactory))        {        }        public Task ExecuteAsync(ActionContext context, FileCallbackResult result)        {            SetHeadersAndLog(context, result, null, false);            return result.callback(context.HttpContext.Response.Body, context);        }    }}

参考文章:
https://stackoverflow.com/questions/42771409/how-to-stream-with-asp-net-core

用法

public ActionResult Zip(){    return new FileCallbackResult("aaa.zip", "application/zip", async (stream, _) =>    {        using (var archive = new ZipArchive(new ZipWrapStream(stream), ZipArchiveMode.Create))        {            for (var i = 0; i < 1000; i++)            {                if (i == 100)                    throw new Exception();                var zipEntry = archive.CreateEntry($@"aaa{i}.pdf", CompressionLevel.Fastest);                using (var zipStream = zipEntry.Open())                {                    var bytes = await System.IO.File.ReadAllBytesAsync(@"D:\专案\aaa.pdf");                    await zipStream.WriteAsync(bytes, 0, bytes.Length);                }            }        }        stream.Close();    });}

结果

一样会有缺档案的问题。


# 版本4 - Webform 的泛型处理常式

Webform 也测看看。

public void ProcessRequest(HttpContext context){    var funcZIP = (Action<Stream>)((stream) =>    {        using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, true))        {            for (var i = 0; i < 1000; i++)            {                if (i == 100)                    throw new Exception();                var zipEntry = archive.CreateEntry($@"aaa{i}.pdf", CompressionLevel.Fastest);                using (var zipStream = zipEntry.Open())                {                    var bytes = System.IO.File.ReadAllBytes(@"D:\专案\aaa.pdf");                    zipStream.Write(bytes, 0, bytes.Length);                }            }        }    });    HttpContext.Current.Response.BufferOutput = false;    HttpContext.Current.Response.Clear();    HttpContext.Current.Response.ContentType = System.Net.Mime.MediaTypeNames.Application.Zip;    HttpContext.Current.Response.AddHeader("content-disposition", "attachment; filename=" + HttpContext.Current.Server.UrlEncode("aaa.zip"));    funcZIP(new ZipWrapStream(HttpContext.Current.Response.OutputStream));    HttpContext.Current.Response.Flush();    HttpContext.Current.Response.Close();    HttpContext.Current.Response.End();}

结果

一样会缺档,看来只要是写入 OutputStream 都会。


总结

除了 Web API 之外,其他三种方法出现 Exception 都会导致 Zip 缺档和损毁,目前还没有解决办法,所以如果可以尽量使用 Web API 处理会比较好。


2020/08/21 更新

发现使用 Request.Abort() 可以中断连线,让浏览器下载出错,这样就不会有缺档和损毁的问题。

不过 .NET Core 已经没有 Request.Abort(),而是改成 HttpContext.Abort(),但这个方法不会真正中断连线,档案还是会下载成功,所以目前无解。
http://img2.58codes.com/2024/emoticon70.gif

除了 .NET Core 不建议使用外,其他版本都可以安心使用,修改后的程式如下。

MVC5

public class PushStreamResult : FileResult{    private readonly Action<Stream> stream;    public PushStreamResult(string fileName, string contentType, Action<Stream> stream)        : base(contentType)    {        this.stream = stream;        this.FileDownloadName = fileName;    }        public override void ExecuteResult(ControllerContext context)    {        try        {            base.ExecuteResult(context);        }        catch(Exception)        {            context.HttpContext.Request.Abort();            throw;        }    }    protected override void WriteFile(HttpResponseBase response)    {        response.Buffer = false;        stream(response.OutputStream);    }}

Webform

public void ProcessRequest(HttpContext context){    var funcZIP = (Action<Stream>)((stream) =>    {        using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, true))        {            for (var i = 0; i < 1000; i++)            {                if (i == 100)                    throw new Exception();                var zipEntry = archive.CreateEntry($@"aaa{i}.pdf", CompressionLevel.Fastest);                using (var zipStream = zipEntry.Open())                {                    var bytes = System.IO.File.ReadAllBytes(@"D:\专案\aaa.pdf");                    zipStream.Write(bytes, 0, bytes.Length);                }            }        }    });    HttpContext.Current.Response.BufferOutput = false;    HttpContext.Current.Response.Clear();    HttpContext.Current.Response.ContentType = System.Net.Mime.MediaTypeNames.Application.Zip;    HttpContext.Current.Response.AddHeader("content-disposition", "attachment; filename=" + HttpContext.Current.Server.UrlEncode("aaa.zip"));    try    {        funcZIP(new ZipWrapStream(HttpContext.Current.Response.OutputStream));    }    catch(Exception)    {        HttpContext.Current.Request.Abort();        throw;    }    HttpContext.Current.Response.Flush();    HttpContext.Current.Response.Close();    HttpContext.Current.Response.End();}

ZipWrapStream

由于 OutputStream 的 Position 是不可读取的,但 ZipArchive 需要这个属性,所以需要另外包装 Stream 类别。

public class ZipWrapStream : Stream{    private readonly Stream stream;    private long position = 0;    public ZipWrapStream(Stream stream)    {        this.stream = stream;    }    public override bool CanSeek { get { return false; } }    public override bool CanWrite { get { return true; } }    public override long Position    {        get { return position; }        set { throw new NotImplementedException(); }    }    public override bool CanRead { get { throw new NotImplementedException(); } }    public override long Length { get { throw new NotImplementedException(); } }    public override void Flush()    {        stream.Flush();    }    public override int Read(byte[] buffer, int offset, int count)    {        throw new NotImplementedException();    }    public override long Seek(long offset, SeekOrigin origin)    {        throw new NotImplementedException();    }    public override void SetLength(long value)    {        throw new NotImplementedException();    }    public override void Write(byte[] buffer, int offset, int count)    {        position += count;        stream.Write(buffer, offset, count);    }}

参考文章:
https://stackoverflow.com/questions/16585488/writing-to-ziparchive-using-the-httpcontext-outputstream


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章