【问题标题】:Uploading HTTP progress tracking上传 HTTP 进度跟踪
【发布时间】:2012-01-01 03:21:29
【问题描述】:

我有 WPF 应用程序,我正在编写将文件发布到社交网络之一。 上传本身工作得很好,但我想提供一些指示我上传的进度。

我尝试了很多方法来做到这一点:

1) HttpWebRequest.GetStream 方法:

using (
 var FS = File.Open(
  localFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    long len = FS.Length;
    HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
    request.Method = "POST";
    request.ProtocolVersion = HttpVersion.Version11;
    request.ContentType = "multipart/form-data; boundary=--AaB03x";
    //predata and postdata is two byte[] arrays, that contains
    //strings for MIME file upload (defined above and is not important)
    request.ContentLength = predata.Length + FS.Length + postdata.Length;
    request.AllowWriteStreamBuffering = false;
    using (var reqStream = request.GetRequestStream())
    {
        reqStream.Write(predata, 0, predata.Length);
        int bytesRead = 0;
        int totalRead = 0;
        do
        {
            bytesRead = FS.Read(fileData, 0, MaxContentSize);
            totalRead += bytesRead;
            reqStream.Write(fileData, 0, bytesRead);
            reqStream.Flush(); //trying with and without this
            //this part will show progress in percents
            sop.prct = (int) ((100*totalRead)/len);
        } while (bytesRead > 0);
        reqStream.Write(postdata, 0, postdata.Length);
    }
    HttpWebResponse responce = (HttpWebResponse) request.GetResponse();
    using (var respStream = responce.GetResponseStream())
    {
        //do things
    }
}

2) WebClient 方式(短得多):

void UploadFile (url, localFilePath)
{
    ...
    WebClient client = new WebClient();
    client.UploadProgressChanged += new UploadProgressChangedEventHandler(UploadPartDone);
    client.UploadFileCompleted += new UploadFileCompletedEventHandler(UploadComplete);
    client.UploadFileAsync(new Uri(url), localFilePath);
    done.WaitOne();

    //do things with responce, received from UploadComplete
    JavaScriptSerializer jssSer = new JavaScriptSerializer();
    return jssSer.Deserialize<UniversalJSONAnswer>(utf8.GetString(UploadFileResponce));
    //so on...
    ...
}

void UploadComplete(object sender, UploadFileCompletedEventArgs e)
{
    UploadFileResponce=e.Result;
    done.Set();
}

void UploadPartDone(object sender, UploadProgressChangedEventArgs e)
{
    //this part expected to show progress
    sop.prct=(int)(100*e.BytesSent/e.TotalBytesToSend);
}

3) 偶TcpClient方式:

using (
 var FS = File.Open(
  localFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    long len = FS.Length;
    long totalRead = 0;
    using (var client = new TcpClient(urli.Host, urli.Port))
    {
        using (var clearstream = client.GetStream())
        {
            using (var writer = new StreamWriter(clearstream))
            using (var reader = new StreamReader(clearstream))
            {
                //set progress to 0
                sop.prct = 0;
                // Send request headers
                writer.WriteLine("POST " + urli.AbsoluteUri + " HTTP/1.1");
                writer.WriteLine("Content-Type: multipart/form-data; boundary=--AaB03x");
                writer.WriteLine("Host: " + urli.Host);
                writer.WriteLine("Content-Length: " + (predata.Length + len + postdata.Length).ToString());
                writer.WriteLine();
                //some data for MIME
                writer.Write(utf8.GetString(predata));
                writer.Flush();
                int bytesRead;
                do
                {
                    bytesRead = FS.Read(fileData, 0, MaxContentSize);
                    totalRead += bytesRead;
                    writer.BaseStream.Write(fileData, 0, bytesRead);
                    writer.BaseStream.Flush();
                    sop.prct = (int) ((100*totalRead)/len);
                } while (bytesRead > 0)
                writer.Write(utf8.GetString(postdata));
                writer.Flush();
                //read line of response and do other thigs...
                respStr = reader.ReadLine();
                ...
            }
        }
    }
}

在所有情况下,文件都已成功发送到服务器。 但进度总是这样:从 0 到 100 运行几秒钟,然后等待文件实际上传(大约 5 分钟 - 文件为 400MB)。

所以我认为文件中的数据缓冲在某处,我跟踪的不是上传,而是缓冲数据。然后必须等到上传完毕。

我的问题是:

1) 有没有办法跟踪实际上传数据? Stream.Write() 或 Flush() 方法(正如我在某处读到的,不适用于 NetworkStream)直到从服务器收到 TCP 数据包已收到的确认后才返回。

2) 或者我可以拒绝缓冲(HttpWebRequest 的 AllowWriteStreamBUffering 不起作用)?

3) 进一步“向下”尝试使用 Socket 是否有意义?

更新:

为了避免对 UI 上显示进度的方式产生任何疑问,我重写了代码以记录文件。 所以,这里是代码:

using (var LogStream=File.Open("C:\\123.txt",FileMode.Create,FileAccess.Write,FileShare.Read))
using (var LogWriter=new StreamWriter(LogStream))
using (var FS = File.Open(localFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    long len = FS.Length;
    HttpWebRequest request = (HttpWebRequest) WebRequest.Create(url);
    request.Timeout = 7200000; //2 hour timeout
    request.Method = "POST";
    request.ProtocolVersion = HttpVersion.Version11;
    request.ContentType = "multipart/form-data; boundary=--AaB03x";
    //predata and postdata is two byte[] arrays, that contains
    //strings for MIME file upload (defined above and is not important)
    request.ContentLength = predata.Length + FS.Length + postdata.Length;
    request.AllowWriteStreamBuffering = false;
    LogWriter.WriteLine(DateTime.Now.ToString("o") + " Start write into request stream. ");
    using (var reqStream = request.GetRequestStream())
    {
        reqStream.Write(predata, 0, predata.Length);
        int bytesRead = 0;
        int totalRead = 0;
        do
        {
            bytesRead = FS.Read(fileData, 0, MaxContentSize);
            totalRead += bytesRead;
            reqStream.Write(fileData, 0, bytesRead);
            reqStream.Flush(); //trying with and without this
            //sop.prct = (int) ((100*totalRead)/len); //this part will show progress in percents
            LogWriter.WriteLine(DateTime.Now.ToString("o") + " totalRead= " + totalRead.ToString() + " / " + len.ToString());
        } while (bytesRead > 0);
        reqStream.Write(postdata, 0, postdata.Length);
    }
    LogWriter.WriteLine(DateTime.Now.ToString("o") + " All sent!!! Waiting for responce... ");
    LogWriter.Flush();
    HttpWebResponse responce = (HttpWebResponse) request.GetResponse();
    LogWriter.WriteLine(DateTime.Now.ToString("o") + " Responce received! ");
    using (var respStream = responce.GetResponseStream())
    {
        if (respStream == null) return null;
        using (var streamReader = new StreamReader(respStream))
        {
            string resp = streamReader.ReadToEnd();
            JavaScriptSerializer jssSer = new JavaScriptSerializer();
            return jssSer.Deserialize<UniversalJSONAnswer>(resp);
        }
    }
}

这是结果(我剪掉了中间):

2011-11-19T22:00:54.5964408+04:00 Start write into request stream. 
2011-11-19T22:00:54.6404433+04:00 totalRead= 1048576 / 410746880
2011-11-19T22:00:54.6424434+04:00 totalRead= 2097152 / 410746880
2011-11-19T22:00:54.6434435+04:00 totalRead= 3145728 / 410746880
2011-11-19T22:00:54.6454436+04:00 totalRead= 4194304 / 410746880
2011-11-19T22:00:54.6464437+04:00 totalRead= 5242880 / 410746880
2011-11-19T22:00:54.6494438+04:00 totalRead= 6291456 / 410746880
.......    
2011-11-19T22:00:55.3434835+04:00 totalRead= 408944640 / 410746880
2011-11-19T22:00:55.3434835+04:00 totalRead= 409993216 / 410746880
2011-11-19T22:00:55.3464837+04:00 totalRead= 410746880 / 410746880
2011-11-19T22:00:55.3464837+04:00 totalRead= 410746880 / 410746880
2011-11-19T22:00:55.3464837+04:00 All sent!!! Waiting for responce... 
2011-11-19T22:07:23.0616597+04:00 Responce received! 

如您所见,程序认为它上传了约 2 秒约 400MB。 7 分钟后文件实际上传,我收到回复。

再次更新:

这似乎是在 WINdows 7 下发生的(不是关于 x64 或 x86 的舒尔)。 当我在 XP 下运行我的代码时,一切正常,并且进度显示绝对正确

【问题讨论】:

    标签: c# file-upload progress


    【解决方案1】:

    这个问题困扰了我至少一天。我已经开始使用WebClient.UploadFileAsync,接下来尝试使用ProgressMessageHandler 使用HttpClient,然后使用我自己的HttpContent 使用HttpClient API。这些方法都不起作用(对我来说)。

    出现HttpWebRequest,它位于大多数(全部?).NET Http 抽象的底部,例如WebClientHttpClient,默认缓冲请求和响应流,我通过查看它确认了这一点间谍。

    正如其他人所指出的,您可以使您的请求以一种或另一种方式使用分块编码,这将有效地禁用缓冲请求流,但这仍然不能修复进度报告。

    我发现有必要在我发送的每个块之后刷新请求流,以便准确反映发送进度,否则您的数据将被简单地缓冲到管道下游一步(可能在 NetworkStream 或操作系统中的某个位置,没有检查)。下面的示例代码对我有用,并且在从 HttpWebResponse 转换回 HttpResponseMessage (你可能不需要,YMMV)方面也做了一个简约的工作。

    public async Task<HttpResponseMessage> UploadFileAsync( string uploadUrl, string absoluteFilePath, Action<int> progressPercentCallback )
        {
            var length = new FileInfo( absoluteFilePath ).Length;
    
            var request = new HttpWebRequest( new Uri(uploadUrl) ) {
                Method = "PUT",
                AllowWriteStreamBuffering = false,
                AllowReadStreamBuffering = false,
                ContentLength = length
            };
    
            const int chunkSize = 4096;
            var buffer = new byte[chunkSize];
    
            using (var req = await request.GetRequestStreamAsync())
            using (var readStream = File.OpenRead(absoluteFilePath))
            {
                progressPercentCallback(0);
                int read = 0;
                for (int i = 0; i < length; i += read)
                {
                    read = await readStream.ReadAsync( buffer, 0, chunkSize );
                    await req.WriteAsync( buffer, 0, read );
                    await req.FlushAsync(); // flushing is required or else we jump to 100% very fast
                    progressPercentCallback((int)(100.0 * i / length));
                }
                progressPercentCallback(100);
            }
    
            var response = (HttpWebResponse)await request.GetResponseAsync();
            var result = new HttpResponseMessage( response.StatusCode );
            result.Content = new StreamContent( response.GetResponseStream() );
    
            return result; 
        }
    

    【讨论】:

      【解决方案2】:

      我遇到了同样的问题。我花了很多时间,解决了如下问题: 防病毒 AVAST。当我关闭它时,我的程序运行良好......

      【讨论】:

        【解决方案3】:

        这个问题发布已经一年多了,但我认为我的帖子可能对某人有用。

        我在显示进度时遇到了同样的问题,它的行为与您描述的完全一样。所以我决定使用正确显示上传进度的HttpClient。然后我遇到了一个有趣的错误——当我让 Fiddler 启动时,HttpClient 开始以意想不到的方式显示其上传进度,比如上面的 WebClient/HttpWebRequest 所以我认为这可能是 WebClient 显示上传进度不正确的问题(我想我有它启动了)。所以我再次尝试使用 WebClient(没有启动类似提琴手的应用程序)并且一切正常,上传进度具有正确的值。我已经在几台装有 win7 和 XP 的 PC 上进行了测试,在所有情况下,进度都显示正确。

        所以,我认为像 Fiddler 这样的程序(可能不仅仅是一个 fiddler)对 WebClient 和其他 .net 类如何显示上传进度有一些影响。

        这个讨论批准了它:

        HttpWebRequest doesn't work except when fiddler is running

        【讨论】:

        • 好的。这似乎是有道理的。我真的可以在打开提琴手的情况下进行测试。我回家再看看,谢谢!
        • 我认为 fiddler 设置了一些系统挂钩,使所有 .net 网络类都能正常工作。
        • 好吧,据我所知,Fiddler 作为 HTTP 代理注册,因此它有自己的缓冲区,甚至在将数据包发送到目标服务器之前,它也会向上传主机确认数据包。在启用 Fiddler 的情况下下载文件时会发生相反的情况(进度长时间保持在 0%,然后随着下载的文件从代理流式传输到发起请求的主机,则快速跳转到 100%)。
        【解决方案4】:

        我的建议是使用新的 HTTPClient 类(在 .NET 4.5 中可用)。它支持进步。

        这篇文章对我帮助很大: http://www.strathweb.com/2012/06/drag-and-drop-files-to-wpf-application-and-asynchronously-upload-to-asp-net-web-api/

        我的上传文件代码:

            private void HttpSendProgress(object sender, HttpProgressEventArgs e)
            {
                HttpRequestMessage request = sender as HttpRequestMessage;
                Console.WriteLine(e.BytesTransferred);
            }
        
            private void Window_Loaded_1(object sender, RoutedEventArgs e)
            {
                ProgressMessageHandler progress = new ProgressMessageHandler();
                progress.HttpSendProgress += new EventHandler<HttpProgressEventArgs>(HttpSendProgress);
        
                HttpRequestMessage message = new HttpRequestMessage();
                StreamContent streamContent = new StreamContent(new FileStream("e:\\somefile.zip", FileMode.Open));
        
                message.Method = HttpMethod.Put;
                message.Content = streamContent;
                message.RequestUri = new Uri("{Here your link}");
        
                var client = HttpClientFactory.Create(progress);
        
                client.SendAsync(message).ContinueWith(task =>
                {
                    if (task.Result.IsSuccessStatusCode)
                    { 
        
                    }
                });
            }
        

        【讨论】:

          【解决方案5】:

          您可以使用WebClientUploadFile 来上传文件,而不是使用写入文件作为文件流。为了跟踪接收和上传的数据的百分比,您可以使用UploadFileAsyn 并订阅其事件。

          在下面的代码中,我使用 UploadFileAsyn 来同步上传文件,但只要你不释放上传器的实例,它就不需要同步。

          class FileUploader : IDisposable
          {
              private readonly WebClient _client;
              private readonly Uri _address;
              private readonly string _filePath;
              private bool _uploadCompleted;
              private bool _uploadStarted;
              private bool _status;
          
              public FileUploader(string address, string filePath)
              {
                  _client = new WebClient();
                  _address = new Uri(address);
                  _filePath = filePath;
                  _client.UploadProgressChanged += FileUploadProgressChanged;
                  _client.UploadFileCompleted += FileUploadFileCompleted;
              }
          
              private void FileUploadFileCompleted(object sender, UploadFileCompletedEventArgs e)
              {
                  _status = (e.Cancelled || e.Error == null) ? false : true;
                  _uploadCompleted = true;
              }
          
              private void FileUploadProgressChanged(object sender, UploadProgressChangedEventArgs e)
              {
                  if(e.ProgressPercentage % 10 == 0)
                  {
                      //This writes the pecentage data uploaded and downloaded
                      Console.WriteLine("Send: {0}, Received: {1}", e.BytesSent, e.BytesReceived);
                      //You can have a delegate or a call back to update your UI about the percentage uploaded
                      //If you don't have the condition (i.e e.ProgressPercentage % 10 == 0 )for the pecentage of the process 
                      //the callback will slow you upload process down
                  }
              }
          
              public bool Upload()
              {
          
                  if (!_uploadStarted)
                  {
                      _uploadStarted = true;
                      _client.UploadFileAsync(_address, _filePath);
                  }
                  while (!_uploadCompleted)
                  {
                      Thread.Sleep(1000);
                  }
                  return _status;
              }
          
              public void Dispose()
              {
                  _client.Dispose();
              }
          }
          

          客户代码:

                      using (FileUploader uploader = new FileUploader("http://www.google.com", @"C:\test.txt"))
                  {
                      uploader.Upload();
                  }
          

          您可以在FileUploadProgressChanged 事件处理程序上注册自定义回调(可能是委托)以更新您的 WPF UI。

          如果您的事件回调执行任何 IO,则上传进度更改事件会被更频繁地调用,这会减慢下载进度。最好不经常更新,例如以下代码仅更新了 10%。

              private int _percentageDownloaded;
          
              private void FileUploadProgressChanged(object sender, UploadProgressChangedEventArgs e)
              {
                  if (e.ProgressPercentage % 10 == 0 && e.ProgressPercentage > _percentageDownloaded)
                  {
          
                      _percentageDownloaded = e.ProgressPercentage;
                      //Any callback instead of printline
                      Console.WriteLine("Send: {0} Received: {1}", e.BytesSent, e.BytesReceived);
                  }
              }
          

          【讨论】:

          • 感谢您的关注,我明天试试您的代码。但在我看来,与我上面描述的方法 2) 相比,不会有任何区别。
          • 是的,看看我最近所做的更改。代码的底部。限制进度更改的回调将提高性能(当您的回调有一些与 IO 相关的内容时)。
          • 问题依然存在:我尝试上传400MB文件时,进度条在10秒内从0变为50%(不能这么快上传),然后卡住5分钟然后(实际上传文件时)进度在不到一秒的时间内从 50 变为 100。
          • 那么问题肯定出在接收端(你上传文件的服务器/服务)
          【解决方案6】:

          在第一个示例中,我认为您的进度条显示的是您从磁盘上的文件写入流的速度 - 而不是实际的上传进度(这就是为什么这一切都以 100% 的速度发生,然后上传就开始了* )。

          我可能是错的 ^^ 并且没有 WPF 经验,但我已将大量文件从 Silverlight 上传到 WCF,并且使用的模型(如您所做的那样)将文件分解为块。发送每个块。当您收到来自服务器的响应(“块 26 收到 ok”)时,更新进度条,除非您 / 知道 / 块 x 成功了,否则您不能(或不应该)更新进度条 - 并且知道这一点的好方法是如果服务器说它得到它。

          *我希望我能在 5 分钟内上传 400Mb。会带我一整天...

          【讨论】:

          • 不要关注WPF,它只负责接口。我还认为它显示了写入缓冲区的进度,然后将数据发送到网络级别的服务器。因此,我需要一些仅在部分数据实际发送到服务器或在发送数据之前不返回的方法时触发的事件。不幸的是,服务器只接受一个大的 http 请求,我无法在 http 级别分解文件
          【解决方案7】:

          快速猜测,您正在 UI 线程上运行此代码。您需要在新线程上运行上传内容。 那时你有2个选择。 1)您在 UI 线程上运行计时器并更新 UI。 2)您使用 Invoke(因为您无法从另一个线程访问 UI)调用更新 UI 来更新 UI。

          【讨论】:

          • 不,所有这些东西都在自己的线程上运行。当我更改 sop.prct 属性时,它是 WPF 绑定和 UI 更新
          • 我以不同的方式观察了进度,包括在调试模式下的逐步操作。例如,使用 TcpClient 时 99% 的时间得到这一行:'respStr = reader.ReadLine();'而其他一切都在不到一分钟的时间内运行
          • 我看不到(我是盲人或在其他地方定义的)fileData byte[] 数组有多大。无论如何,它不应超过 16 - 256kb。
          • 另外,您可能无法从工作人员那里访问 UI。欲了解更多信息,请参阅:msdn.microsoft.com/en-us/magazine/cc163328.aspx
          • 是的,fileData 定义在其他地方,长度为 1MB。我只是尝试了较小的尺寸,但没有帮助。在 UI 中显示进度一切都很好。我实际上可以看到进度条如何从 0% 变为 100%。我的问题是上面的大部分代码都执行了几秒钟,无论文件大小如何,然后挂起直到上传结束等待响应。而且我实际上无法理解服务器何时收到部分数据
          猜你喜欢
          • 1970-01-01
          • 2014-06-03
          • 2013-05-19
          • 2016-06-27
          • 1970-01-01
          • 1970-01-01
          • 2017-07-03
          • 2023-03-31
          相关资源
          最近更新 更多