65.9K
CodeProject 正在变化。 阅读更多。
Home

Dime 流式上传

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (37投票s)

2005年3月10日

9分钟阅读

viewsIcon

504414

downloadIcon

2872

本文概述了一种克服通过 Web 服务发送大文件问题的技术。

Screenshot of demo app

通知:有新文章可用

本文使用了 Web 服务增强功能 (WSE) 版本 2 和 .NET 版本 1.1。我写了一篇使用 WSE 3 和 .NET 2.0 的新文章,使用了 MTOM 而非 DIME,后者更快且更易于使用。本文保留给需要 .NET 1.1 / WSE 2 解决方案的用户。

摘要

本文介绍了一种解决方案,用于通过 HTTP Web 服务上传大文件,克服 IIS 请求超时和 IIS/ASP.NET 请求大小限制。除了提供大文件的可靠传输系统外,它还可以使 Windows Forms 应用程序使用进度条定期显示传输状态。

引言

如果您曾开发过将文件(可能很大)发送到具有 HTTP Web 服务的 Web 服务器的 Windows Forms 应用程序,您可能已经知道 .NET 框架处理此任务的效果不佳。Web 服务最初设计用于发送单个消息,但我们开发者就像我们一样,开始使用它们发送字节数组,即文件的二进制内容。对于大文件,这种方法不可扩展,因为 IIS 有请求超时时间和最大请求大小,这限制了我们可以发送到服务器的任何 Web 服务请求的大小。您还可能让用户等待很长时间才能返回操作成功或失败的结果。您也可能在客户端应用程序或服务器上遇到 OutOfMemoryException(因为字节数组必须一次性存储在内存中)。

感谢 Tulio Rodriguez

本文最初概述了一种将文件分割成 X 个文件片段,然后将它们全部发送到服务器,并在服务器上合并以重现原始文件的解决方案。这是一种通过 HTTP 实现“分块”并为用户提供定期反馈的快速方法(前提是分块足够小)。这种方法的问题显然是在发生任何意外错误时,客户机和服务器计算机上存在孤立文件的风险。

本文已根据 Tulio Rodriguez 允许我使用和发布的代码进行了更新。他的方法不是将文件分割成片段,而是将字节数组一次发送一个到服务器,服务器只是将每个块追加到服务器上的文件中。这是一个更优雅的解决方案,他为此方法付出了所有功劳。我对他的代码进行了以下修改:

  • 添加了性能改进,修复了错误,删除了不必要的代码。
  • 添加了结构化异常处理,而不是返回错误代码。
  • 实现了 Dime 附件而不是字节数组。这提高了性能并减少了带宽。原因是字节在作为数组发送到普通 Web 方法时会序列化为 XML。Dime 允许未序列化的二进制字节作为附件发送到 XML 消息,从而避免了在序列化和反序列化过程中 XML 应用的昂贵“填充”。
  • 添加了对非常大文件的支持。

替代方法

在您将此集成到应用程序之前,请查看其他可能节省您时间和精力的方法:

  • 使用 Web 服务增强功能 (WSE) 的 Dime 分块功能使用 TCP 或 UDP。WSE 版本 2 支持“分块”,其中附件被分成小部分并单独发送,但这在 HTTP 上不起作用,而许多应用程序更喜欢使用 HTTP 来解决防火墙和部署问题。如果您可以使用 TCP 或 UDP,您很可能应该选择该路线。
  • 如果您只是想要一个普通 Web 服务的进度条,并且不担心超时或请求大小限制,Matt Powel 有一个文档齐全的解决方案,它会拦截传出流量并报告回您的进度条。我发现这个解决方案有点混乱,因为您需要向应用程序添加 SOAP 扩展,这使我的部署方案复杂化。但据我所知,他的解决方案无法解决 IIS 超时或最大请求长度问题。

分步概述

在本文提供的演示应用程序中,有一个 Windows Forms 应用程序,它连接到一个 Web 应用程序上的 Web 服务。文件从客户端应用程序发送,通过 Web 服务保存在 Web 服务器上。

  • 用户在 Win-Forms 应用程序中选择一个文件,并可以指定上传每个块的大小。然后用户单击上传按钮。
  • 在服务器上创建一个“实例”对象,该对象负责管理服务器上的上传并跟踪文件。该对象保留在服务器上的会话状态中,供客户端应用程序调用的每个 AppendChunk() 方法使用。
  • 上传在工作线程上启动,该线程一个接一个地发送块,直到上传完成。服务器接收每个块并将其追加到文件中。进行了一些错误检查,以确保块在正确的位置追加。
  • 工作线程每次发送一个块时都会引发一个事件,以允许用户界面更新进度条和/或状态栏消息。
  • 上传完成后,从会话状态中删除“实例”对象,以释放服务器上的内存。

性能

在我的本地网络环境测试中,我上传了一个 2.5 GB 的 DVD ISO 映像,分成 16 MB 的块,耗时不到 8 分钟。(我不得不将一些数据类型从 int 改为 long 来适应巨大的文件大小!)16 MB 的块显然非常大,但在内网环境中可能很合适。在我的 CMS 应用程序中,有通过拨号连接的客户端,我将块大小设置为 32 Kb,这在整个上传过程中为用户提供了快速反馈。

在一些性能分析中,我注意到对于快速网络,块大小对每个 AppendChunk() 操作的耗时没有明显影响。因此,最好选择网络可以容纳的最大块大小,同时仍能提供合理的进度条更新频率。

安装要求

  • 一个名为 UploadWeb 的虚拟目录,它应该指向 UploadWeb 文件夹。
  • 对虚拟目录的匿名访问权限。
  • 对虚拟目录中“Uploads”文件夹的写入权限。

可能的编译错误

如果您想编辑源代码并重新编译应用程序,您必须从 Microsoft 下载 Visual Studio 的 WSE(Web 服务增强功能)。一些用户在更新 Web 服务引用时遇到了问题,因为如果没有 WSE,Visual Studio 就不会创建 WSE 版本的代理类。如果您遇到以下任何编译错误,那么未为 Visual Studio 安装 WSE 可能是问题所在。

  • 'BufferedUploadWin.BufferedUpload' 不包含 'RequestSoapContext' 的定义
  • 类型或命名空间名称 'UploadWse' 在类或命名空间 'UploadWin.BufferedUploadServer' 中不存在(您是否缺少程序集引用?)

如果您在使用 WSE 时遇到问题,请访问 MSDN 的 WSE 网站

代码摘要

  • 步骤 1:用户单击上传按钮

    首先,我们创建一个“BufferedUpload”对象。这是 WinForms 应用程序用于将文件块发送到 Web 服务的辅助类。从下面的代码可以看到,块大小设置为“块大小”Domain-Up-Down 控件中的值。添加了两个事件处理程序,一个用于更新进度条,另一个用于响应上传状态的变化,例如 CompletedFailed 等。“BeginUpload()”方法是异步的,因此上传期间应用程序保持响应。

    ws = new BufferedUpload();
    ws.ChunkSize = Int32.Parse(this.dudChunkSize.Text)*1000;    // kilobytes
    
    ws.ProgressChanged += new ProgressChangedEventHandler(Upload_ProgressChanged);
    ws.StateChanged += new EventHandler(Upload_StateChanged);
    ws.BeginUploadChunks(this.textBox1.Text);
  • 步骤 2:初始化服务器上的“Instance”对象并开始上传块

    下面的代码概述了上传例程的一个简化版本。它本质上是一个 while 循环,它不断地将字节读入缓冲区,将字节作为 Dime 附件(通过 MemoryStream)附加,并将附件发送到 Web 服务。有关代码的进一步解释可以在源代码中的注释中找到。

    FileStream fs = new FileStream(Filename, FileMode.Open, FileAccess.Read);
    int bytesRead = 0; // used to store the number of bytes written each time 
    
    byte[] buffer = new byte;
    if (this.sentBytes == 0) 
    {
        // initialise an upload Instance to manage the file I/O for this upload
    
        this.instanceId = this.Initialize(Path.GetFileName(Filename), 
                                                     this.overwrite);
    }
    bytesRead = fs.Read(buffer, 0, bufferSize);
    while (bytesRead > 0 && this.state != UploadState.Aborting) 
    { 
        // copy the byte buffer into a memory stream
    
        // to send in a dime attachment
    
        MemoryStream ms = new MemoryStream(buffer, 0, buffer.Length); 
        DimeAttachment dimeAttach = new DimeAttachment("image/gif", 
                                         TypeFormat.MediaType, ms);
        this.RequestSoapContext.Attachments.Add(dimeAttach); 
        this.AppendChunk(this.instanceId, sentBytes, bytesRead);
        this.sentBytes += bytesRead; 
        bytesRead = fs.Read(buffer, 0, bufferSize); // read the next chunk (if it 
    
    }
  • 步骤 3:(在服务器上)将每个块追加到文件

    AppendChunk() Web 方法简单地将 Dime 附件读回 byte[] 并调用 Instance.AppendChunk() 方法,如下所示:

    public void AppendChunk(byte[] buffer, long offset, int bufferSize)
    {
        // make sure that the file exists
    
        if (!System.IO.File.Exists(this.tempFilename))
            CustomSoapException("Cannot Find Temp File", this.tempFilename);
                 // if the temp file size is not the same
    
                 // as the offset then something went wrong....
    
        if (tempFilesize != offset) 
            CustomSoapException("Transfer Corrupted", 
               String.Format("The file size is {0}, expected {1} bytes", 
               tempFilesize, offset));
        else
        {
            // offset matches the filesize, so the chunk
    
            // is to be inserted at the end of the file.
    
            FileStream FileStream = new FileStream(this.tempFilename, 
                                                    FileMode.Append);
            FileStream.Write(buffer, 0, bufferSize);
            FileStream.Close();
        }
    }

将此代码集成到您自己的应用程序中

在您的 Win-Forms 应用程序中,添加“BufferUpload.cs”文件,并遵循演示应用程序中使用的逻辑。您不需要在 PC 上安装 WSE2 即可使其正常工作,因为 DLL 已随演示应用程序一起打包。只需在您的 WinForms 和 WebForms 项目中引用此 DLL 即可。

在您的 Web 应用程序中,添加“Instance.cs”和“upload.asmx”文件(带有 .asmx 的代码隐藏),并确保 Win 应用程序引用了您的 Web 服务的正确 URL。还要确保在您的 web.config 文件中包含 WSE 部分(从演示应用程序中的那个复制)。

Windows Forms 应用程序中的附加功能

您会注意到在演示应用程序中,状态栏的左侧会显示一个等待光标图标。这除了进度条之外,还能向用户传达一种“繁忙”的感觉。我从 FotoVision .NET 示例应用程序借用了这个技巧。另外,如果您想知道,进度条实际上并不是状态栏的一部分,它只是放置在状态栏上方并固定在角落。我将这两个功能组合成一个“FormBar”控件,它应该是 Win-Forms 应用程序的一种通用进度工具。它提供了更新状态栏消息和进度条的方法,这些方法可以安全地从 UI 线程或工作线程调用。

我还添加了一个简单的“重试”功能。如果由于任何原因上传失败,它会弹出一个表单,并在块未能到达时从最后一个成功的位置继续。要测试这一点,请在传输过程中暂停您的 Web 服务器,您将看到重试窗口弹出。它会一直失败,直到您恢复 Web 服务器。

结论

在过去的两年里,我断断续续地花了大量精力寻找一种适合通过 HTTP 上传大文件的解决方案,我认为这是迄今为止最简单、最有效的解决方案。它非常健壮,并且支持续传功能是一个额外的优点。另外,能够可靠地上传 DVD ISO 映像,在我看来也相当不错!

注释

如果您有任何问题/建议,请在下方发布。

尽情享用!

© . All rights reserved.