保持同步

使用 Sync Framework 创建同步提供程序

Joydip Kanjilal

下载代码示例

Microsoft Sync Framework 是一个功能完善的平台,用于同步脱机和联机数据,便于应用程序、服务和设备等进行协作和脱机访问。它独立于协议和数据库,并提供了支持以下功能的技术和工具:设备漫游、共享功能,以及离线提取网络化数据,然后在以后的某个时间进行同步的功能。

使用 Sync Framework 构建的应用程序可以在网络上使用任何协议从任何数据源同步数据。它是一个功能完善的同步平台,便于应用程序、服务和设备进行脱机和联机数据访问。Sync Framework 具有可扩展的提供程序模型,可供托管和非托管代码用来同步两个数据源之间的数据。

本文将介绍同步的概念,以及如何将 Sync Framework 集成到项目中。具体而言,我们将介绍数据同步基础、Sync Framework 的体系结构组件以及如何使用同步提供程序。

要使用 Sync Framework 和本文中的代码示例,需要安装 Visual Studio 2010 和 Sync Framework 运行时 2.0 或更高版本。您可以从 Sync Framework 开发人员中心下载该运行时(包含在 Microsoft Sync Framework 2.0 可再发行组件包中)。

Sync Framework 基础

Sync Framework 包含四个主要组件:运行时、元数据服务、同步提供程序和参与方。

Sync Framework 运行时提供用于在数据源之间同步数据的基础结构。它还提供一个 SDK,开发人员可以对其进行扩展以实现自定义提供程序。

元数据服务提供存储同步元数据(包含同步会话期间使用的信息)的基础结构。同步元数据包括版本、锚点和更改检测信息。在自定义提供程序的设计和开发过程中,也会使用同步元数据。

同步提供程序用于在副本或端点之间同步数据。副本是一个同步单元,用于标识实际的数据存储。举例来说,如果同步两个数据库之间的数据,每个数据库都称为一个副本。副本是用唯一标识符(称为副本键)标识的。此处的端点也称为数据存储。本文稍后将更深入地讨论提供程序。

参与方指的是可以检索待同步数据的位置。参与方可以是完整参与方、部分参与方和简单参与方。

完整参与方是指具备以下功能的设备:可以创建新的数据存储,可以存储同步元数据信息,可以在设备自身上运行同步应用程序。完整参与方包括桌面计算机、便携式计算机和 Tablet。完整参与方可以与其他参与方同步数据。

部分参与方是指可以创建新的数据存储和存储同步元数据信息,但不能在设备自身上运行同步应用程序的设备。USB 存储设备或智能手机可以是部分参与方。请注意,部分参与方可以与完整参与方同步数据,但不能与其他部分参与方同步数据。

简单参与方包括不能存储新数据或执行应用程序,只能提供所请求信息的设备。RSS 源、Amazon 和 Google Web 服务都属于简单参与方。

同步提供程序

同步提供程序是一个组件,它可以参与同步过程,使一个副本与其他副本同步数据。每个副本都应该有一个同步提供程序。

要同步数据,需要启动一个同步会话。应用程序连接会话中的源和目标同步提供程序,以便于副本间进行数据同步。

同步会话期间,目标提供程序向源提供程序提供关于其数据存储的信息。源提供程序确定哪些源副本更改对于目标副本而言是未知的,然后将此类更改的列表推送给目标提供程序。目标提供程序检测自己的项与该列表中的项之间是否存在任何冲突,然后将更改应用到自己的数据存储。Sync Framework 引擎为所有这些同步过程提供了便利。

Sync Framework 支持三个适用于数据库、文件系统和源同步的默认提供程序:

  • 适用于支持 ADO.NET 的数据源的同步提供程序
  • 适用于 RSS 和 Atom 源的同步提供程序
  • 适用于文件和文件夹的同步提供程序

您还可以扩展 Sync Framework,从而创建自己的自定义同步提供程序,在设备和应用程序之间交换信息。

数据库同步提供程序(以前在 Sync Framework 1.0 中称为 ADO.NET 同步服务)支持启用了 ADO.NET 的数据源的同步。通过构建未连接数据应用程序,可以在支持 ADO.NET 的数据源(如 SQL Server)之间进行同步。它支持漫游、共享和离线提取数据。任何使用数据库提供程序的数据库都可以与 Sync Framework 支持的其他数据源(包括文件系统、Web 服务,甚至是自定义数据存储)一起参与同步过程。

Web 同步提供程序(以前的 FeedSync 同步服务)支持同步 RSS 和 ATOM 源。在 FeedSync 之前,这一技术称为简单共享扩展,最初是由 Ray Ozzie 设计的。请注意,Web 同步提供程序不会替代现有技术,如 RSS 或 Atom 源。它只是提供了一种向现有 RSS 或 Atom 源添加同步功能的简单方法,以便独立于所用平台或设备的其他应用程序或服务使用这些源。

文件同步提供程序(以前的文件系统同步服务)支持对系统中的文件和文件夹进行同步。它可用于在同一系统或网络中不同系统之间同步文件和文件夹。您可以在采用 NTFS、FAT 或 SMB 文件系统的系统中同步文件和文件夹。提供程序使用 Sync Framework 元数据模型实现文件数据的对等同步,同时支持任意拓扑(客户端/服务器、交错和对等),还支持可移动介质。此外,文件同步提供程序还支持增量式同步、冲突和更改检测、在操作的预览和非预览模式中同步,以及在同步过程中筛选和跳过文件。

使用内置同步提供程序

在这部分中,我将演示如何使用内置同步提供程序实现一个简单的应用程序,用于同步系统中两个文件夹的内容。

FileSyncProvider 类可用来创建文件同步提供程序。该类扩展了 UnManagedSyncProvider 类,并且实现 IDisposable 接口。FileSyncScopeFilter 类用于包含或排除将参与同步过程的文件和文件夹。

FileSyncProvider 使用同步元数据检测副本中的更改。同步元数据包含参与同步过程的所有文件和文件夹的有关信息。实际上,有两种同步元数据:副本元数据和项元数据。文件同步提供程序存储参与同步过程的所有文件和文件夹的元数据。以后,它使用这些文件和文件夹的文件大小、属性和上次访问时间检测更改。

打开 Visual Studio 2010,创建一个新的 Windows Presentation Foundation (WPF) 项目。将项目命名为 SyncFiles,然后保存项目。打开 MainWindow.xaml 文件,创建一个类似于图 1 所示的 WPF 窗体。

图 1 示例同步应用程序

可以看到,您拥有用来选取源和目标文件夹的控件。还拥有用来显示同步统计信息以及源和目标文件夹内容的控件。

在“解决方案资源管理器”中右键单击项目,单击“添加引用”,然后添加 Microsoft.Synchronization 程序集。

现在,在 MainWindow.xaml.cs 文件中添加一个新的 GetReplicaID 方法,该方法返回一个 GUID,如图 2 中的代码所示。如果对 SyncOrchestrator 实例调用 Synchronize 方法,则会在每个使用该唯一 GUID 的文件夹或副本中创建一个名为 filesync.metadata 的元数据文件。GetReplicaID 方法将此 GUID 保存在一个文件中,以便下次调用此方法时,不会为该特定文件夹生成新的 GUID。GetReplicaID 方法首先检查是否存在包含副本 ID 的文件。如果未找到这样的文件,则创建一个新的副本 ID 并存储在文件中。如果存在这样的文件(因为以前生成了该文件夹的副本 ID),则返回此文件的副本 ID。

图 2 GetReplicaID

private Guid GetReplicaID(string guidPath) {
  if (!File.Exists(guidPath)) {
    Guid replicaID = Guid.NewGuid();
    using (FileStream fileStream = 
      File.Open(guidPath, FileMode.Create)) {
      using (StreamWriter streamWriter = 
        new StreamWriter(fileStream)) {

        streamWriter.WriteLine(replicaID.ToString());
      }
    }

    return replicaID;
  }
  else {
    using (FileStream fileStream = 
      File.Open(guidPath, FileMode.Open)) {
      using (StreamReader streamReader = 
        new StreamReader(fileStream)) {

        return new Guid(streamReader.ReadLine());
      }
    }
  }
}

接下来,添加一个名为 GetFilesAndDirectories 的方法,该方法返回副本位置下的文件和文件夹列表(请参见图 3)。 文件夹名应作为参数传递给该方法。

图 3 获取副本文件和文件夹

private List<string> GetFilesAndDirectories(String directory) {
  List<String> result = new List<String>();
  Stack<String> stack = new Stack<String>();
  stack.Push(directory);

  while (stack.Count > 0) {
    String temp = stack.Pop();

    try {
      result.AddRange(Directory.GetFiles(temp, "*.*"));

      foreach (string directoryName in 
        Directory.GetDirectories(temp)) {
        stack.Push(directoryName);
      }
    }
    catch {
      throw new Exception("Error retrieving file or directory.");
    }
  }

  return result;
}

在同步过程之前和之后,都要使用此方法,用于显示源和目标文件夹中的文件和文件夹的列表。 PopulateSourceFileList 和 PopulateDestinationFileList 方法调用 GetFilesAndDirectories,填充显示源和目标文件夹中的文件和目录的列表框(有关详细信息,请参阅代码下载)。

btnSource_Click 和 btnDestination_Click 事件处理程序用于选择源和目标文件夹。 这两个方法都使用 FolderBrowser 类来显示一个对话框,供用户选择源或目标文件夹。 FolderBrowser 类的完整源代码可以从本文的代码下载部分下载。

现在,需要编写 Button 控件的 Click 事件处理程序,它在同步开始前通过禁用按钮启动。 然后,它使用源和目标路径作为参数调用 Synchronize 方法。 最后,启动同步过程,捕捉所有错误,在同步完成后启用按钮:

btnSyncFiles.IsEnabled = false; 
// Disable the button before synchronization starts
Synchronize(sourcePath, destinationPath);
btnSyncFiles.IsEnabled = true; 
// Enable the button after synchronization is complete

Synchronize 方法接受源和目标路径,然后同步两个副本的内容。 在 Synchronize 方法中,使用 SyncOperationStatistics 类的一个实例检索同步过程的统计信息:

SyncOperationStatistics syncOperationStatistics;

此外,还创建源和目标同步提供程序,创建一个名为 synchronizationAgent 的 SyncOrchestrator 实例,将 GUID 分配给源和目标副本,然后将两个提供程序附加到该实例。 SyncOrchestrator 负责协调同步会话:

sourceReplicaID = 
  GetReplicaID(Path.Combine(source,"ReplicaID"));
destinationReplicaID = 
  GetReplicaID(Path.Combine(destination,"ReplicaID"));

sourceProvider = 
  new FileSyncProvider(sourceReplicaID, source);
destinationProvider = 
  new FileSyncProvider(destinationReplicaID, destination); 

SyncOrchestrator synchronizationAgent = 
  new SyncOrchestrator();
synchronizationAgent.LocalProvider = sourceProvider;
synchronizationAgent.RemoteProvider = destinationProvider;

最后,启动同步过程,捕捉所有错误,释放相应的资源,如图 4 所示。 本文的代码下载提供完整的源项目,其中包含错误处理和其他实现详细信息。

图 4 同步副本

try {
  syncOperationStatistics = synchronizationAgent.Synchronize(); 

  // Assign synchronization statistics to the lstStatistics control
  lstStatistics.Items.Add("Download Applied: " + 
    syncOperationStatistics.DownloadChangesApplied.ToString());
  lstStatistics.Items.Add("Download Failed: " + 
    syncOperationStatistics.DownloadChangesFailed.ToString());
  lstStatistics.Items.Add("Download Total: " + 
    syncOperationStatistics.DownloadChangesTotal.ToString());
  lstStatistics.Items.Add("Upload Total: " + 
    syncOperationStatistics.UploadChangesApplied.ToString());
  lstStatistics.Items.Add("Upload Total: " + 
    syncOperationStatistics.UploadChangesFailed.ToString());
  lstStatistics.Items.Add("Upload Total: " + 
    syncOperationStatistics.UploadChangesTotal.ToString());
}
catch (Microsoft.Synchronization.SyncException se) {
  MessageBox.Show(se.Message, "Sync Files - Error");
}
finally {
  // Release resources once done
  if (sourceProvider != null) 
    sourceProvider.Dispose();
  if (destinationProvider != null) 
    destinationProvider.Dispose();
}

您还可以报告同步会话的同步进度。 为此,请执行以下步骤:

  1. 为 ApplyingChange 事件注册一个事件处理程序。
  2. 将 FileSyncProvider 的 PreviewMode 属性设置为 true,启用预览模式。
  3. 采用一个整数计数器,在每次触发 Applying­Change 事件时递增计数。
  4. 启动同步过程。
  5. 将 FileSyncProvider 的 PreviewMode 属性设置为 false,禁用预览模式。
  6. 再次启动同步过程。

筛选和跳过文件

使用 Sync Framework 进行同步时,会自动跳过某些文件,包括 Desktop.ini、Thumbs.db、具有系统和隐藏属性的文件,以及元数据文件。 您可以应用静态筛选器来控制要同步的文件和文件夹。 具体而言,这些筛选器可以排除不需要参与同步过程的文件。

要使用静态筛选器,需要创建 FileSyncScopeFilter 类的一个实例,然后将包含和排除筛选器作为参数传递给该实例的构造函数。 此外,也可以对 FileSyncScopeFilter 实例使用 FileNameExcludes.Add 方法,从同步会话中筛选出一个或多个文件。 然后,在创建 FileSyncProvider 实例时传入此 FileSyncScopeFilter 实例。 例如:

FileSyncScopeFilter fileSyncScopeFilter = 
  new FileSyncScopeFilter();
fileSyncScopeFilter.FileNameExcludes.Add("filesync.id");
FileSyncProvider fileSyncProvider = 
  new FileSyncProvider(Guid.NewGuid(), 
  "D:\\MyFolder",fileSyncScopeFilter,FileSyncOptions.None);

同样,可以从同步过程中排除所有 .lnk 文件:

FileSyncScopeFilter fileSyncScopeFilter = 
  new FileSyncScopeFilter();
fileSyncScopeFilter.FileNameExcludes.Add("*.lnk");

甚至可以使用 FileSyncOptions 显式设置同步会话选项:

FileSyncOptions fileSyncOptions = 
  FileSyncOptions.ExplicitDetectChanges | 
  FileSyncOptions.RecycleDeletedFiles |
  FileSyncOptions.RecyclePreviousFileOnUpdates |
  FileSyncOptions.RecycleConflictLoserFiles;

要在同步过程中跳过一个或多个文件,请注册 ApplyingChange 事件的一个事件处理程序,将 SkipChange 属性设置为 true:

FileSyncProvider fileSyncProvider;
fileSyncProvider.AppliedChange += 
  new EventHandler (OnAppliedChange);
destinationProvider.SkippedChange += 
  new EventHandler (OnSkippedChange);

现在,可以实现 OnAppliedChange 事件处理程序来显示发生的更改:

public static void OnAppliedChange(
  object sender, AppliedChangeEventArgs args) {
  switch (args.ChangeType) {
    case ChangeType.Create:
      Console.WriteLine("Create " + args.NewFilePath);
      break;
    case ChangeType.Delete:
      Console.WriteLine("Delete" + args.OldFilePath);
      break;
    case ChangeType.Overwrite:
      Console.WriteLine("Overwrite" + args.OldFilePath);
      break;
    default:
      break;
  }
}

请注意,为了清晰起见,此示例进行了简化。 代码下载中包含功能更完备的实现。

要了解同步过程中跳过某一特定文件的原因,可以实现 OnSkippedChange 事件处理程序:

public static void OnSkippedChange(
  object sender, SkippedChangeEventArgs args) {

  if (args.Exception != null)
    Console.WriteLine("Synchronization Error: " + 
      args.Exception.Message); 
}

生成并执行应用程序。单击“Source Folder”按钮可选择源文件夹。使用“Destination Folder”可选择目标文件夹。可以看到,在同步之前,每个文件夹中的文件的列表都显示在各自的列表框中(请参见图 1)。因为同步尚未开始,“Synchronization Statistics”列表框不显示任何内容。

现在,单击“Synchronize”按钮启动同步过程。源和目标文件夹同步后,它们各自的列表框中会显示同步后两个文件夹的内容。“Synchronization Statistics”列表框现在显示所完成任务的有关信息(请参见图 5)。

图 5 同步完成

处理冲突

Sync Framework 管理基于时间戳的同步所涉及的所有复杂问题,包括推迟冲突、失败、中断和循环。为在同步会话过程中处理数据冲突,Sync Framework 遵循以下策略之一:

  • 源获胜:在本策略中,出现冲突时,总是采用源数据存储中的更改。
  • 目标获胜:在本策略中,出现冲突时,总是采用目标数据存储中的更改。
  • 合并:在本策略中,出现冲突时,将合并更改。
  • 记录冲突:在本策略中,将推迟或记录冲突。

了解同步流程

SyncOrchestrator 实例控制同步会话和会话期间的数据流。同步流程始终是单向的,您将一个源提供程序附加到源副本,将一个目标提供程序附加到目标副本。第一步是创建源和目标提供程序,为它们分配唯一的副本 ID,将两个提供程序附加到源和目标副本:

FileSyncProvider sourceProvider = 
  new FileSyncProvider(sourceReplicaID, @"D:\Source");
FileSyncProvider destinationProvider = 
  new FileSyncProvider(destinationReplicaID, @"D:\Destination");

接下来,创建一个 SyncOrchestrator 实例,将两个提供程序附加给它。对 SyncOrchestrator 实例调用 Synchronize 方法,在源和目标提供程序之间创建一个链接:

SyncOrchestrator syncAgent = new SyncOrchestrator();
syncAgent.LocalProvider = sourceProvider;
syncAgent.RemoteProvider = destProvider;
syncAgent.Synchronize();

此后,Sync Framework 可以在进行同步会话时进行很多调用。 我们来看一看。

对源和目标提供程序都调用 BeginSession,指示同步提供程序要加入同步会话。 请注意,如果会话无法启动或提供程序初始化不正确,BeginSession 方法会引发 InvalidOperationException:

public abstract void BeginSession(
  SyncProviderPosition position, 
  SyncSessionContext syncSessionContext);

Sync Framework 对目标提供程序的实例调用 GetSyncBatchParameters。 目标提供程序返回其知识(特定副本所知的版本或更改的紧凑表示形式)和请求的批处理大小。 此方法接受两个输出参数,即 batchSize 和 knowledge:

public abstract void GetSyncBatchParameters(
  out uint batchSize, 
  out SyncKnowledge knowledge);

Sync Framework 对源提供程序调用 GetChangeBatch。 此方法接受两个输入参数,即目标的批处理大小和知识:

public abstract ChangeBatch GetChangeBatch(
  uint batchSize, 
  SyncKnowledge destinationKnowledge, 
  out object changeDataRetriever);

现在,源同步提供程序以 changeDataRetriever 对象的形式向目标提供程序发送更改的版本和知识的摘要。

对目标提供程序调用 ProcessChangeBatch 方法来处理更改:

public abstract void ProcessChangeBatch(
  ConflictResolutionPolicy resolutionPolicy, 
  ChangeBatch sourceChanges, 
  object changeDataRetriever, 
  SyncCallbacks syncCallbacks, 
  SyncSessionStatistics sessionStatistics);

对批处理中的每个更改,都对目标同步提供程序调用 SaveItemChange。 如果要实现自己的自定义提供程序,应使用源副本发送的更改更新目标副本,然后用源知识更新元数据存储中的元数据:

void SaveItemChange(SaveChangeAction saveChangeAction, 
  ItemChange  change, SaveChangeContext context);

对目标同步提供程序调用 StoreKnowledgeForScope,在元数据存储中保存知识:

public void StoreKnowledgeForScope(
  SyncKnowledge knowledge, 
  ForgottenKnowledge forgottenKnowledge)

对源和目标提供程序都调用 EndSession,指示同步提供程序将离开它先前加入的同步会话:

public abstract void EndSession(
  SyncSessionContext syncSessionContext);

自定义同步提供程序

现在,您已经了解了默认同步提供程序的工作原理。 如前所述,您还可以实现自定义同步提供程序。 自定义同步提供程序可扩展内置同步提供程序的功能。 如果要同步的数据存储没有提供程序,就需要自定义同步提供程序。 此外,也可以创建自定义同步提供程序来实现更改单元,从而更好地控制更改跟踪,减少冲突数量。

要设计自己的同步提供程序,需要创建一个类来扩展 KnowledgeSyncProvider 抽象类,并实现 IChangeDataRetriever 和 INotifyingChangeApplierTarget 接口。 请注意,这些类和接口是 Microsoft.Synchronization 命名空间的一部分。

举一个自定义提供程序的示例,假定要实现一个同步提供程序在数据库之间同步数据。 这只是一个简单示例的概述,可以对它进行扩展来实现更为复杂的方案。

首先,在 SQL Server 2008 中创建三个数据库(我将它们命名为 ReplicaA、ReplicaB 和 ReplicaC),在每个数据库中都创建一个名为 Student 的表。 自定义提供程序将同步这三个 Student 表中的记录。 接下来,创建一个名为 Student 的实体,用于对 Student 表执行 CRUD 操作。

创建一个名为 Student 的类,该类具有字段 StudentID、FirstName 和 LastName,然后创建在数据库中执行 CRUD 操作必需的帮助程序方法:

public class Student {
  public int StudentID { get; set; }
  public String FirstName { get; set; }
  public String LastName { get; set; }
  //Helper methods for CRUD operations
...
}

创建一个名为 CustomDBSyncProvider 的类,并从 KnowledgeSyncProvider、IChangeDataRetriever、INotifyingChangeApplierTarget 和 IDisposable 接口对它进行扩展:

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Synchronization;
using Microsoft.Synchronization.MetadataStorage; 
public class CustomDBSyncProvider : KnowledgeSyncProvider, 
  IChangeDataRetriever, 
  INotifyingChangeApplierTarget,IDisposable {
...

实现自定义数据库同步提供程序中必需的方法,创建 UI 来显示每个 Student 表的内容(有关详细信息,请参阅本文的代码下载)。

现在,创建自定义同步提供程序的三个实例,将它们附加到每个 Student 数据库表。 最后,在自定义同步提供程序的帮助下,将一个副本的内容与其他副本的内容同步:

private void Synchronize(
  CustomDBSyncProvider sourceProvider, 
  CustomDBSyncProvider destinationProvider) {

  syncAgent.Direction = 
    SyncDirectionOrder.DownloadAndUpload;
  syncAgent.LocalProvider = sourceProvider;
  syncAgent.RemoteProvider = destinationProvider;
  syncStatistics = syncAgent.Synchronize();
}

同步处理

可以看到,Sync Framework 提供了一个简单但功能完善的同步平台,这个平台可以在脱机和联机数据之间进行无缝同步。 它可独立于所用协议和数据存储对数据进行同步。 它可用于进行简单的文件备份,也便于针对基于协作的网络进行扩展。 您还可以创建自定义同步提供程序来支持没有现成同步提供程序的数据源。

Joydip Kanjilal 是一位独立软件顾问,也是 2007 年度以来 ASP.NET 领域的 Microsoft MVP。他还是一位演讲家和作家,您可以通过 aspadvice.com/blogs/joydip 了解他的书籍、文章和博客。

衷心感谢以下技术专家对本文的审阅: Liam Cavanagh