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

使用后台智能传输服务 (BITS)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (17投票s)

2007年2月13日

CPOL

11分钟阅读

viewsIcon

94856

downloadIcon

3004

使用 BITS 系统服务。

Sample image

引言

BITS 是 Windows 2000、XP、Server 2003 和 Vista 中提供的一项系统服务。系统使用它来上传和下载 Internet 文件;Windows Update 使用此服务。该服务是一个基本的 COM 组件,并通过一些接口公开其对象。本文重点介绍如何在应用程序中包装 BITS 来添加、删除和监视作业,向作业添加文件,以及实现其他功能来自动化任务。

背景

很可能,BITS 是为了管理后台系统更新(从一个节到另一个节)以维护上传/下载数据的完整性而开发的。实际上,只有系统使用此服务,但 Microsoft 在 PSDK 中记录了这一“安心之举”,并免费分发了一个用于管理它的应用程序:bitsadmin。它包含在 Windows XP Service Pack 2 支持工具中,并支持所有 BITS 命令。此应用程序以控制台模式运行,因此我将其实现为一个使用 COM 组件的 UI 应用程序。演示应用程序不是下载管理器,它可能是创建下载管理器的第一步。

Using the Code

C++ 项目是使用 Visual C++ 2005 Express、PSDK Server 2003 SP1 (2005.03)、ATL 3.0(在 PSDK 源代码中)和 WTL 7.5(SourceForge)创建的。WTLWizard 用于创建它,但代码已被修改,以删除 CFrameWnd 实现和其他一些对象;它根本没有优化,唯一的目的是实现 BITS 组件并了解其工作原理。它以 Unicode 编译;不支持 ANSI。BITS 是一个像 GDI+ 一样的 Unicode 组件,所有 string 都必须是 Unicode 格式。此服务在 Windows 95/98/ME 和 NT 中不可用。要将 BITS 插入通用项目中,请确保为正确的平台编译它;请参阅 使用 Windows 头文件,以正确定义预处理器常量并以 Unicode 格式编译它(这不是必需的,但可以避免麻烦)。最后,在项目主头文件中(标准 Visual Studio 项目为 StdAfx.h)包含头文件 bits.h

BITS 概述

该服务可以在后台和前台上传和下载文件,在连接时恢复作业,并在关闭系统会话时暂停作业。文件仅使用 HTTP 和 HTTPS 协议传输,支持或不支持身份验证。组件抽象使用作业作为基本单位;一个作业定义了传输调度模型,可以包含一个或多个文件。它通过 ID 标识。两个或多个作业可以具有相同的名称,但不能具有相同的 ID;名称区分大小写。每个作业都有自己的优先级;有四种优先级类型:低、普通、高和前台。对于前三种类型,传输是在后台进行的。共享连接的其他应用程序可以停止作业传输并占用全部带宽;前台模式下的作业使用全部带宽。所有作业共享传输时间,因此大作业不会阻塞小作业。

像许多基本的 COM 组件一样,BITS 公开了一个主对象,所有其他对象都通过该主对象访问。

IBackgroundCopyManager 主服务 COM 对象,用于创建和枚举作业。
IEnumBackgroundCopyJobs 作业枚举器对象。作业计数和作业属性反映查询枚举器时的时间状态。它用于获取作业并向用户显示属性。作业状态在枚举器请求发出时被划掉。
IBackgroundCopyJob
IBackgroundCopyJob2
IBackgroundCopyJob3
作业对象。根据 BITS 版本,有三个接口用于作业。然而,在经典的 COM 风格中,基本接口 IBacgroundCopyJob 实现所有方法,用于添加、删除、监视作业、枚举和添加文件。其他接口专门用于特殊通知、上传回复和凭据。这些功能在此处不作描述。本文档中的所有操作都使用基本接口。作业对象可以通过 IEnumBackgroundCopyJobs 集合或 IBackgroundCopyManager(按 ID)访问。
IEnumBackgroundCopyFiles 文件枚举器对象,是作业的文件集合。此对象具有与作业枚举器相同的功能,每个作业都有一个。
IBackgroundCopyFile
IBackgroundCopyFile2
文件对象,它有一些方法用于获取和设置属性。最相关的功能由基本接口实现;在本文档中,仅使用此接口。
IBackgroundCopyError 错误信息对象,用于获取描述性错误字符串。
IBackgroundCopyCallback 此接口定义了作业触发的事件;它必须实现并注册到作业。它通知错误和作业状态;但是,可以通过轮询循环检索相同的信息。演示应用程序使用主消息循环来获取作业状态和属性。

创建主组件对象

为了简化 COM 编程,BITS 接口被转换为 CComPtr<> 特化对象

   typedef CComPtr<IBackgroundCopyManager>    CComBitsManager;
   typedef CComPtr<IEnumBackgroundCopyJobs>   CComBitsEnumJobs;
   typedef CComPtr<IBackgroundCopyJob>        CComBitsJob;
   typedef CComPtr<IEnumBackgroundCopyFiles>  CComBitsEnumFiles;
   typedef CComPtr<IBackgroundCopyFile>       CComBitsFile;
   typedef CComPtr<IBackgroundCopyError>      CComBitsError;

每个对象的所有方法都非常直接和简单。BITS 不是一个复杂的组件。对于返回 string 的方法,组件会分配内存,您只需要使用 API ::CoTaskMemFree() 释放该内存。

   ...
   CString strText;
   LPWSTR pwstrText = NULL;
   hResult = comobj->SomeMethod (&pwstrText);
   if (SUCCEEDED (hResult))
   {
      strText = pwstrText;
      ::CoTaskMemFree (pwstrText);
   }
   ...

在上面的示例中,comobj 是一个 BITS 对象,SomeMethod 是一个带有 string 参数的方法。在调用该方法之前,声明一个 Unicode string 指针,并将其初始化为 NULL。调用该方法并传递指针的地址;如果函数成功,将 string 保存到另一个位置,为了简单起见,可以保存在 CString 对象中,然后释放方法返回的 string 。如果您的项目以 Unicode 编译,可以使用以上行来实现;如果不是,则需要进行 Unicode 到 ANSI 的转换。

现在,我们可以开始使用该组件了。主对象可以在应用程序初始化时创建,并在退出时释放。CComPtr 不需要显式释放,析构函数会自动完成这项工作。对象必须全局定义,或者作为某个全局对象的成员;在后一种选择中,它在容器对象构造函数中创建,在析构函数中释放。

   // Create BITS manager object
   ...
   CComBitsManager comBits;
   hResult = comBits.CoCreateInstance (__uuidof (BackgroundCopyManager),
                                       NULL, CLSCTX_LOCAL_SERVER);
   ...

   // Release BITS manager object
   ...
   comBits.Release ();
   ...

所有其他对象都必须在您要使用它们时进行查询。不要查询作业枚举器对象并将其保存以备后用,作业集合中的作业属性和状态不会更新。始终在需要获取作业属性和状态时查询作业枚举器,并在完成后释放它,如下一节所述。

监视作业

当我们想包装一项服务时,获取其状态和相关对象的信息非常重要。在大多数情况下,我们无法保存对象以供以后使用,它们最常在数量和形式上发生变化。每次我们想知道服务当前管理的对象数量时,都必须查询它们。此操作在刷新循环中完成,对象将在循环结束时释放。它们的状态和属性值以人类可读的形式显示给用户,并保存在其他对象类型中。此过程需要大量工作;在示例代码中,刷新时间可以在选项对话框中自定义,从 1 到 20 秒。为了简化 UI 更新(TreeViewListView 控件),作业和文件数据保存在这些 C++ 类中:CBitsJobCBitsFile(请参阅源代码)。

   // Enumerating Jobs
   ...
   CComBitsEnumJobs comEnumJobs;
   CComBitsJob comJob;
   hResult = comBits.EnumJobs (0, &comEnumJobs);
   if (SUCCEEDED (hResult))
   {
      ULONG ulCount = 0;
      hResult = comEnumJobs->GetCount (&ulCount);
      hResult = comEnumJobs->Reset ();
      // signed/unsigned syndrome
      int iCount = ulCount;
      for (int i = 0; i < iCount; i++)
      {
         hResult = comEnumJobs->Next (1, &comJob, NULL);
         if (SUCCEEDED (hResult))
         {
            // Get job data and update UI
            ...
            // Get job files and update UI
            ...
            // End using Job
            comJob.Release ();
         }
      }
      // End using EnumJobs
      comEnumJobs.Release ();
   }
   ...

当一个函数返回一个无符号值(除了 DWORDWORDBYTE)时,我立即将其转换为有符号值。这最常发生在无符号值用作计数器(对象计数、颜色计数等)时,但对此有一个原因。通常,计数器在 for/while 循环中使用有符号整数作为索引,并将其与计数器进行比较,以在索引达到计数器值(减一)时停止循环。为了避免编译器警告或将无符号转换为有符号值,我立即进行转换。在源代码示例中,我将其注释为“有符号/无符号综合症”,不用理会。

上面的代码显示了如何枚举作业。EnumJobs 方法的第一个参数是标志。目前,只有一个标志被定义。在示例中,标志设置为 0。这意味着只枚举当前登录用户的作业。要枚举所有用户的作业,请将标志设置为 BG_JOB_ENUM_ALL_USERS。接下来,获取作业计数并定位枚举器位置以开始。for 循环遍历 comJob 对象中的作业。如果函数成功,则获取其属性和状态并刷新用户界面,然后以相同的方式枚举文件,如本代码所示。

   // Enumerate Files
   ...
   CComBitsEnumFiles comEnumFiles;
   CComBitsFile comFile;
   hResult = comJob.EnumFiles (&comEnumFiles);
   if (SUCCEEDED (hResult))
   {
      ULONG ulCount = 0;
      hResult = comEnumFiles->GetCount (&ulCount);
      hResult = comEnumFiles->Reset ();
      // signed/unsigned syndrome

      int iCount = ulCount;
      for (int i = 0; i < iCount; i++)
      {
         hResult = comEnumFiles->Next (1, &comFile, NULL);
         if (SUCCEEDED (hResult))
         {
            // Get file data and update UI
            ...
            // End using File
            comFile.Release ();
         }
      }
      // End using EnumFiles
      comEnumFiles.Release ();
   }
   ...

枚举文件与枚举作业类似。作业有许多属性、一个状态和进度数据。属性包括创建的日期和时间、传输优先级、名称、ID 和描述、所有者、连接远程服务器和开始传输之前的延迟时间,以及一个无进度超时时间,之后作业将引发错误,并且通常会丢失已传输的作业数据并尝试新的连接。最后两个参数以秒为单位。重试延迟默认为 10 分钟(600 秒),第二个是一个较大的超时时间 - 1,209,600 秒,相当于 14 天 - 这意味着如果作业在此期间无法传输数据,状态将设置为“error”,并且作业将被重置。在此事件发生之前的状态称为“瞬时错误”。进度数据保存在一个结构中,该结构的成员是:添加到作业的总文件数和已传输的文件数,要传输的总字节数,以及已传输的字节数。

文件数据仅限于远程和本地文件名以及进度,一个包含三个成员的结构:要传输的总字节数,已传输的字节数,以及一个完成标志 - 此标志指定文件可供用户使用,而不是表示已完全传输。要测试文件是否已完全传输,请比较已传输的字节数和总字节数;如果它们的值相同,则表示文件已成功传输。

创建和管理作业和文件

所有 BITS 操作都由作业对象实现,只有作业创建是主对象的方法。作业最重要的属性是 ID。这是创建作业所需的唯一值。ID 必须具有 GUID 格式,通常用于在 *.idl 文件中创建 COM 对象。有一个 Windows API 用于创建随机 GUID

   ...
   // Create a Job
   CComBitsJob comJob;
   GUID guidID;
   hResult = ::UuidCreate (&guidID);
   if (SUCCEEDED (hResult))
   {
      hResult = comBits->CreateJob (TEXT ("The Job"), BG_JOB_TYPE_DOWNLOAD,
                                    &guidID, &comJob);
      ...
   }
   ...

新创建的作业具有普通优先级,其状态为挂起。使用作业的方法设置初始状态并添加文件。

   // Job common operations
   hResult = comJob->Resume ();
   hResult = comJob->Suspend ();
   hResult = comJob->Cancel ();
   hResult = comJob->Complete ();

ResumeSuspend 方法不言自明。Cancel 方法从列表中删除作业,并且所有数据都将丢失。Complete 方法将文件提供给用户,并从列表中删除作业。当作业中的所有文件都传输完成后,它们对用户来说是不可用的。为此,必须完成所有文件,只有 Complete 方法才能做到这一点。在作业中的所有文件都传输完成后,示例应用程序会调用 Complete 方法,并通过对话框通知用户。

要添加文件,请使用 AddFileAddFileSet 方法。前者一次添加一个文件;后者添加一个文件列表。您要添加文件所需的一切就是远程文件 URL 和传输文件的本地文件系统路径。

   // Files operations
   hResult = comJob->AddFile (strRemoteFile, strLocalFile);
   hResult = comJob->AddFileSet (iCount, oFileSetArray);

远程 URL 协议必须是 HTTP 或 HTTPS。URL 必须指定一个静态文件。不支持重定向。如果 URL 无效,作业将拒绝该文件。作业按添加顺序一次下载一个文件:第一个添加的文件是第一个传输的文件。

错误处理

当发生错误时,作业的状态将变为“error”。要获取错误信息,首先从作业中获取错误对象,然后调用其方法以获取上下文和消息的描述性 string。在获取 string 消息时,您必须指定语言 ID。并非所有语言都受支持,因此如果获取 string 对某种语言失败,请尝试检索美国英语语言的 string 。在我的系统(意大利语)上,上下文错误 string 仅以意大利语提供,错误消息 string 仅以美国英语提供。真神秘!

更新

2008年6月2日

  • 根据文章底部讨论中“hagrdan”的建议,更新了 CBitsFileCBitsJob 类中的比较运算符重载以及后置生成过程。
  • 更新了 stdafx.h 以进行 ATL 编译,而无需修改源代码,并更新为 WTL 8.0。
  • 更新了 CApp::Create 中的一个错误,模块初始化移至嵌套 if 语句的顶部。
© . All rights reserved.