在 Windows Forms 应用程序中托管 WCF 服务
演示如何在 Windows Forms 应用程序中托管多个单例服务。
引言
本文讨论了 WCF 服务模型的一些特性,该模型是 .NET 3.0 的一部分。文章演示了如何在 Windows Forms 应用程序中托管同一服务和契约的多个实例。这些服务随后被属于同一应用程序的客户端窗体以及由另一个应用程序创建的同一客户端窗体所使用。它还演示了如何在不使用任何配置文件的情况下创建并使用单例服务。
背景
因为我当时在国外,无法随身携带我的模型火车控制站,所以我决定编写一个简单的模拟器,以便我可以继续进行我正在设计的、用于驱动该控制站的更大型的软件开发工作。
首先,我必须为在轨道上运行且可由控制站驾驶的机车创建一个简单的模拟。由于我最近参加了 WCF 培训,我认为使用新的 WCF 服务模型来编写这部分会是一个很好的实践。几年前,我可能会写一些 COM 服务器来模拟机车,但技术已经发展,为 .NET 3.0 开发的新服务模型远比 COM 强大。
实际上,我想模拟的是机车 DCC 解码器。这个小硬件设备用于通过其地址来控制轨道上的机车。控制站发送命令来改变速度、方向和开关灯光。它还可以通过地址读取设备的状态。这可以很容易地用一个单例服务来模拟。不存在可伸缩性问题,因为这些机车将在单台机器上运行,它们的数量不会很大,对特定机车的连接数也不会很多。起初,我想为服务使用一个 Sharable
实例,但这种实例模式在 WCF 的最新版本中已经消失了……
使用代码:服务契约
下面是模拟一个简单机车解码器的接口的样子:
[ServiceContract]
public interface IDCCLocomotiveContract
{
/// Gets the running direction of that locomotive
/// </summary>
/// <returns></returns>
[OperationContract(IsOneWay=false)]
Direction GetDirection();
/// <summary>
/// Changes the direction of that locomotive
/// </summary>
/// <param name="direction">New direction</param>
[OperationContract]
void ChangeDirection(Direction direction);
/// <summary>
/// Sets the new speed value
/// </summary>
/// <param name="speed">Speed value (0 - 28)</param>
[OperationContract]
void SetSpeed(byte speed);
/// <summary>
/// Gets the current locomotive speed
/// </summary>
/// <returns>Speed value</returns>
[OperationContract(IsOneWay=false)]
byte GetSpeed();
/// <summary>
/// Switch the main light
/// </summary>
/// <param name="state">ON if true, OFF if false</param>
[OperationContract]
void SwitchLight(bool state);
/// <summary>
/// Gets the main light status
/// </summary>
/// <returns>true if ON, false otherwise</returns>
[OperationContract(IsOneWay=false)]
bool GetLightState();
}
服务实现
WCF 允许仅通过为实现服务的类使用一个特性参数来管理实例行为。有三种不同的实例模式。当使用 PerCall
模式时,每次调用服务,你都会得到一个全新的实例。这意味着所有数据在调用之间都会丢失。PerSession
模式在每次调用之间维持一个会话,直到代理被释放。PerSession
是默认模式,所以你不需要指定它。最后,Single
模式会保持同一个服务实例,直到服务器本身被关闭。请注意,在 WCF 的早期版本(May CTP 及更早版本)中,PerCall
是默认模式,并且曾有一个现在已不存在的 Sharable
模式。
下面是这个简单服务的实现方式:
/// <summary>
/// Implements the IDCCLocomotiveContract
/// </summary>
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
class DCCLocomotiveService : IDCCLocomotiveContract, IDisposable
{
const byte
MaxSpeed = 28,
MinSpeed = 0;
protected byte m_speed = 0;
protected bool m_light = false;
protected Direction m_direction = Direction.Forward;
public Direction GetDirection()
{
return m_direction;
}
public void ChangeDirection(Direction direction)
{
m_direction = direction;
}
public void SetSpeed(byte speed)
{
if (speed >= MinSpeed && speed <= MaxSpeed)
m_speed = speed;
}
public byte GetSpeed()
{
return m_speed;
}
public void SwitchLight(bool state)
{
m_light = state;
}
public bool GetLightState()
{
return m_light;
}
}
演示应用程序
我创建了两个简单的应用程序。“机车工厂”的行为就像你把机车放到轨道上一样,“机车监视器”则允许观察机车的参数。“机车工厂”是一个窗口应用程序,为每台机车启动一个服务器。每个服务器都获得一个与机车地址相关的地址。它有一个用于机车契约的端点。我选择这种实现方式,是因为我希望能够像从轨道上移除机车一样停止服务器。为了测试,我让工厂可以通过一个控制对话框来控制机车。起初,我想为这些机车使用一个服务和多个端点,但这是不可能的,因为所有端点必须在服务主机启动之前创建。
我使用一个 XML 文件来包含可用机车的列表。基本上,我使用了地址和名称。根据该文件创建了一个复选框列表。当你选中一个元素时,它会启动机车服务器并为其创建端点。当你取消选中该元素时,它会停止服务器。
“机车工厂”的主用户界面显示在文章的开头。
一个按钮允许从一个对话框向给定的机车发送命令。我就是在这里发现了一个奇怪的行为。在我的初稿中,我是在与应用程序相同的线程中创建 ServiceHost
实例。当我在对话框中调用从 ChannelFactory
获得的 IDCCLocomotiveContract
实例上的一个方法时,它会因超时而失败。然而,从另一个应用程序中使用相同的 ChannelFactory
进行相同的调用却能正常工作。
托管服务
我设计了一个简单的类,它在不同于应用程序线程的另一个线程中创建 ServiceHost
实例。似乎 Windows Forms 应用程序由于窗口消息机制引入了一些限制,我猜想这种消息机制与 WCF 使用的消息机制之间存在一些干扰。这个类创建了一个 Thread
来为我的契约启动 ServiceHost
,并等待应用程序停止它。它使用一个简单的模式来停止线程,即用一个布尔值触发线程方法的结束。线程不应该使用 Abort
方法来停止,因为在我们的情况下,它必须关闭 ServiceHost
。调用 Abort
会让 ServiceHost
保持打开状态。
class ThreadedServiceHost<TService, TContract>
{
const int SleepTime = 100;
private ServiceHost m_serviceHost = null;
private Thread m_thread;
private string
m_serviceAddress,
m_endpointAddress;
private bool m_running;
private Binding m_binding;
public ThreadedServiceHost(
string serviceAddress,
string endpointAddress, Binding binding)
{
m_binding = binding;
m_serviceAddress = serviceAddress;
m_endpointAddress = endpointAddress;
m_thread = new Thread(new ThreadStart(ThreadMethod));
m_thread.Start();
}
void ThreadMethod()
{
try
{
m_running = true;
// Start the host
m_serviceHost = new ServiceHost(
typeof(TService),
new Uri(m_serviceAddress));
m_serviceHost.AddServiceEndpoint(
typeof(TContract),
m_binding,
m_endpointAddress);
m_serviceHost.Open();
while (m_running)
{
// Wait until thread is stopped
Thread.Sleep(SleepTime);
}
// Stop the host
m_serviceHost.Close();
}
catch (Exception)
{
if (m_serviceHost != null)
{
m_serviceHost.Close();
}
}
}
/// <summary>
/// Request the end of the thread method.
/// </summary>
public void Stop()
{
lock (this)
{
m_running = false;
}
}
}
服务的代理
我的应用程序一个有趣的问题是,我预先不知道机车的数量和它们的地址,所以我在构建中没有使用任何 App.Config 文件。我也没有使用任何代理,而是使用了 ChannelFactory
,它允许动态地创建一个代理。我使用的是 NetTcpBinding
,这对于本地服务来说是合适的,并且最终可以在 Windows 分布式环境中使用。ChannelFactory
接受一个从你的服务契约和 IChannel
接口派生的接口。然后它会动态地为你的服务创建一个代理,你可以像使用你的契约接口一样使用它。
下面是我用来为我的服务创建代理的代码:
// Creates the corresponding endpoint
EndpointAddress endPoint = new EndpointAddress(
new Uri(string.Format(Constants.LocoServerBaseAddress,
address) + address));
// Creates the proper binding
System.ServiceModel.Channels.Binding binding = new NetTcpBinding();
// Creates channel factory with the binding and endpoint
m_dccLocoFactory = new ChannelFactory(binding, endPoint);
m_dccLocoFactory.Open();
// Creates the channel and opens it to work with the service
m_locoChannel = m_dccLocoFactory.CreateChannel();
m_locoChannel.Open();
使用 ChannelFactory
创建此代理的接口如下:
///<summary>
/// Interface used to create the Proxy for IDCCLocomotiveContract
///</summary>
interface IDCCLocomotiveChannel : IDCCLocomotiveContract, IClientChannel
{
}
ChannelFactory
在一个对话框中使用,该对话框可以配置为向机车服务发送命令或观察不同的参数。
这里是该对话框两个版本的截图,它们实际上是由相同的代码实现的:
机车控制是“机车工厂”的一个子窗体,而机车监视器是“机车监视器”应用程序的子窗体,后者是一个简单的应用程序,根据给定的地址连接到机车服务。
就像一个真实的机车解码器一样,机车服务不提供任何通知。机车解码器是一个被动设备,不能自行向控制站发送信息。为了在不同的客户端中反映变化,每个客户端必须管理自己的轮询。
关注点
你可以深入研究这个简单应用程序的完整代码,我希望它能给你一些关于如何使用 WCF 新服务模型的想法。当然,这只是对这个非常强大的面向服务应用程序(SOA)框架的惊鸿一瞥。WCF 是一个非常完整的框架,用于在微软世界中开发基于 SOA 的应用程序。然而,由于它基于像 WS-* 这样的开放标准,应用程序可以被设计成与遵循这些标准的其他应用程序互操作,无论实现它的技术是什么。
尽管这只是一个关于 WCF 能做什么的简单演示,但它应该能帮助那些正在探索这个新服务模型的人。在做的过程中,我发现了一些有趣的要点,比如在 Forms 应用程序中托管服务需要为服务使用一个特定的线程。我还发现,如果你需要启动和停止同一服务的单个实例,你不能使用多个端点,而必须为你的每个实例使用一个主机。
另一个有趣的要点是动态创建主机和代理,因为它们的地址依赖于一个列表,并且是预先未知的。