You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

WPF Task下载文件:网络中断异常未触发及续传方法咨询

WPF大文件下载:异常捕获与续传实现方案

嘿,这个问题我之前做WPF大文件下载时也踩过坑,咱们一步步拆解解决:

一、为啥网络断开时没触发异常?

你遇到的情况其实挺典型的:很多网络流在连接意外断开时,不会直接抛出IOException,而是让strm.Read()返回0(表示流已结束)。如果你的代码只是判断“读取到字节数>0才继续”,就会直接退出任务,完全意识不到这是异常情况。

另外还有一种可能:你没正确处理Task里的异常。如果Task.Run中的代码抛出了异常,但你既没await这个Task,也没通过Task.Exception去捕获,那这个异常就会被“静默吞掉”,看起来就像代码直接退出了。

修复异常检测的代码示例

假设你原来的下载代码大概是这样:

await Task.Run(() =>
{
    using var client = new HttpClient();
    using var response = client.GetStreamAsync(url).Result;
    using var fileStream = new FileStream(localPath, FileMode.Create);
    var buffer = new byte[8192];
    int bytesRead;
    long totalBytes = response.Length;
    long downloadedBytes = 0;
    while ((bytesRead = response.Read(buffer, 0, buffer.Length)) > 0)
    {
        fileStream.Write(buffer, 0, bytesRead);
        downloadedBytes += bytesRead;
        progress.Report((float)downloadedBytes / totalBytes * 100);
    }
});

那你需要做两个关键调整:

  1. 先确保HTTP请求本身成功,并且能获取到文件总大小;
  2. bytesRead == 0但下载进度还没到100%时,手动抛出异常——这说明流提前结束了,肯定是网络出问题了:
await Task.Run(async () =>
{
    using var client = new HttpClient();
    // 先获取响应头,再获取流,这样能提前拿到文件大小
    using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
    response.EnsureSuccessStatusCode(); // 确保请求本身是成功的(比如不是404、500)
    
    long totalBytes = response.Content.Headers.ContentLength ?? -1;
    long downloadedBytes = 0;
    var buffer = new byte[8192];
    
    using var responseStream = await response.Content.ReadAsStreamAsync();
    using var fileStream = new FileStream(localPath, FileMode.Create);
    
    int bytesRead;
    while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
    {
        await fileStream.WriteAsync(buffer, 0, bytesRead);
        downloadedBytes += bytesRead;
        
        if (totalBytes > 0)
        {
            progress.Report((float)downloadedBytes / totalBytes * 100);
        }
    }
    
    // 这里是关键:如果没下载完就返回0,说明异常中断了
    if (totalBytes > 0 && downloadedBytes < totalBytes)
    {
        throw new IOException("网络连接意外中断,下载未完成");
    }
});

另外,一定要在调用的地方await这个Task,并加上try-catch,这样异常才能被捕获到:

try
{
    await DownloadLargeFile(url, localPath, progress);
}
catch (Exception ex)
{
    // 这里给用户提示,比如弹个MessageBox
    MessageBox.Show($"下载出错啦:{ex.Message}");
}

二、如何实现网络恢复后的续传?

续传的核心是利用HTTP的Range请求头——简单说就是告诉服务器:“我已经下载了X字节,你从X字节之后开始发就行”。具体步骤如下:

1. 记录已下载的字节数

先检查本地文件是否存在,存在的话就获取它的大小,这就是已经下载的字节数:

long startByte = 0;
if (File.Exists(localPath))
{
    startByte = new FileInfo(localPath).Length;
}

2. 发送Range请求

给HttpClient的请求添加Range头,指定从startByte开始下载:

using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, url);
// 设置Range:从startByte开始,到文件末尾(null表示末尾)
request.Headers.Range = new RangeHeaderValue(startByte, null);

using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

这里要注意:不是所有服务器都支持Range请求。如果服务器支持,会返回206 Partial Content状态码;如果不支持,会返回200 OK并发送整个文件,这时候你得考虑是覆盖本地文件还是提示用户无法续传。

3. 追加写入本地文件

打开文件时用FileMode.Append,把新下载的字节追加到文件末尾,而不是覆盖:

using var fileStream = new FileStream(localPath, FileMode.Append, FileAccess.Write, FileShare.None);

完整的续传代码示例

public async Task ResumeDownloadAsync(string url, string localPath, IProgress<float> progress, CancellationToken cancellationToken = default)
{
    long startByte = 0;
    long totalBytes = 0;
    
    // 获取已下载的字节数
    if (File.Exists(localPath))
    {
        startByte = new FileInfo(localPath).Length;
    }
    
    using var client = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Get, url);
    
    // 如果已经下载过一部分,就发送Range请求
    if (startByte > 0)
    {
        request.Headers.Range = new RangeHeaderValue(startByte, null);
    }
    
    using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
    response.EnsureSuccessStatusCode();
    
    // 获取文件总大小:续传时,服务器会在Content-Range头里返回完整文件大小
    if (response.StatusCode == HttpStatusCode.PartialContent)
    {
        totalBytes = response.Content.Headers.ContentRange.Length ?? -1;
    }
    else
    {
        totalBytes = response.Content.Headers.ContentLength ?? -1;
    }
    
    // 如果拿不到总大小,就没法计算进度和判断是否下载完成
    if (totalBytes == -1)
    {
        throw new InvalidOperationException("无法获取文件总大小,无法支持续传");
    }
    
    var buffer = new byte[8192];
    long downloadedBytes = startByte;
    
    using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
    using var fileStream = new FileStream(localPath, FileMode.Append, FileAccess.Write, FileShare.None);
    
    int bytesRead;
    while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
    {
        await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
        downloadedBytes += bytesRead;
        
        // 更新进度
        progress.Report((float)downloadedBytes / totalBytes * 100);
        
        // 检查是否被取消
        cancellationToken.ThrowIfCancellationRequested();
    }
    
    // 最后检查是否真的下载完成
    if (downloadedBytes < totalBytes)
    {
        throw new IOException("下载中断,未完成");
    }
}

额外小贴士

  • 处理服务器不支持Range的情况:如果发送Range请求后返回200 OK,说明服务器不支持续传,你可以提示用户,或者直接重新下载整个文件;
  • 加入CancellationToken,方便用户随时取消下载;
  • 可以把已下载的字节数和文件URL存在本地配置里,避免每次都读取大文件的长度(毕竟大文件读长度也有点耗时);
  • 网络恢复后,只要再次调用这个续传方法,它会自动检测已下载的部分,继续剩下的下载。

内容的提问来源于stack exchange,提问作者Saimon Frost

火山引擎 最新活动