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

引言
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 |
作业对象。根据 BITS 版本,有三个接口用于作业。然而,在经典的 COM 风格中,基本接口 IBacgroundCopyJob 实现所有方法,用于添加、删除、监视作业、枚举和添加文件。其他接口专门用于特殊通知、上传回复和凭据。这些功能在此处不作描述。本文档中的所有操作都使用基本接口。作业对象可以通过 IEnumBackgroundCopyJobs 集合或 IBackgroundCopyManager (按 ID)访问。 |
IEnumBackgroundCopyFiles |
文件枚举器对象,是作业的文件集合。此对象具有与作业枚举器相同的功能,每个作业都有一个。 |
IBackgroundCopyFile |
文件对象,它有一些方法用于获取和设置属性。最相关的功能由基本接口实现;在本文档中,仅使用此接口。 |
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 更新(TreeView
和 ListView
控件),作业和文件数据保存在这些 C++ 类中:CBitsJob
和 CBitsFile
(请参阅源代码)。
// 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 ();
}
...
当一个函数返回一个无符号值(除了 DWORD
、WORD
或 BYTE
)时,我立即将其转换为有符号值。这最常发生在无符号值用作计数器(对象计数、颜色计数等)时,但对此有一个原因。通常,计数器在 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 ();
Resume
和 Suspend
方法不言自明。Cancel
方法从列表中删除作业,并且所有数据都将丢失。Complete
方法将文件提供给用户,并从列表中删除作业。当作业中的所有文件都传输完成后,它们对用户来说是不可用的。为此,必须完成所有文件,只有 Complete
方法才能做到这一点。在作业中的所有文件都传输完成后,示例应用程序会调用 Complete
方法,并通过对话框通知用户。
要添加文件,请使用 AddFile
或 AddFileSet
方法。前者一次添加一个文件;后者添加一个文件列表。您要添加文件所需的一切就是远程文件 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”的建议,更新了
CBitsFile
和CBitsJob
类中的比较运算符重载以及后置生成过程。 - 更新了 stdafx.h 以进行 ATL 编译,而无需修改源代码,并更新为 WTL 8.0。
- 更新了
CApp::Create
中的一个错误,模块初始化移至嵌套if
语句的顶部。