上传HTTP进度跟踪

16

我正在编写一个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) 进一步"向下"尝试使用套接字是否有意义?
更新: 为避免在 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下运行代码时,一切都完美地工作,进度条也显示得非常正确。

7个回答

4

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

我也遇到了显示进度的同样问题,它的表现和你所描述的完全相同。因此,我决定使用HttpClient,它可以正确地显示上传进度。然后我遇到了一个有趣的bug——当我启动了Fiddler时,HttpClient开始以意外的方式显示其上传进度,就像WebClient / HttpWebRequest上面那样,所以我想可能是WebClient显示上传进度不正确的问题(我想我当时它是在运行的)。因此,我再次尝试了WebClient(没有启动类似Fiddler的应用程序),所有内容都正常工作,上传进度具有正确的值。我已在几台win7和XP的PC上进行了测试,在所有情况下,进度都正确显示。

因此,我认为像Fiddler这样的程序(可能不仅仅是fiddler)会影响WebClient和其他.NET类显示上传进度的方式。

这个讨论证实了这一点:

除非运行fiddler,否则HttpWebRequest无法正常工作


好的。这似乎是可行的。我可以在打开Fiddler的情况下进行测试。我回家后再检查一下,谢谢! - Lumen
我认为Fiddler设置了一些系统钩子,使得所有的.NET网络类都不能正常工作。 - Vlad
据我所知,Fiddler被列为一个HTTP代理,因此它将拥有自己的缓冲区并确认上传主机的数据包,甚至在它们被发送到目标服务器之前。当启用Fiddler下载文件时会出现相反的情况(进度条长时间停留在0%,然后在从代理流式传输下载文件到发起请求的主机时快速跳到100%)。 - Johannes Rudolph

3
您可以使用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)相比,似乎不会有任何区别。 - Lumen
是的,看看我最近做的更改。代码底部。限制回调函数在进度更改时会增加性能(当您的回调函数有一些IO相关的东西时)。 - Joseph Jeganathan
问题仍然存在:当我尝试上传一个 400MB 的文件时,进度条在 10 秒内从 0% 变为 50%(这么快肯定不可能上传完),然后它会冻结 5 分钟,最终在实际完成上传时,进度条在不到一秒的时间内从 50% 变为 100%。 - Lumen
那么问题必须出在接收端(您要上传文件的服务器/服务)了。 - Joseph Jeganathan

1
这个问题困扰我至少一天了。我开始使用WebClient.UploadFileAsync,然后尝试了HttpClientProgressMessageHandler,接着为HttpClient API编写了自己的HttpContent。但是这些方法都没有奏效(对我而言)。
看起来HttpWebRequest在大多数(或全部?).NET Http抽象中位于底部,如WebClientHttpClient,默认情况下缓冲请求和响应流,我通过在ILSpy中查看确认了这一点。
正如其他人指出的那样,你可以让你的请求使用分块编码的方式之一,这将有效地禁用请求流的缓冲,但这仍然无法解决进度报告的问题。
我发现必须在发送每个块后刷新请求流,以准确反映发送进度,否则你的数据将只是被缓冲到管道中的下一个步骤(可能在NetworkStream或操作系统中,我没有检查)。下面的示例代码适用于我,还可以将HttpWebResponse最小化地转换回HttpResponseMessage(你可能不需要,具体情况因人而异)。
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; 
    }

1

我的建议是使用新的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)
            { 

            }
        });
    }

0

我曾经遇到过同样的问题。花费了很多时间,最终通过以下方法解决了这个问题: 杀毒软件AVAST。当我关闭它时,我的程序可以完美地运行...


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

不,所有这些东西都在自己的线程上运行。这是WPF绑定和UI更新,当我更改sop.prct属性时。 - Lumen
我以不同的方式观察进度,包括在调试模式下逐步进行。例如,使用TcpClient时,99%的时间会得到这行代码:'respStr = reader.ReadLine();',而其他所有内容都在一分钟内运行完成。 - Lumen
我看不到(可能是因为我瞎了或者这个信息在其他地方定义了)fileData byte[]数组的大小。无论如何,它的大小都不应该超过16-256kb。 - Ivar
另外,您可能无法从工作线程访问UI。有关更多信息,请参见:http://msdn.microsoft.com/en-us/magazine/cc163328.aspx - Ivar
是的,fileData在其他地方被定义,并且它的长度为1MB。我尝试了更小的大小,但没有帮助。在UI中显示进度一切正常。实际上,我可以看到进度条从0%到100%的过程。我的问题是,无论文件大小如何,大部分以上代码都会在几秒钟内执行,然后挂起,直到上传完成并等待响应。而我实际上无法理解服务器何时收到数据的一部分。 - Lumen
显示剩余3条评论

0
在第一个示例中,我认为你的进度条显示的是从磁盘文件流中写入的速度,而不是实际的上传进度(这就是为什么它所有的进度都会在很快的情况下到达100%,然后上传会继续进行 *)。
我可能错了 ^^,而且没有 WPF 经验,但我已经从 Silverlight上传大文件到WCF,那里使用的模型是(和你一样)将文件分成块。发送每个块。当您从服务器收到响应(“块26接收正常”)时,更新进度条,因为实际上,除非您知道某个块被传输成功,否则您不能(或不应该)更新进度条-并且如果服务器说已经收到,则知道这一点的好方法是它。
*我希望我能在5分钟内上传400Mb。这需要我一整天...

请不要关注WPF,它只负责界面。我认为它显示了将写入缓冲区的进度,然后在网络层将数据发送到服务器。因此,我需要一个只有在部分数据实际发送到服务器时才触发的事件或者直到数据被发送后才返回的方法。不幸的是,服务器只接受单个大型HTTP请求,我无法在HTTP级别上拆分文件。 - Lumen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接