【问题标题】:C# Async ApiController Closing OutputStream PrematurelyC# Async ApiController 过早关闭 OutputStream
【发布时间】:2015-04-10 22:23:23
【问题描述】:

今天的问题是,当使用 WebApi 2 和基于 Async ApiController 的 Get 方法时,它会返回文件的内容。当我将 Get 方法更改为同步时,它工作得很好,但是一旦我将它转换回异步,它就会过早地关闭流。 (提琴手报告连接被中止)工作同步代码是:

 public void Get(int id)
    {
        try
        {
            FileInfo fileInfo = logic.GetFileInfoSync(id);
            HttpResponse response = HttpContext.Current.Response;
            response.Clear();
            response.ClearContent();
            response.Buffer = true;
            response.AddHeader("Content-Disposition", "attachment; filename=\"" + fileInfo.Node.Name + fileInfo.Ext + "\"");
            response.AddHeader("Content-Length", fileInfo.SizeInBytes.ToString());
            response.ContentType = "application/octet-stream";
            logic.GetDownloadStreamSync(id, response.OutputStream);
            response.StatusCode = (int)HttpStatusCode.OK;
            //HttpContext.Current.ApplicationInstance.CompleteRequest();
             response.End();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

而GetDownloadStreamSync如下:

public async Task GetDownloadStream(string fileIdentifier, Stream streamToCopyTo)
{
    string filePath = Path.Combine(fileIdentifierFolder, fileIdentifier);
    using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, false))
    {
        fs.CopyTo(streamToCopyTo);
    }
 }

--------异步代码----------

异步版本完全相同,除了:

public async Task Get(int id)
{
    FileInfo fileInfo = await logic.GetFileInfoSync(id); // database opp
            HttpResponse response = HttpContext.Current.Response;
            response.Clear();
            response.ClearContent();
            response.Buffer = true;
            response.AddHeader("Content-Disposition", "attachment; filename=\"" + fileInfo.Node.Name + fileInfo.Ext + "\"");
            response.AddHeader("Content-Length", fileInfo.SizeInBytes.ToString());
            response.ContentType = "application/octet-stream";
            await logic.GetDownloadStreamSync(id, response.OutputStream); 
                           //database opp + file I/O
            response.StatusCode = (int)HttpStatusCode.OK;
             //HttpContext.Current.ApplicationInstance.CompleteRequest();
             response.End();
}

GetDownloadStream 的异步实现如下:(streamToCopyTo 是来自 response.OutputStream 的 OutputStream)

    public async Task GetDownloadStream(string fileIdentifier, Stream streamToCopyTo)
{
    using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, true))
    {
        await fs.CopyToAsync(streamToCopyTo);
    }
}

我们正在尝试从头到尾采用 async/await 模式,所以希望有人知道为什么这会失败?我也尝试过不调用 Response.End()、Response.Flush() 和 HttpContext.Current.ApplicationInstance.CompleteRequest()。此外,为了响应下面的问题/cmets,我在 response.End() 上放置了一个断点,结果它没有被 GetDownloadStream 方法命中。也许 OutputStream 不是异步的?欢迎任何想法! 谢谢

****************************** 最终解决方案 ******************* ********

非常感谢所有发表评论的人,尤其感谢 @Noseratio 对 FileOptions.DeleteOnClose 的建议。

[HttpGet]
public async Task<HttpResponseMessage> Get(long id)
{
        HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
        Node node = await logic.GetFileInfoForNodeAsync(id);

        result.Content = new StreamContent(await logic.GetDownloadStreamAsync(id));
        result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
        {
            FileName = node.Name + node.FileInfo.Extension
        };
        result.Content.Headers.ContentLength = node.FileInfo.SizeInBytes;
        return result
}

GetDownloadStreamAsync 看起来像这样:

 FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, FileOptions.DeleteOnClose | FileOptions.Asynchronous);

我忽略了我也在动态解密文件流,这确实有效,所以对于那些感兴趣的人......

FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, FileOptions.DeleteOnClose | FileOptions.Asynchronous);
RijndaelManaged rm = new RijndaelManaged();
return new CryptoStream(fs, GetDecryptor(rm, password), CryptoStreamMode.Read);

【问题讨论】:

  • MSDN 的解释是:“await 表达式不会阻塞它正在执行的线程。相反,它会导致编译器注册 async 方法的其余部分作为对等待的任务。然后控制权返回到异步方法的调用者。当任务完成时,它调用它的继续,并且异步方法的执行从它停止的地方继续。等待表达式只能出现在立即封闭的方法的主体中、lambda 表达式或由 async 修饰符标记的匿名方法。在其他地方,它被解释为标识符。"
  • 异步情况下方法的签名是什么? (我假设Task Get...,但最好确定一下)。
  • 如果您在问题中遗漏了什么,您可以发布完整的异步版本吗?
  • 我已经编辑了额外的代码。
  • 为什么一定要自己指定内容长度?你必须做这么多工作才能做这样简单的事情,这似乎很奇怪。你刚刚复制的流会发生什么?您的异步内容我们听起来。

标签: c# asp.net-mvc asp.net-web-api async-await asp.net-web-api2


【解决方案1】:

需要一个完整的复制案例来回答您的确切问题,但我认为您根本不需要 async/await。我还认为您应该尽可能避免直接使用HttpContext.Current.Response,尤其是在异步 WebAPI 控制器方法中。

在这种特殊情况下,您可以使用HttpResponseMessage:

[HttpGet]
public HttpResponseMessage Get(int id)
{
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    FileInfo fileInfo = logic.GetFileInfoSync(id);

    FileStream fs = new FileStream(
        filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, false);

    result.Content = new StreamContent(fs);
    result.Content.Headers.ContentType = 
        new MediaTypeHeaderValue("application/octet-stream");
    result.Content.Headers.ContentDisposition = 
        new ContentDispositionHeaderValue("attachment") 
        {
            FileName = fileInfo.Node.Name + fileInfo.Ext
        };
    result.Content.Headers.ContentLength = fileInfo.SizeInBytes;

    return result;
}

这里没有显式的异步,所以方法不是async。但是如果你还需要引入一些await,方法应该是这样的:

[HttpGet]
public async Task<HttpResponseMessage> Get(int id)
{
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    // ...
    await fs.CopyToAsync(streamToCopyTo)
    // ...
    return result;
}

【讨论】:

  • 关于第一个解决方案的问题,它是否让 FileStream 保持打开状态?我上面的解决方案有一个很好的 using 语句,以确保它被正确关闭和处理,而在第一个解决方案中,我有点担心 FileStream 保持打开状态,这是真的吗?我正在尝试将 FileStream 包装在一个 using 中,并在文件下载后从本地驱动器中删除该文件是否更有意义?
  • @TChadwick,不,它不会保持打开状态,一旦完成,WebAPI 运行时将在流上调用Close。如果此时需要删除它,只需使用FileOptions.DeleteOnClose | FileOptions.Asynchronous 作为new FileStream 的最后一个参数。
  • @TChadwick,那么你最终让这种方法奏效了吗?
  • 我确实让它工作了,最终的实现是在我最近编辑的帖子中。查看 *****Final Solution ***** 部分,并感谢 FileOptions.DeleteOnClose,它是让我的想法发挥作用的神奇酱汁。 :D
【解决方案2】:

你问题的根源其实在于Response.End()的使用。当您运行 Async 时,它将在完成流式传输文件内容之前执行 Response.End()。使用 Sync 版本时不会出现这种情况,因为 Response.End() 在完成流式传输文件内容之后才会被调用。

Response.End() 是一种非常糟糕的方式来表示您已完成处理,因为它会引发 TreadAbortException。相反,您应该使用HttpContext.Current.ApplicationInstance.CompleteRequest()

查看这篇文章了解更多信息Response.End, Response.Close, and How Customer Feedback Helps Us Improve MSDN Documentation

【讨论】:

  • 感谢您的回复,马修。不管有没有Response.End(),问题都是一样的,我也试过CompleteRequest()。
  • @TChadwick,这是我最近编写的一个内容助手,用于帮助通过 HTTP 响应将文件流式传输到客户端。 pastebin.com/5B29Pgdp 它不是异步的,但它应该在异步上下文中工作。试试看它是否有效。
  • 再次感谢您的回复,我对此进行了深入研究,当我在 Sync 中运行这些相同的调用时,它们工作正常。我发现调用 CompleteRequest() 实际上将响应代码更改为 204 (No Content) 而不是 200 导致浏览器不下载文件,并且我不得不调用 Response.End() 让它返回一个 200 代码和让它工作。我想接受异步读取文件并将其异步写入OutputStream,但在这一点上似乎不现实......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-02-25
  • 1970-01-01
  • 2015-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多