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

WCF 双工重入服务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (11投票s)

2009年3月25日

CPOL

7分钟阅读

viewsIcon

86249

downloadIcon

2900

本文展示了一种在 WCF 中实现双工可重入服务的实践方法。

引言

本文阐述了在 WCF 中使用并发设置为“可重入”的双工模式。通常,双工是三种消息交换模式 (MEP) 之一,它们是:

  • 单向
  • 请求-响应(同步和异步)
  • 双工

另一方面,WCF 服务的并发性处理的是有多少个并发线程可以同时访问这些服务。

本文的目的并非全面讨论 WCF 中的 MEP 和并发性;事实上,这应该是一本书的内容。因此,本文假定您熟悉(但不一定有经验)以下内容:

  • WCF 编程
  • WCF 中的 MEP
  • WCF 中的并发模式
  • 线程的基本概念

场景

本文的示例模拟了一个场景:客户端应用程序向服务发送注册请求,以获取低温下降的通知。之后,服务将以双工模式响应客户端,这意味着客户端不会等待响应,而是服务将通过回调通知客户端温度下降的情况。

构建双工服务

服务在 Windows 应用程序中自托管。这是一个使用 `wsDualHttpBinding` 的双工服务。服务代码如下所示:

[ServiceContract(CallbackContract=typeof(IClientCallback))]
public interface ITemperature
{
    [OperationContract(IsOneWay=true)]
    void RegisterForTempDrops();
}

public interface IClientCallback
{
    [OperationContract(IsOneWay = true)]
    void TempUpdate(double temp);
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
                 ConcurrencyMode = ConcurrencyMode.Single)]
public class Temperature : ITemperature
{
    public void RegisterForTempDrops()
    {
        OperationContext ctxt = OperationContext.Current;
        IClientCallback callBack = ctxt.GetCallbackChannel<iclientcallback>();
        Thread.Sleep(3000); //simulate update happens somewhere;
                            //for example monitoring a database field
        callBack.TempUpdate(10);
    }
}

让我们来检查代码

  • ITemperature”是服务契约,“IClientCallback”是客户端的回调接口。客户端想要了解温度下降的操作“RegisterForTempDrops”将被客户端使用;另一方面,这些客户端必须实现回调接口,以便服务能够调用其“TempUpdate”方法进行通知。请注意,“RegisterForTempDrops”和“TempUpdate”都是单向操作。
  • ServiceBehavior 特性定义了 PerSession 实例模式和 Single 并发模式。这些是 WCF 的默认设置,但显式编写代码始终是更好的做法。一瞥之下,PerSession 实例模式意味着每个客户端将在第一次调用时获得自己的服务实例,并且该实例将继续为客户端提供服务,直到代理被显式关闭或会话超时。另一方面,Single 并发模式意味着一次只有一个线程可以访问服务实例;因此,如果客户端尝试向分配给它的服务实例发出多个并发调用,这些调用将被排队并一次处理一个,因为服务(ConcurrencyMode.Single)已禁用多线程。
  • Temperature”类实现了“RegisterForTempDrops”方法。此方法回调到客户端并通过“TempUpdate”方法以双工模式发送通知。

服务的双工配置如下所示:

6.JPG

在这里,我们使用 wsDualHttpBinding 进行双工操作。还公开了元数据交换终结点 (MEX),它允许客户端通过 Visual Studio 使用“添加服务引用”选项。

最后,启动自托管服务的代码

internal static ServiceHost myServiceHost = null;
private void button1_Click(object sender, EventArgs e)
{
    myServiceHost = new ServiceHost(typeof(Temperature));
    myServiceHost.Open();
    MessageBox.Show("Service Started!");
}

private void button2_Click(object sender, EventArgs e)
{
    if (myServiceHost.State != CommunicationState.Closed)
    {
        myServiceHost.Close();
        MessageBox.Show("Service Stopped!");
    }
}

构建双工客户端

客户端是一个控制台应用程序,它消耗该服务并通过实现回调接口来接收结果。第一步是向 WCF 服务添加服务引用。首先,您必须通过运行 Windows 应用程序并单击“开始”按钮来启动服务。接下来,在客户端,使用 VS 添加服务引用,如下所示(请注意使用服务配置中指定的基址;这得益于 MEX 终结点):

1.JPG

客户端代码如下所示:

public class CallBackHandler : ITemperatureCallback
{
    static InstanceContext site = new InstanceContext(new CallBackHandler());
    static TemperatureClient proxy = new TemperatureClient(site);

    public void TempUpdate(double temp)
    {
        Console.WriteLine("Temp dropped to {0}", temp);
    }

    class Program
    {
        static void Main(string[] args)
        {
            proxy.RegisterForTempDrops();

            Console.ReadLine();

        }
    }
}

让我们来检查代码

  • 为了让客户端能够参与双工对话,它需要有自己的终结点供服务回调。这段代码是通过“添加服务引用”选项自动生成的。
  • 请注意,客户端类“CallBackHandler”是如何实现服务中定义的类型“ITemperatureCallback”的;这将启用双工通信。
  • InstanceContext”保存有关服务实例的上下文信息;客户端使用此上下文来创建代理。这与使用非双工服务不同,后者不需要“InstanceContext”。在这种情况下,“InstanceContext”保存了客户端为参与双工操作而自动创建的通道的引用。
  • 使用代理,客户端调用“RegisterForTempDrops”服务方法。
  • 然后,客户端将从服务接收对“TempUpdate”方法的调用。

运行示例

在服务应用程序运行的情况下,创建客户端控制台应用程序的新实例,并注意到三秒钟后(模拟温度下降事件的时间),客户端应用程序将显示一条通知消息,如下所示:

2.JPG

示例架构

现在,让我们看看刚才发生的事情的简单架构图。下图显示了服务和客户端之间的双工通信:

3.JPG

  1. 客户端使用服务的单向操作“RegisterForTempDrops”;单向操作不会返回响应。
  2. 服务处理请求,然后以单向操作“TempUpdate”回调到客户端。双工通信已结束;就是这么简单!

但是,请记住,服务行为被定义为单并发模式;这意味着一次只有一个线程可以访问服务实例。好的,那么现在您能想象如果“TempUpdate”方法不是单向的会发生什么吗?假设“TempUpdate”方法应该向服务返回一个布尔值,以指示它已成功收到通知。让我们看看更新后的架构图,以了解这种情况:

4.JPG

  1. 同样,客户端调用单向操作“RegisterForTempDrops”(线程 A)。
  2. 此外,服务处理请求,并调用客户端上的“TempUpdate”方法。
  3. 现在最大的区别是“TempUpdate”不再是单向操作;相反,它向服务返回一个布尔值。因此,现在客户端正试图调用服务以返回“TempUpdate”方法的返回值。这将发生在另一个线程 B 上。但是,请记住,线程 A 仍然“占用”着服务实例,该实例的并发模式设置为 Single,因此它一次只能服务一个线程。结果,线程 B 将被卡住,您将陷入死锁状态,其中线程 B 正在争夺资源(服务实例);尽管如此,该资源仍被线程 A 占用,而线程 A 不愿释放,仅仅因为从线程 A 的角度来看,通信周期尚未完成。

想亲眼看看?请继续阅读。

修改示例

为了看到上述场景的实际效果,请更改回调操作并将其返回类型设置为 bool,而不是 void,并删除单向标记。服务代码现在显示如下:

[ServiceContract(CallbackContract=typeof(IClientCallback))]
public interface ITemperature
{
    [OperationContract(IsOneWay=true)]
    void RegisterForTempDrops();
}

public interface IClientCallback
{
    //[OperationContract(IsOneWay = true)]
    //void TempUpdate(double temp);

    [OperationContract]
    bool TempUpdate(double temp);
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
                 ConcurrencyMode = ConcurrencyMode.Single)]
public class Temperature : ITemperature
{
    public void RegisterForTempDrops()
    {
        OperationContext ctxt = OperationContext.Current;
        IClientCallback callBack = ctxt.GetCallbackChannel<IClientCallback>();
        Thread.Sleep(3000); //simulate update happens somewhere; 
                            //for example monitoring a database field
        callBack.TempUpdate(10);
    }
}

启动服务并在客户端更新服务引用。唯一的更改是从“TempUpdate”方法返回布尔值。客户端代码如下所示:

public class CallBackHandler : ITemperatureCallback
{
    static InstanceContext site = new InstanceContext(new CallBackHandler());
    static TemperatureClient proxy = new TemperatureClient(site);

    public bool TempUpdate(double temp)
    {
        Console.WriteLine("Temp dropped to {0}", temp);
        return true;
    }

    class Program
    {
        static void Main(string[] args)
        {
            proxy.RegisterForTempDrops();

            Console.ReadLine();

        }
    }
}

现在,运行客户端,您会发现没有任何响应。您实际上将进入死锁状态。那么,我们如何解决这个问题呢?

可重入并发

解决方案实际上非常简单:只需将 ServiceBehavior 特性的 ConcurrencyMode 值设置为 Reentrant 而不是 Single

为了理解 Reentrant 模式,请考虑下图:

5.JPG

当客户端调用服务时,调用线程(A)会用一个标记标记,我们称之为“M”。现在,服务响应客户端,客户端处理请求,并将布尔结果发回给服务,这发生在线程(B)上,该线程也用标记“M”标记。WCF 检查传入的服务线程是否具有与第一个线程相同的标记,然后这意味着传入的调用是对已发出的调用的响应,因此,该调用被接受。

所以总而言之,我们仍然是单线程的,但增加了允许此特定调用回传的功能;否则,我们将遇到死锁。这就是带有可重入功能的单线程!

为了看到这个实际效果,只需将 ServiceBehaviorConcurrencyMode 改为 Reentrant 而不是 Single。如下所示:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, 
                 ConcurrencyMode = ConcurrencyMode.Reentrant)]

这次,客户端调用成功,并且完整的周期在没有死锁的情况下完成。

示例程序

您可以在文章开头下载服务和客户端应用程序。

© . All rights reserved.