WCF 双工重入服务






4.64/5 (11投票s)
本文展示了一种在 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
”方法以双工模式发送通知。
服务的双工配置如下所示:
在这里,我们使用 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 终结点):
客户端代码如下所示:
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
”方法的调用。
运行示例
在服务应用程序运行的情况下,创建客户端控制台应用程序的新实例,并注意到三秒钟后(模拟温度下降事件的时间),客户端应用程序将显示一条通知消息,如下所示:
示例架构
现在,让我们看看刚才发生的事情的简单架构图。下图显示了服务和客户端之间的双工通信:
- 客户端使用服务的单向操作“
RegisterForTempDrops
”;单向操作不会返回响应。 - 服务处理请求,然后以单向操作“
TempUpdate
”回调到客户端。双工通信已结束;就是这么简单!
但是,请记住,服务行为被定义为单并发模式;这意味着一次只有一个线程可以访问服务实例。好的,那么现在您能想象如果“TempUpdate”方法不是单向的会发生什么吗?假设“TempUpdate”方法应该向服务返回一个布尔值,以指示它已成功收到通知。让我们看看更新后的架构图,以了解这种情况:
- 同样,客户端调用单向操作“
RegisterForTempDrops
”(线程 A)。 - 此外,服务处理请求,并调用客户端上的“
TempUpdate
”方法。 - 现在最大的区别是“
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
模式,请考虑下图:
当客户端调用服务时,调用线程(A)会用一个标记标记,我们称之为“M”。现在,服务响应客户端,客户端处理请求,并将布尔结果发回给服务,这发生在线程(B)上,该线程也用标记“M”标记。WCF 检查传入的服务线程是否具有与第一个线程相同的标记,然后这意味着传入的调用是对已发出的调用的响应,因此,该调用被接受。
所以总而言之,我们仍然是单线程的,但增加了允许此特定调用回传的功能;否则,我们将遇到死锁。这就是带有可重入功能的单线程!
为了看到这个实际效果,只需将 ServiceBehavior
的 ConcurrencyMode
改为 Reentrant
而不是 Single
。如下所示:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession,
ConcurrencyMode = ConcurrencyMode.Reentrant)]
这次,客户端调用成功,并且完整的周期在没有死锁的情况下完成。
示例程序
您可以在文章开头下载服务和客户端应用程序。