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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2010年4月16日

CPOL

6分钟阅读

viewsIcon

65537

downloadIcon

669

总结 WCF/WCF Silverlight 中的客户端异步调用。介绍 ServiceClient 类。

引言

本文介绍了 WCF 和 WCF Silverlight 中使用基于导出的服务协定运行时生成的代理进行异步调用的方法,并介绍了一个 ServiceClient<T> 类,以简化异步调用。

异步操作总览

异步操作意味着实际操作正在与启动线程并行执行,启动线程在操作进行过程中可以继续执行其任务。

就远程调用而言,可以区分两种主要的异步调用类型:服务器端异步调用和客户端异步调用。

服务器端

在这种情况下,从客户端的角度来看,远程调用是完全同步的。客户端调用服务上的一个方法,该方法会正常返回(远程调用结束),因此客户端可以继续工作。但关键在于,服务器在服务方法中并未完成操作,而是启动了一个单独的线程来执行它。

这完全是一个服务器端解决方案。但是,系统需要考虑这一点进行设计,因为在远程调用结束时您不会收到结果。如果客户端也需要操作的结果,则服务应回调客户端,这需要额外的设施(此时,客户端变成服务器),可能并不简单(客户端可能需要打开监听端口,必须将对象引用/委托传递给服务:在 .NET Remoting 中,可以通过引用(MarshalByRefObject)或指向此类对象的委托来传递对象。WCF 不支持按引用传递对象,您必须使用双工通道(使用支持它的绑定)。

GeneralA.png

客户端

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

GeneralB.png

在本文中,我只讨论这种类型的异步调用。

标准的 .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 方法签名

在使用导出的(共享的)服务协定时,您所能做的就是手动重构客户端上的接口(通过复制源代码或派生另一个接口),并为接口添加必要的 BeginXXXEndXXX 方法。您不需要做任何其他事情,因为在调用 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 项目中,为所有方法添加 BeginXXXEndXXX 方法对,并删除原始方法。DataContracts 也可以复制或仅使用 VS.NET 的链接文件功能进行链接。

正如您所看到的,这并没有付出太大的牺牲。

使用 ServiceClient<T> 使其更轻松

现在,在 Silverlight 客户端上,我们已经有了经过重构的服务接口,其中包含所有 BeginXXX 方法,因此我们可以开始使用它们了。

然而,在每次服务调用时,我们需要:

  1. 获取代理的引用
  2. 使用 AsyncCallback 委托调用 BeginXXX 并传递参数
  3. 为此委托提供一个处理程序
  4. 在处理程序中调用 EndInvoke 以获取结果
    并且,由于我们在工作线程上,我们还需要:
  5. 在可以执行请求的 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 的应用程序拥有自定义的基类。

© . All rights reserved.