WCF/WCF Silverlight 客户端异步调用总结





5.00/5 (4投票s)
总结 WCF/WCF Silverlight 中的客户端异步调用。介绍 ServiceClient 类。
引言
本文介绍了 WCF 和 WCF Silverlight 中使用基于导出的服务协定运行时生成的代理进行异步调用的方法,并介绍了一个 ServiceClient<T>
类,以简化异步调用。
异步操作总览
异步操作意味着实际操作正在与启动线程并行执行,启动线程在操作进行过程中可以继续执行其任务。
就远程调用而言,可以区分两种主要的异步调用类型:服务器端异步调用和客户端异步调用。
服务器端
在这种情况下,从客户端的角度来看,远程调用是完全同步的。客户端调用服务上的一个方法,该方法会正常返回(远程调用结束),因此客户端可以继续工作。但关键在于,服务器在服务方法中并未完成操作,而是启动了一个单独的线程来执行它。
这完全是一个服务器端解决方案。但是,系统需要考虑这一点进行设计,因为在远程调用结束时您不会收到结果。如果客户端也需要操作的结果,则服务应回调客户端,这需要额外的设施(此时,客户端变成服务器),可能并不简单(客户端可能需要打开监听端口,必须将对象引用/委托传递给服务:在 .NET Remoting 中,可以通过引用(MarshalByRefObject
)或指向此类对象的委托来传递对象。WCF 不支持按引用传递对象,您必须使用双工通道(使用支持它的绑定)。

客户端
我们称客户端异步远程调用是指从客户端异步调用服务,而这完全是客户端解决方案。事实上,从服务的角度来看,远程调用是同步的。因此,执行回调的工作线程在客户端启动,这完全是客户端行为。

在本文中,我只讨论这种类型的异步调用。
标准的 .NET 异步调用模型 (delegate.BeginInvoke)
我假设大家都熟悉使用 Delegate.BeginInvoke()
方法异步调用委托。事实上,如果委托指向的方法是简单对象的方法,它将在 CLR 线程池中的一个线程上被调用。但如果它是代理(WCF 或 Remoting)的方法,代理本身将提供异步功能,以利用 I/O 完成端口。
对于 .NET Remoting,真实代理是 System.Runtime.Remoting.Proxies.RemotingProxy
,它会在通道上调用 AsyncProcessMessage
。因此,调用将是异步的,调用线程会立即返回。
delegate User UserHandler(int id);
//obtaining proxy
IUserService srv = (IUserService)Activator.GetObject(typeof(IUserService), url);
//build delegate on method
UserHandler uh = new UserHandler(srv.GetUser);
//make async call
uh.BeginInvoke(id, null, null); //returns immediately,
//while operation is still in progress
WCF 中会发生什么?
在 WCF 中,真实代理的类型是 System.ServiceModel.Channels.ServiceChannelProxy
。此代理实现 **即使我们使用 BeginInvoke 调用,也会同步调用服务方法**。
delegate User UserHandler(int id);
//obtaining proxy
IUserService srv = (IUserService)new ChannelFactory<IUserService>().CreateCannel(url);
//build delegate on method
UserHandler uh = new UserHandler(srv.GetUser);
//make async call
uh.BeginInvoke(id, null, null); //blocks the calling thread for entire period of call
WCF 仅在代理上调用的方法以 BeginXXX()
开头并用 [OperationContract(AsyncPattern=true)]
属性修饰时才发出异步调用。
如果您生成代理(导入服务协定),svcutil.exe 和 VS.NET 都可以为服务中的所有方法生成这些 BeginXXX
方法,但如果您使用导出的服务协定,服务协定(由服务和客户端共享)当然不会包含这些方法,而只会包含原始服务方法。请参阅下一节。
手动重构客户端的服务接口(协定),添加 BeginXXX、EndXXX 方法签名
在使用导出的(共享的)服务协定时,您所能做的就是手动重构客户端上的接口(通过复制源代码或派生另一个接口),并为接口添加必要的 BeginXXX
、EndXXX
方法。您不需要做任何其他事情,因为在调用 BeginXXX
时,代理实际上会异步调用服务上的 XXX。
[ServiceContract]
public interface IUserService
{
[OperationContract()]
User GetUser(int id);
//Add this:
[OperationContract(AsyncPattern=true)]
IAsyncResult BeginGetUser(int id, AsyncCallback callback, object state);
User EndGetUser(IAsyncResult asyncResult);
//
}
现在您可以简单地调用 BeginGetUser
IUserService srv = (IUserService)new ChannelFactory<IUserService>().CreateCannel(url);
srv.BeginGetUser(id, null, null); //now call returns immediately,
//while operation is in progress
请注意,您甚至不需要额外的委托。
Silverlight 的区别/解决方案
Silverlight 与 .NET 相比有许多区别/不足之处,其中一点是 Silverlight **不允许同步调用任何 WCF 服务**。它们确实想避免任何开发人员调用阻塞主线程(进而阻塞浏览器)的服务,因此只允许异步调用。考虑到 **您甚至不能在 Silverlight 中使用 .NET 程序集**,使用导入的服务协定似乎是最方便的选择。
但是您仍然可以使用导出的服务协定。您只需将服务协定的源代码复制到您的 Silverlight 项目中,为所有方法添加 BeginXXX
、EndXXX
方法对,并删除原始方法。DataContracts
也可以复制或仅使用 VS.NET 的链接文件功能进行链接。
正如您所看到的,这并没有付出太大的牺牲。
使用 ServiceClient<T> 使其更轻松
现在,在 Silverlight 客户端上,我们已经有了经过重构的服务接口,其中包含所有 BeginXXX
方法,因此我们可以开始使用它们了。
然而,在每次服务调用时,我们需要:
- 获取代理的引用
- 使用
AsyncCallback
委托调用BeginXXX
并传递参数 - 为此委托提供一个处理程序
- 在处理程序中调用
EndInvoke
以获取结果
并且,由于我们在工作线程上,我们还需要: - 在可以执行请求的 UI 更新之前,将执行上下文切换到 UI 线程。
为了使我们的客户端应用程序更具可读性和结构性,我们可以将这些包装在一个类中。对于每个服务接口,我们可以有一个类来封装代理创建和返回值提取(在 UI 线程上),这样我们就无需在每次调用时都这样做。
基本思想是有一个基类 ServiceClient
,它将服务接口作为 T
泛型参数,并基于此自动在内部创建代理并将其存储为受保护的成员。它还有一个 FireEvent() protected
方法,可以在 UI 线程上调用任何委托。
public class ServiceClient<T> : IDisposable where T : class
{
protected T _proxy = null;
//This looks up endpoint configuration with the name of type T
//as the endpointname
public ServiceClient() : this(typeof(T).Name)
{
}
//This looks up endpoint configuration with given endpointName
public ServiceClient(string endpointName)
{
//WCF proxy creation.
//If u use any IOC container to get service object reference,
//you can change this line to use that
_proxy = new ChannelFactory<T>(endpointName).CreateChannel();
}
protected void FireEvent
(Delegate del, bool autoMarshallToUI, params object[] args)
{
//fire outbound event
if (!Deployment.Current.Dispatcher.CheckAccess() && autoMarshallToUI)
{
//on UI thread
Deployment.Current.Dispatcher.BeginInvoke(del, args);
}
else
{
//on current thread
del.DynamicInvoke(args);
}
}
//........
//....
}
现在,对于您的每个服务接口,您可以从该基类派生一个类,将服务接口作为 T
参数传递,并为每个服务方法编写一个 public AsyncXXX
方法和一个 private
处理程序 XXXCallback
。
AsyncXXX
方法获取参数和一个任意委托,您希望将结果传递给该委托。在其中,只需调用代理的 BeginXXX
方法,传递一个指向 XXXCallback
方法的 AsyncCallback
,并在 stateObject
中传递我们的任意委托。因此,当调用结束且 XXXCallback
被调用时,它会对代理调用 EndInvoke
以获取结果,然后使用 FireEvent
和结果调用 UI 线程上的我们的任意委托。
public class UserServiceClient : ServiceClient<IUserService>
{
public UserServiceClient()
{
}
public UserServiceClient(string endpointName) : base(endpointName)
{
}
//1. GetUser
public IAsyncResult AsyncGetUser(int id, Action<User> cb)
{
return _proxy.BeginGetUser(id, GetUserCallback, cb);
}
private void GetUserCallback(IAsyncResult ar)
{
try
{
User user = _proxy.EndGetUser(ar);
FireEvent((Delegate)ar.AsyncState, true, user);
}
catch (Exception ex)
{
//handle communication errors
HandleError(ex);
}
}
//2. GetUserByName
//....
}
在此示例中,我们使用 Action<User>
作为我们的自定义回调委托。
基类还有一个 HandleError(Exception ex)
方法,您可以在其中放置通用的错误处理逻辑。(此处未显示,但包含在附加的示例源代码中。)
这是一个完全通用的解决方案,但稍作修改即可使其更易于与您的特定应用程序配合使用。我认为最佳实践是为每个派生自 ServiceClient
的应用程序拥有自定义的基类。