后台传输

使用后台传输 API 通过网络可靠地复制文件。 后台传输 API 提供应用暂停期间在后台运行的高级上载和下载功能,并持续至应用终止。 API 监视网络状态,并在连接丢失时自动暂停和恢复传输。传输还具备数据感知和电池感知能力,这意味着下载活动会根据当前的连接状态和设备电池状态进行调整。 API 非常适合使用 HTTP(S)上传和下载大型文件。 FTP 也受支持,但仅适用于下载。

注释

Windows.Networking.BackgroundTransfer API 是可在 WinUI 3(Windows 应用 SDK) 桌面应用和 UWP 应用中工作的Windows 运行时 (WinRT) API。 后台传输需要包标识;未打包的应用不能使用此 API。

后台传输独立于调用应用运行,主要用于视频、音乐和大型图像等资源的长期传输操作。 对于这些方案,使用后台传输至关重要,因为即使应用暂停,下载也会继续进行。

如果要下载可能很快完成的小型资源,则应使用 HttpClient API 而不是后台传输。

使用 Windows.Networking.BackgroundTransfer

后台传输功能的工作原理是什么?

当应用使用后台传输启动传输时,将使用 BackgroundDownloaderBackgroundUploader 类对象配置和初始化请求。 每个传输操作由系统单独处理,与调用应用分开。 如果你想在应用的 UI 中向用户显示状态信息,则可以使用进度信息;此外,应用还可以在传输过程中暂停、继续、取消,甚至从数据中读取内容。 系统处理传输的方式促进智能电源使用,并防止连接应用遇到应用挂起、终止或突然网络状态更改等事件时可能出现的问题。

注释

由于单个应用的资源限制,应用在任意时刻的传输操作总数(DownloadOperations + UploadOperations)不应超过 200 个。 超过该限制可能会使应用的传输队列处于不可恢复状态。

启动应用程序时,它必须对所有现有的 DownloadOperationUploadOperation 对象调用 AttachAsync。 不这样做将导致已完成的传输泄漏,最终会使你使用后台传输功能无用。

使用后台传输执行经过身份验证的文件请求

后台传输提供了一些方法,用于支持基本服务器和代理身份验证凭据、Cookie,以及为每次传输操作使用自定义 HTTP 标头(通过 SetRequestHeader)。

此功能如何适应网络状态更改或意外关闭?

当发生网络状态更改时,后台传输功能通过智能地利用连接和运营商数据计划状态信息(由 连接 功能提供)来维护每个传输操作的一致体验。 若要为不同的网络方案定义行为,应用使用 BackgroundTransferCostPolicy 定义的值为每个操作设置成本策略。

例如,为操作定义的成本策略可以指示当设备使用按流量计费的网络时应自动暂停该操作。 然后,建立与“无限制”网络的连接时,会自动恢复传输(或重启)。 有关如何按成本定义网络的详细信息,请参阅 NetworkCostType

虽然后台传输功能具有自己的用于处理网络状态更改的机制,但网络连接应用还有其他常规连接注意事项。 使用 Windows.Networking.Connectivity API 监视连接状态和费用信息。

注释

对于在移动设备上运行的应用,有一些功能允许用户监视和限制根据连接类型、漫游状态和用户数据计划传输的数据量。 因此,即使 BackgroundTransferCostPolicy 指示应继续传输,后台传输在手机上也可能会被暂停。

下表显示,在给定手机当前状态的情况下,每个 BackgroundTransferCostPolicy 值何时允许在手机上进行后台传输。 可以使用 ConnectionCost 类来确定手机的当前状态。

设备状态 UnrestrictedOnly 默认 始终
已连接到 Wi‑Fi 允许 允许 允许
按流量计费连接,非漫游,未超出数据限制,预计将保持在限制内 Deny 允许 允许
按流量计费的连接、未漫游、受数据限制、计划超出限制 Deny Deny 允许
按流量计费的连接、漫游、受数据限制 Deny Deny 允许
按流量计费的连接、不受数据限制。 仅当用户启用“限制数据感知 UI 中的后台数据”时,才会发生此状态。 Deny Deny Deny

上传文件

使用后台传输时,上传作为 UploadOperation 存在,该上传公开了许多用于重启或取消操作的控制方法。 应用事件(例如挂起或终止)和连接状态变化都会由系统按 UploadOperation 自动处理;上传会在应用挂起期间继续进行,或者在应用终止后暂停并保留其状态。 此外,设置 CostPolicy 属性将指示应用在用于 Internet 连接的按流量计费的网络时是否开始上传。

以下示例将指导你完成基本上传的创建和初始化,以及如何枚举和重新引入以前应用会话中保持的操作。

上传单个文件

创建上传从 BackgroundUploader 开始。 该类用于提供使应用能够在创建结果 UploadOperation 之前配置上传的方法。 以下示例演示如何使用所需的 UriStorageFile 对象执行此操作。

标识上传的文件和目标

在开始创建 UploadOperation 之前,首先需要标识要上传到的位置的 URI,以及要上传的文件。 在以下示例中,uriString 值使用来自 UI 输入的字符串填充,而 file 值则使用 PickSingleFileAsync 操作返回的 StorageFile 对象填充。

function uploadFile() {
    var filePicker = new Windows.Storage.Pickers.FileOpenPicker();
    filePicker.fileTypeFilter.replaceAll(["*"]);

    filePicker.pickSingleFileAsync().then(function (file) {
        if (!file) {
            printLog("No file selected");
            return;
        }

        var upload = new UploadOp();
        var uriString = document.getElementById("serverAddressField").value;
        upload.start(uriString, file);

        // Store the upload operation in the uploadOps array.
        uploadOperations.push(upload);
    });
}

创建和初始化上传操作

在上一步中, uriString文件 值将传递给下一个示例 UploadOp 的实例,其中用于配置和启动新的上传操作。 首先,将分析 uriString 以创建所需的 Uri 对象。

接下来,BackgroundUploader 使用提供的 StorageFile文件)的属性填充请求标头,并使用 StorageFile 对象设置 SourceFile 属性。 然后调用 SetRequestHeader 方法以插入文件名(作为字符串提供)和 StorageFile.Name 属性。

最后, BackgroundUploader 创建 UploadOperationupload)。

function UploadOp() {
    var upload = null;
    var promise = null;

    this.start = function (uriString, file) {
        try {
        
            var uri = new Windows.Foundation.Uri(uriString);
            var uploader = new Windows.Networking.BackgroundTransfer.BackgroundUploader();

            // Set a header, so the server can save the file (this is specific to the sample server).
            uploader.setRequestHeader("Filename", file.name);

            // Create a new upload operation.
            upload = uploader.createUpload(uri, file);

            // Start the upload and persist the promise to be able to cancel the upload.
            promise = upload.startAsync().then(complete, error, progress);
        } catch (err) {
            displayError(err);
        }
    };
    // On application activation, reassign callbacks for a upload
    // operation persisted from previous application state.
    this.load = function (loadedUpload) {
        try {
            upload = loadedUpload;
            promise = upload.attachAsync().then(complete, error, progress);
        } catch (err) {
            displayError(err);
        }
    };
}

请注意使用 JavaScript 承诺定义的异步方法调用。 来看一下上一个示例中的一行:

promise = upload.startAsync().then(complete, error, progress);

异步方法调用后面跟着一个 then 语句,该语句表明,当异步方法调用返回结果时,会调用由应用程序定义的方法。 有关此编程模式的详细信息,请参阅 使用 promise 的 JavaScript 中的异步编程

上传多个文件

标识上传的文件和目标

在使用单个 UploadOperation 传输多个文件的场景中,该过程像往常一样开始:首先提供所需的目标 URI 和本地文件信息。 与上一部分中的示例类似,URI 由最终用户以字符串形式提供,也可以使用 FileOpenPicker 通过用户界面指定文件。 但是,在此方案中,应用应改为调用 PickMultipleFilesAsync 方法,以便通过 UI 选择多个文件。

function uploadFiles() {
       var filePicker = new Windows.Storage.Pickers.FileOpenPicker();
       filePicker.fileTypeFilter.replaceAll(["*"]);

       filePicker.pickMultipleFilesAsync().then(function (files) {
          if (files === 0) {
             printLog("No file selected");
                return;
          }

          var upload = new UploadOperation();
          var uriString = document.getElementById("serverAddressField").value;
          upload.startMultipart(uriString, files);

          // Persist the upload operation in the global array.
          uploadOperations.push(upload);
       });
    }

为提供的参数创建对象

接下来的两个示例使用在上一步结束时调用的单个示例方法 startMultipart 中包含的代码。 为便于说明,用于创建 BackgroundTransferContentPart 对象数组的方法中的代码,已从用于创建最终生成的 UploadOperation 的代码中拆分出来。

首先,用户提供的 URI 字符串将初始化为 Uri。 接下来,传递给此方法的 IStorageFile 对象(files)数组被循环访问,每个对象用于创建新的 BackgroundTransferContentPart 对象,然后放置在 contentParts 数组中。

    upload.startMultipart = function (uriString, files) {
        try {
            var uri = new Windows.Foundation.Uri(uriString);
            var uploader = new Windows.Networking.BackgroundTransfer.BackgroundUploader();

            var contentParts = [];
            files.forEach(function (file, index) {
                var part = new Windows.Networking.BackgroundTransfer.BackgroundTransferContentPart("File" + index, file.name);
                part.setFile(file);
                contentParts.push(part);
            });

创建和初始化多部分上传操作

当我们用表示每个待上传 IStorageFile 的所有 BackgroundTransferContentPart 对象填充好 contentParts 数组后,就可以使用 Uri 调用 CreateUploadAsync,以指明请求将发送到何处。

        // Create a new upload operation.
            uploader.createUploadAsync(uri, contentParts).then(function (uploadOperation) {

               // Start the upload and persist the promise to be able to cancel the upload.
               upload = uploadOperation;
               promise = uploadOperation.startAsync().then(complete, error, progress);
            });

         } catch (err) {
             displayError(err);
         }
     };

重启中断的上传操作

UploadOperation 完成或被取消后,所有相关的系统资源都会被释放。 但是,如果应用在发生上述任一操作之前终止,则会暂停任何活动操作,并且与每个操作关联的资源仍被占用。 如果未枚举这些操作并重新引入到下一个应用会话,它们将不会完成,并且将继续占用设备资源。

  1. 在定义枚举持久操作的函数之前,我们需要创建一个数组,其中包含它将返回的 UploadOperation 对象:

    var uploadOperations = [];
    
  2. 接下来,定义枚举持久化操作并将其存储在数组中的函数。 请注意,用于将回调重新分配给 UploadOperationload 方法(如果它在应用终止后仍被保留)是在我们稍后将在本节中定义的 UploadOp 类中。

    function Windows.Networking.BackgroundTransfer.BackgroundUploader.getCurrentUploadsAsync() {
        .then(function (uploads) {
            for (var i = 0; i < uploads.size; i++) {
                var upload = new UploadOp();
                upload.load(uploads[i]);
                uploadOperations.push(upload);
            }
        }
    };
    

下载文件

使用后台传输时,每个下载都作为 DownloadOperation 存在,该下载公开了许多用于暂停、恢复、重启和取消操作的控制方法。 系统根据 DownloadOperation 自动处理应用事件(例如暂停或终止)和连接更改;在应用挂起期间或暂停时,下载会继续运行,并且在应用终止后,仍然保持运行。 对于移动网络场景,设置 CostPolicy 属性将指示你的应用在使用按流量计费的网络进行 Internet 连接时,是否会开始或继续下载。

如果要下载可能很快完成的小型资源,则应使用 HttpClient API 而不是后台传输。

以下示例将引导你完成基本下载的创建和初始化,以及如何枚举和重新引入从上一个应用会话中保留的操作。

配置并启动后台传输文件下载任务

以下示例演示如何使用表示 URI 和文件名的字符串来创建 URI 对象和将包含所请求文件的 StorageFile 。 在此示例中,新文件会自动放置在预定义的位置。 或者,可以使用 FileSavePicker 允许用户指示在设备上保存文件的位置。 请注意,用于将回调重新分配给 DownloadOperationload 方法(如果该对象在应用终止后仍被保留,则会调用该方法)位于本节后面定义的 DownloadOp 类中。

function DownloadOp() {
    var download = null;
    var promise = null;
    var imageStream = null;

    this.start = function (uriString, fileName) {
        try {
            // Asynchronously create the file in the pictures folder.
            Windows.Storage.KnownFolders.picturesLibrary.createFileAsync(fileName, Windows.Storage.CreationCollisionOption.generateUniqueName).done(function (newFile) {
                var uri = Windows.Foundation.Uri(uriString);
                var downloader = new Windows.Networking.BackgroundTransfer.BackgroundDownloader();

                // Create a new download operation.
                download = downloader.createDownload(uri, newFile);

                // Start the download and persist the promise to be able to cancel the download.
                promise = download.startAsync().then(complete, error, progress);
            }, error);
        } catch (err) {
            displayException(err);
        }
    };
    // On application activation, reassign callbacks for a download
    // operation persisted from previous application state.
    this.load = function (loadedDownload) {
        try {
            download = loadedDownload;
            printLog("Found download: " + download.guid + " from previous application run.<br\>");
            promise = download.attachAsync().then(complete, error, progress);
        } catch (err) {
            displayException(err);
        }
    };
}

请注意使用 JavaScript 承诺定义的异步方法调用。 查看上一代码示例中的第 17 行:

promise = download.startAsync().then(complete, error, progress);

异步方法调用后面跟着一个 then 语句,它指定了由应用定义、在异步方法调用返回结果时会被调用的方法。 有关此编程模式的详细信息,请参阅 使用 promise 的 JavaScript 中的异步编程

添加其他操作控制方法

可以通过实现其他 DownloadOperation 方法来提高控制级别。 例如,将以下代码添加到上面的示例将引入取消下载的功能。

// Cancel download.
this.cancel = function () {
    try {
        if (promise) {
            promise.cancel();
            promise = null;
            printLog("Canceling download: " + download.guid + "<br\>");
            if (imageStream) {
                imageStream.close();
            }
        }
        else {
            printLog("Download " + download.guid + " already canceled.<br\>");
        }
    } catch (err) {
        displayException(err);
    }
};

在启动时枚举已持久化的操作

完成或取消 DownloadOperation 后,将释放任何关联的系统资源。 但是,如果应用在发生上述任一事件之前终止,下载将暂停并保留在后台。 以下示例演示如何将持久下载重新引入到新的应用会话中。

  1. 在定义枚举持久操作的函数之前,我们需要创建一个数组,其中包含它将返回的 DownloadOperation 对象:

    var downloadOps = [];
    
  2. 接下来,定义枚举持久化操作并将其存储在数组中的函数。 请注意,调用用于重新分配持久 DownloadOperation 回调的加载方法位于我们稍后在本部分中定义的 DownloadOp 示例中。

    // Enumerate outstanding downloads.
    Windows.Networking.BackgroundTransfer.BackgroundDownloader.getCurrentDownloadsAsync().done(function (downloads) {
    
        for (var i = 0; i < downloads.size; i++) {
            var download = new DownloadOp();
            download.load(downloads[i]);
            downloadOps.push(download);
        }
    });
    
  3. 现在可以使用填充的列表重新启动挂起的操作。

后处理

Windows 10中的新功能是在后台传输完成后运行应用程序代码的功能,即使应用未运行也是如此。 例如,应用可能需要在电影下载完毕后更新可用电影列表,而不是每次启动应用扫描新电影。 或者你的应用可能想要使用其他服务器或端口重试处理失败的文件传输。 对成功和失败的传输调用后处理,因此可以使用它来实现自定义错误处理和重试逻辑。

后处理使用现有的后台任务基础结构。 创建一个后台任务,并在开始传输之前将其与传输相关联。 然后,这些传输会在后台执行;完成后,系统会调用你的后台任务来执行后处理。

后期处理使用新的类 BackgroundTransferCompletionGroup。 此类类似于现有的 BackgroundTransferGroup ,因为它允许将后台传输组合在一起,但 BackgroundTransferCompletionGroup 增加了指定在传输完成后要运行的后台任务的功能。

按如下方式发起带后处理的后台传输。

  1. 创建 BackgroundTransferCompletionGroup 对象。 然后,创建 BackgroundTaskBuilder 对象。 将生成器对象的 Trigger 属性设置为完成组对象,并将生成器的 TaskEntryPoint 属性设置为应在传输完成时执行的后台任务的入口点。 最后,调用 BackgroundTaskBuilder.Register 方法注册后台任务。 请注意,许多完成组可以共享一个后台任务入口点,但每个后台任务注册只能有一个完成组。
var completionGroup = new BackgroundTransferCompletionGroup();
BackgroundTaskBuilder builder = new BackgroundTaskBuilder();

builder.Name = "MyDownloadProcessingTask";
builder.SetTrigger(completionGroup.Trigger);
builder.TaskEntryPoint = "Tasks.BackgroundDownloadProcessingTask";

BackgroundTaskRegistration downloadProcessingTask = builder.Register();
  1. 接下来,将后台传输与完成组关联。 创建完所有传输后,启用完成组。
BackgroundDownloader downloader = new BackgroundDownloader(completionGroup);
DownloadOperation download = downloader.CreateDownload(uri, file);
Task<DownloadOperation> startTask = download.StartAsync().AsTask();

// App still sees the normal completion path
startTask.ContinueWith(ForegroundCompletionHandler);

// Do not enable the CompletionGroup until after all downloads are created.
downloader.CompletionGroup.Enable();
  1. 后台任务中的代码从触发器详细信息中提取操作列表,然后代码可以检查每个操作的详细信息,并为每个操作执行适当的后处理。
public class BackgroundDownloadProcessingTask : IBackgroundTask
{
    public async void Run(IBackgroundTaskInstance taskInstance)
    {
    var details = (BackgroundTransferCompletionGroupTriggerDetails)taskInstance.TriggerDetails;
    IReadOnlyList<DownloadOperation> downloads = details.Downloads;

    // Do post-processing on each finished operation in the list of downloads
    }
}

后处理任务是常规后台任务。 它属于所有后台任务池的一部分,并遵循与所有后台任务相同的资源管理策略。

此外请注意,后处理不会代替前台完成处理程序。 如果应用定义了前台完成处理程序,并且应用在文件传输完成时正在运行,则将同时调用前台完成处理程序和后台完成处理程序。 前台任务和后台任务的调用顺序无法确定。 如果同时定义这两个任务,则应确保这两个任务能够正常运行,并且不会相互干扰。

请求超时

需要考虑两种主要的连接超时情况:

  • 为传输建立新连接时,如果未在五分钟内建立连接请求,则会中止连接请求。

  • 建立连接后,将中止两分钟内未收到响应的 HTTP 请求消息。

注意 在任一方案中,假设存在 Internet 连接,后台传输将自动重试最多三次请求。 如果未检测到互联网连接,后续请求将等待,直到检测到互联网连接为止。

调试指南

在Microsoft Visual Studio中停止调试会话相当于关闭应用;PUT 上传已暂停,POST 上传将终止。 即使在调试期间,你的应用也应列出所有已持久化的上传任务,然后重新启动或取消它们。 例如,如果对调试会话的以前操作没有兴趣,则可以让应用在应用启动时取消枚举的持久上传操作。

在调试会话期间,当系统随着应用启动开始枚举下载/上载时,如果对该调试会话之前的操作没有兴趣,你可以让应用取消它们。 请注意,如果存在Visual Studio项目更新(如对应用清单所做的更改)以及应用卸载并重新部署,则 GetCurrentUploadsAsync 无法枚举使用上一个应用部署创建的操作。

在开发期间使用后台传输时,可能会遇到活动缓存和已完成传输操作的内部缓存可能不同步的情况。这可能会导致无法启动新的传输操作或与现有操作和 BackgroundTransferGroup 对象交互。 在某些情况下,尝试与现有操作交互可能会触发崩溃。 如果将 TransferBehavior 属性设置为 Parallel,则可能会出现此结果。 此问题仅在开发过程中的某些方案中发生,不适用于应用的最终用户。

使用Visual Studio的四种方案可能会导致此问题。

  • 创建一个新项目,其应用名称与现有项目相同,但语言不同(例如,从 C++ 到 C#)。
  • 在现有项目中,将目标体系结构(例如从 x86 更改为 x64)。
  • 你更改了现有项目中的区域性(例如,从中性改为 en-US)。
  • 在包清单中添加或删除功能(例如,在现有项目中添加 企业身份验证)。

常规应用服务(包括添加或删除功能的清单更新)不会在应用的最终用户部署上触发此问题。 若要解决此问题,请完全卸载应用的所有版本,并使用新的语言、体系结构、区域性或功能重新部署。 这可以通过 “开始” 屏幕或使用 PowerShell 和 Remove-AppxPackage cmdlet 来完成。

Windows.Networking.BackgroundTransfer 中的异常

当将无效的统一资源标识符 (URI) 字符串传递给 Windows.Foundation.Uri 对象的构造函数时,将引发异常。

.NET:Windows。Foundation.Uri 类型在 C# 和 VB 中显示为 System.Uri

在 C# 和Visual Basic中,可以使用 .NET 4.5 中的 System.Uri 类和 System.Uri.TryCreate 方法之一来测试构造 URI 之前从应用用户收到的字符串,从而避免此错误。

在 C++ 中,没有方法可以尝试将字符串解析为 URI。 如果应用获取 Windows.Foundation.Uri 用户输入,则构造函数应位于 try/catch 块中。 如果引发异常,应用可以通知用户并请求新的主机名。

Windows.Networking.backgroundTransfer 命名空间提供了便捷的帮助程序方法,并使用 Windows.Networking.Sockets 命名空间中的枚举来处理错误。 这可用于在应用中以不同的方式处理特定网络异常。

Windows.Networking.backgroundTransfer 命名空间中的异步方法上遇到的错误将作为 HRESULT 值返回。 BackgroundTransferError.GetStatus 方法用于将网络错误从后台传输操作转换为 WebErrorStatus 枚举值。 大多数 WebErrorStatus 枚举值对应于本机 HTTP 或 FTP 客户端操作返回的错误。 应用可以根据异常的原因筛选特定的 WebErrorStatus 枚举值来修改应用行为。

对于参数验证错误,应用还可以使用异常中的 HRESULT 详细了解导致异常的错误。 Winerror.h 头文件中列出了可能的 HRESULT 值。 对于大多数参数验证错误,返回的 HRESULTE_INVALIDARG

重要 API