本次课程主要讲解了以下几个议题
•什么是智能客户端?
•离线用户需求与技术挑战
•数据通讯策略
•连接管理
•客户端数据缓存和同步
•Offline Application Block
什么是智能客户端?
•丰富的用户界面(Microsoft® Windows® Forms):利用winform的用户界面
•后台连接服务
•客户端安全运行
•支持通过网络自动部署和更新
•支持断开连接操作:数据保存在本地,当连接时提交到服务器
离线用户需求与技术挑战
用户需求:
1、当连接到网络或者断开网络连接时,不影响用户使用
2、只能同步特定用户的数据
3、初始化数据的部署要与应用程序的部署类似
4、只同步发生了变化的数据
我们面临的技术挑战
1、离线数据可能存在冲突:因为用户可以离线编辑数据,我们可能会面临并发冲突的问题。
2、数据大纲可能发生变化
3、不可靠或者速度较慢的网络连接
4、同步时的数据集中问题
5、为离线用户流水号的分发
通讯策略:
• ADO.NET
• Enterprise Services (COM+)
•Microsoft® Message Queuing Services(MSMQ)
•数据库合并复制
数据传输策略
•面向对象
–通过方法调用或者属性来传递数据对象
•数据集或者自定义业务实体
•面向服务
– Invoke操作,传递或者接收参数
–传递包含数据的消息
•面向数据
–执行查询/复制/队列
数据库
ADO.NET
•采用偶尔连接策略
–只有在获取和更新数据时才连接到数据库
•数据装载到数据集中
•可以将数据集保存在本地客户端
–隔离存储空间
•支持冲突检测
Enterprise Services
COM+ & MSMQ
•企业级应用,实现客户端与服务器异步调用
•MSMQ采用队列保存调用过程
–支持自动重试
•采用COM组件技术
合并复制
SQL Server 2005
• 支持冲突检测的双向数据同步
• 支持数据过滤以便于每个用户都能够获得自己的数据
• 支持通过Internet同步数据的能力
• 在第一次同步时能够自动部署每位用户的数据子集
• 创建订阅和同步控制以及监视的新的API
• 支持数据库大纲的变化
• 性能和可扩展性都较Microsoft® SQL Server™ 2000有了很大的改观
连接管理
•识别与控制在线vs 离线操作
–修改界面/ 功能
•识别技术
–尝试连接操作– 错误处理
–WinInet API
– Offline Block Connection Management
– NetworkChange/NetworkInterface类(Microsoft® .NET 2.0)
先看一个demo,这个demo用来监测客户端是否在线
- public partial class Form1 : Form
- {
- public Form1()
- {
- InitializeComponent();
- }
- private void Form1_Load(object sender, EventArgs e)
- {
- //调用函数,判断网络是否连接
- OnAddressChanged(this, EventArgs.Empty);
- //网络接口的网络地址改变的事件,并给这个事件加上事件处理器
- NetworkChange.NetworkAddressChanged += new NetworkAddressChangedEventHandler(OnAddressChanged);
- }
- void OnAddressChanged(object sender, EventArgs e)
- {
- //判断网络是否连接
- bool connected = IsOnline();
- //判断调用此方法的线程是否是创建控件的线程.
- if(InvokeRequired)
- {
- //如果不是,通过委托来异步调用修改控件的值
- SetStatus del = delegate(bool status)
- {
- UpdateStatus(status);
- };
- Invoke(del, new object[]{connected});
- }
- else
- {
- //如果是,那么直接修改Label的值
- UpdateStatus(connected);
- }
- }
- void UpdateStatus(bool connected)
- {
- if(connected)
- {
- connectionLabel.Text = "Connected";
- }
- else
- {
- connectionLabel.Text = "Not Connected";
- }
- }
- static bool IsOnline()
- {
- //得到所用的网络适配器,并把它放在数组里
- NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces();
- //循环遍历每个网络适配器
- foreach(NetworkInterface adapter in adapters)
- {
- //得到网络适配器的ip协议的配置信息
- IPInterfaceProperties properties = adapter.GetIPProperties();
- //如果当前的网络适配器的操作状态不是可运行的,那么继续循环
- if(adapter.OperationalStatus != OperationalStatus.Up)
- {
- continue;
- }
- //如果是网络适配器的状态是可运行的,那么得到ip协议的单播地址集合
- UnicastIPAddressInformationCollection addressCollection = properties.UnicastAddresses;
- //遍历单播网络地址集合
- foreach(UnicastIPAddressInformation addressInfo in addressCollection)
- {
- //判断地址是否是环回地址
- if(IPAddress.IsLoopback(addressInfo.Address))
- {
- continue;
- }
- //判断网络地址是否是不使用任何网络接口的ip地址(IPV6)
- if(addressInfo.Address.ToString() == IPAddress.IPv6None.ToString())
- {
- continue;
- }
- //判断网络地址是否是不使用任何网络接口的ip地址
- if(addressInfo.Address.ToString() == IPAddress.None.ToString())
- {
- continue;
- }
- //如果通过检测表明此网络适配器是可用的,返回true
- return true;
- }
- }
- //循环完成之后没有找到可用的网络适配器,返回false
- return false;
- }
- }
- //委托的定义
- delegate void SetStatus(bool status);
这个demo演示了我们在客户端如何来检测网络的状态
客户端数据缓存
• 内存
• 保存数据传输对象
– 文件系统
– 隔离存储区
• 数据库
– Microsoft® SQL Server™ 2000 Desktop Engine (MSDE)
– SQL Server™ 2005 Express Edition
– SQL Server™ 2005 Mobile Edition
• 消息队列
– MSMQ
– 数据库
• Custom, SQL Service Broker
– Enterprise Services
在离线的情况下,我们可以将数据保存在客户端,我们可以通过文件系统的隔离存储区来保存,也可以通过sqlserver的Mobile版本等来保存
下面是一个保存数据到文件系统的隔离存储区的例子
我们首先利用数据绑定向导生成对应表的DataSet,BindingSource和Adapter,然后在窗体上拖入BindingNavigator和MenuStrip控件,在BindingNavigator控件上添加一个Save按钮和一个ComboBox控件,ComboBox用来控制是否在线,Save按钮用来保存数据.在MenuStrip控件上加一个Load的菜单,用来控制读取数据.
- public partial class Form1 : Form
- {
- public Form1()
- {
- InitializeComponent();
- }
- private bool Online = false;
- private void Form1_Load(object sender, EventArgs e)
- {
- }
- private void tsbSave_Click(object sender, EventArgs e)
- {
- //调用校验控件的方法,判断数据校验是否成功.
- this.Validate();
- //将更改状态结束
- this.wFUSERSBindingSource.EndEdit();
- //得到在线状态
- Online = GetNetworkStatus();
- if(Online)
- {
- //如果在线,那么通过Adapter对象Update数据集DataSet
- wFUSERSTableAdapter.Update(this.workflowDataSet.WFUSERS);
- }
- else
- {
- //离线则将数据集保存在隔离存储区里.
- CacheHelper.WriteDataSetToisolatedStorage(this.workflowDataSet, "MyFile");
- }
- }
- private void loadToolStripMenuItem_Click(object sender, EventArgs e)
- {
- //得到在线状态
- Online = GetNetworkStatus();
- if(Online == true)
- {
- //如果在线则从数据库得到数据,并填充到DataSet里
- this.wFUSERSTableAdapter.Fill(this.workflowDataSet.WFUSERS);
- }
- else
- {
- //如果离线,那么从隔离存储区里得到数据,并填充到DataSet里
- CacheHelper.ReadDataSetFromIsolatedStorage(this.workflowDataSet, "MyFile");
- }
- }
- //得到网络的状态,这里并没有真实的判断网络的状态,而是根据下拉框
- //的选择来模拟网络状态.
- private bool GetNetworkStatus()
- {
- if(0 == toolStripComboBox1.SelectedIndex)
- {
- return true;
- }
- else
- {
- return false;
- }
- }
- }
这是窗体的代码,用来控制界面显示和向服务端提交数据,在这段代码内有一个CacheHelper类,代码如下
- public class CacheHelper
- {
- /// <summary>
- /// 写数据到隔离存储区
- /// </summary>
- /// <param name="data">保存数据的DataSet</param>
- /// <param name="fileName">保存数据的文件名</param>
- public static void WriteDataSetToisolatedStorage(DataSet data, string fileName)
- {
- //根据程序集得到对应用户的隔离存储区
- IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForAssembly();
- //构造一个隔离存储区的文件流,并指定文件名和用户隔离存储区对象
- using(IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(fileName, System.IO.FileMode.Create, isoStore))
- {
- //构造一个流,用来写数据
- using(StreamWriter writer = new StreamWriter(isoStream))
- {
- //将DataSet里的数据通过流写入到隔离存储文件,包括DataSet的原始值和当前值,将来用来解决并发冲突的问题
- data.WriteXml(writer, XmlWriteMode.DiffGram);
- }
- }
- }
- /// <summary>
- /// 从隔离存储区读取数据到DataSet
- /// </summary>
- /// <param name="data">用来保存数据的DataSet</param>
- /// <param name="fileName">读取的文件名</param>
- public static void ReadDataSetFromIsolatedStorage(DataSet data, string fileName)
- {
- //根据程序集得到对应用户的隔离存储区
- IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForAssembly();
- //构造一个隔离存储区的文件流,并指定文件名和用户隔离存储区对象
- using(IsolatedStorageFileStream isoStream = new IsolatedStorageFileStream(fileName, FileMode.Open, isoStore))
- {
- //构造一个流,用来读取数据
- using(StreamReader reader = new StreamReader(isoStream))
- {
- //将保存在隔离存储区的数据读取到DataSet,包含原始值和当前值
- data.ReadXml(reader, XmlReadMode.DiffGram);
- }
- }
- }
- }
我们可以在CacheHelper类里面看到,如何将数据保存到存储隔离区,那么具体的存储隔离区的目录在C:/Documents and Settings/Administrator/Local Settings/Application Data/IsolatedStorage目录下
数据同步
•引用数据vs. 可操作数据
•面向数据
–合并复制
•面向服务
–远程方法调用
–消息传递
•确认返回消息
•轮询
在职能客户端中我们将利用Com+配合MSMQ来将发送来的消息存放到消息队列中
看下面一个demo
- //定义Com+的名称
- [assembly:ApplicationName("QCDemo")]
- //定义组件是在服务器中运行还是在客户端运行
- [assembly:ApplicationActivation(ActivationOption.Server)]
- //定义生成全局程序集的类库用的密钥文件
- [assembly:AssemblyKeyFile(@"C:/test.snk")]
- //为程序集启用队列支持,并启用应用程序从"消息队列"队列读取方法的调用
- [assembly:ApplicationQueuing(Enabled=true, QueueListenerEnabled=true)]
- //为程序集指定访问控制,这里为不启用安全控制,并不验证身份.
- [assembly:ApplicationAccessControl(Value=false, Authentication=AuthenticationOption.None)]
- namespace QCDemo
- {
- //为IQComponent接口启用队列支持
- [InterfaceQueuing]
- //定义接口Com可视,并指定接口的Guid值,在视频中没有此Atrribute
- //但经过我的测试,如果不加此Attribute将不能将Com对象转换为.net中的接口
- [ComVisible(true)]
- [Guid("30C780E0-74FC-41d3-9BDB-7674DFDCC5A3")]
- public interface IQComponent
- {
- void Function(string param);
- }
- [ComVisible(true)]
- [Guid("83BA6E50-599E-4f58-BBF7-D6BED7935C43")]
- public class QComponent : ServicedComponent, IQComponent
- {
- //无参构造函数,必须
- public QComponent()
- {
- }
- #region IQComponent 成员
- public void Function(string param)
- {
- MessageBox.Show(param);
- }
- #endregion
- }
- }
编写好以上代码之后,需要将Assembly中的一些设置进行改变,如下:
- // 将 ComVisible 设置为 false 使此程序集中的类型
- // 对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型,
- // 则将该类型上的 ComVisible 属性设置为 true。
- [assembly: ComVisible(true)]
接下来需要将写好的Com+组件注册到Com+服务中,如下图:
我们可以看到QCDemo已经装载到Com+服务里,我们需要通过一个命令来将QCDemo装载到Com+服务里,但在装载到Com+服务之前,我们需要安装MSMQ服务,打开添加删除windows组件>应用程序服务器里>消息队列
安装好消息队列之后,我们可以利用regsvcs 命令将生成的com+组件装载到Com+服务里。在Visual Studio命令提示行里,进入QCDemo生成的debug目录,运行regsvcs -i QCDemo.dll,就可将QCDemo装载到Com+服务器里。注意,我们在生成QCDemo之前需要用sn命令生成强名称密钥文件,如:sn -k mykey.snk。在做做完这些配置之后,我们需要将生成的dll加载到全局程序集里,同样在Visual Studio命令行里用gacutil -i QCDemo.dll,将QCDemo加载到全局程序集缓存中。
现在我们已经将服务端配置好了,下一步是在客户端进行调用
- public partial class Form1 : Form
- {
- public Form1()
- {
- InitializeComponent();
- }
- private void button1_Click(object sender, EventArgs e)
- {
- //通过非托管类型方法集,创建指定名称对象标识的接口指针
- IQComponent iQC = (IQComponent)Marshal.BindToMoniker("queue:/new:QCDemo.QComponent");
- iQC.Function("hello world");
- Marshal.ReleaseComObject(iQC);
- }
- }
这里需要将QCDemo的dll添加到引用,我在添加引用的时候,在Com组里面看到了QCDemo的tlb文件,表明QCDemo已经添加到Com的程序集里,但是我添加的时候弹出tlb是.net程序集导出的,无法将其作为引用添加,请改为添加对.net程序集的引用的错误,最后,只好通过浏览找到QCDemo.dll然后添加到引用中,这个问题暂时还不知道原因
运行这个ClientDemo,我们我可以看到弹出来的消息框,如果QCDemo这个com+的应用程序没有启动,我们将不会看到弹出的消息框,但是,这个请求已经保存在队列里面我们可以在计算机管理>消息队列中看到
QCDemo中的消息队列会保存未处理的消息,并根据时间自动重试,然后将消息自动向QCDemo_0等消息队列中存储,最后放入到QCDemo_DeadQueue就是死亡的队列中,当我们启动QCDemo的应用程序之后,COM+将从QCDemo的消息队列中轮询得到消息,并弹出消息框。
数据同步
并发性问题
•解决并发冲突
–乐观锁于悲观锁
• ADO.NET检测/ 异常传播
–采用乐观锁
•通过ADO.NET事务的方式解决
–多数据库同步
•分布式事务
– Enterprise Services
– "Indigo"
并发性冲突是一个经常碰到的问题,如当用户A下载数据之后修改了数据,但是并没有提交到服务器,用户B修改了数据并且提交到了服务,在B提交之后,A提交数据,这时候,数据服务器上的数据对于A来说是已经修改了,这时候,就发生了并发性的冲突
解决并发性冲突有乐观锁和悲观锁两种方式。
悲观所:用户修改数据的时候一直锁定数据,让其他用户不能修改数据。这种方式效率比较低下,用户体验也不好
乐观锁:用户修改数据的时候不锁定数据,只是在将数据提交到数据库的时候锁定数据,并且用户得到数据的时候有一个数据库的原始副本和一个修改后的数据,提交到数据库的时候,检测原始副本和数据库的数据是否一致,如果一致,那么将修改后的数据提交到数据库,如果不一致,那么将抛出异常,不提交到数据库。
在我们离线数据访问的Demo中我们可以做这个测试,先从数据库Load数据,然后通过别的方式修改数据库的数据,然后在Demo中我们修改数据提交到数据库,系统将抛出并发性冲突的异常。但是这个是可以配置的,如下图:
在配置的DataSet中配置菜单的高级选项中,使用开发式并发选项勾中就表示使用乐观锁来处理并发冲突,如果不勾中,则不检测并发冲突,直接更新,并且我检测了两种生成的代码,在配置了并发冲突中,多了Original参数,包括DataRowVersion的配置属性
Offline Application Block•连接状态检测/ 控制
•下载/ 上传数据
•队列数据请求
•引用数据缓存
•异步请求处理
•加密/ 存储数据签名
•提供连接检测模型,数据请求队列,数据缓存,服务代理