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

通用 Windows 服务: 作为控制台应用程序调试, 并通过系统托盘进行管理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (43投票s)

2014年3月11日

CPOL

7分钟阅读

viewsIcon

50094

downloadIcon

2935

Windows 服务的通用解决方案:作为控制台应用程序运行以进行调试,可自行安装和卸载,提供描述、消息队列和处理、状态观察以及通过系统托盘图标进行控制。该代码几乎可以重用于任何 Windows 服务。

引言

.NET Windows 服务是最受欢迎的 Windows 项目类型之一。然而,Visual Studio 为其提供的模板并不方便。在我看来,该模板的主要问题在于生成的代码无法作为控制台应用程序运行以方便调试。但这并非唯一的问题。处理 Windows 服务时,开发人员通常需要实现比 Visual Studio 模板提供的更合适的线程模型。此外,如果能通过系统托盘中的图标和弹出菜单来管理服务并查看其当前状态,那就更好了。本文和代码将解决所有这些问题。

背景

如上所述,使用 Windows 服务的一个好做法是服务本身与其在系统托盘中的控件协同工作。由于 Windows 服务没有视觉方面(实际上甚至在与普通应用程序不同的桌面环境中运行),因此另一个应用程序应该确保在系统托盘中正确指示服务的当前状态。在我们的示例中,这个额外的应用程序是用 WPF 编写的。通常,这个额外的“托盘目标”应用程序与 Windows 服务在同一台机器上运行。因此,使用 IPC(net.pipe)绑定的双工 WCF 似乎是这两个进程之间通信的自然选择,其中 Windows 服务充当服务器。

许多开发人员(包括我)特别喜欢能够轻松重用用于解决实际问题的代码。对于大多数实际情况,本文提供的整个代码几乎可以按原样使用(只更改文本变量和模块名称),只需在少数几个地方插入特定的 Windows 服务功能。SampleWinService 项目中所有可能需要更改的地方都位于标记为“DIFFERS FOR EACH SERVICE”(因服务而异)的相应区域。

托盘目标应用程序的用户界面提供适当的图标,指示服务的当前状态,以及一个弹出菜单,通过右键单击图标激活。

  • 服务未运行


  • 服务正在运行

请注意,指示当前服务状态的图标在服务状态更改后会有一定的延迟更新。

设计

Windows 服务 SampleWinService 具有以下特点:

  • 能够作为控制台应用程序或 Windows 服务运行;

  • 作为 Windows 服务使用时,支持自安装和自卸载;

  • 通过托管 WCF 双工服务对象,作为“有用”客户端和 WPF 托盘目标应用程序的服务器;

  • 提供基于命令的工作范式,操作如下。客户端应用程序通过 WCF 通信发送命令。命令由 WCF 服务对象排队到命令队列。命令由一个专用的工作线程循环处理。处理结果(如果需要)将发送给调用的有用客户端,而当前 Windows 服务状态将作为回调通过 WCF 双工机制发送给 WPF 托盘目标应用程序。双工 WCF 在有用客户端数量不多且它们位于服务机器的同一局域网内时特别有用。通常,托盘目标应用程序与 Windows 服务在同一台机器上运行。因此,在这种情况下,使用 IPC(net.pipe)绑定的双工 WCF 也是通信的一个好选择。

在我们的示例中,外部应用程序通过发送命令来促使服务执行有用的工作。在实际生活中,也可能存在其他有用的命令来源,例如来自硬件、驱动程序、计时器的输入等。

我们的 SampleWinService 的设计基于以下合理的假设:

  • 托盘目标 GUI 应用程序与服务在同一台机器上运行;

  • Windows 服务的客户端数量有限;

  • 服务与其所有客户端之间没有防火墙。

第一个假设允许托盘目标应用程序使用 System.Management.ManagementEventWatcher 类提供的关于服务状态变化的异步通知。第二个和第三个假设证明了使用双工 WCF 服务的合理性。当然,上述假设并非强制性的,仅为简化起见。我们将在本文的“讨论”章节中简要回顾其他情况。

系统的框图如下所示:

Windows 服务

Windows 服务包含 WCF 服务对象,该对象接受来自有用客户端和发送命令给服务的托盘目标客户端的调用。WCF 部分实现在 Libraries\ServiceLibs 文件夹中的 CommunicationSvc 项目。接口 ICommSvc 允许客户端向服务发送 Cmd 类型的命令。该接口由 CommSvc 类型实现。Cmd 类包含命令名称和参数,以及发送命令的客户端信息。ICallback 接口构成了客户端回调的契约。为了方便客户端,WCF 服务支持 CommunicationSvc.ICommSvc 合约的三个端点,分别使用 netNamedPipeBindingnetTcpBindingwsDualHttpBinding 绑定。

由于根据上述假设,我们的 Windows 服务客户端不多,并且客户端可以进行回叫通知,因此服务可以在独立的后台线程中处理客户端命令,该线程循环运行。CommunicationSvc 项目提供了一个单例类 CommandQueue<T>,用于同步命令队列,该队列以 Cmd 命令类型为参数。CommSvc.Command() 方法将传入的命令排队以便处理。命令的入队和处理在主要的 Windows 服务进程 SampleWinService 中进行。WorkerThread.StartThread() 中的 PeriodicProcessingDelegate 的匿名方法将命令出队,并使用静态方法 CommandProcessor.Process() 进行处理。该方法负责实际处理命令。它是服务中唯一根据给定 Windows 服务的实际任务而变化的部分。处理完成后,类 SampleWinSvc 的 SendStatusToAllClients()SendReport() 方法将处理的状态和结果作为异步回调发送给客户端。

客户端

托盘目标和有用客户端都使用 WCF 的客户端部分(代理)。为了在不同类型的客户端之间共享相同的代码,它被放在一个单独的程序集 ClientLib 中。此外,托盘目标客户端使用 Philipp Sumi ([1], www.hardcodet.net) 开发的特定 NotifyIconWpf 程序集,该程序集为 WPF 应用程序提供托盘图标和弹出菜单的基础结构。托盘目标和有用客户端都实现了 ICommSvcCallback 接口(对应服务器端的 ICallback 接口),以接收来自服务的通知调用。有趣的是,在某些情况下,托盘目标 GUI 应用程序甚至可以替代 Windows 服务,例如,当您需要运行该应用程序来截屏(Windows 服务在不同桌面环境中运行无法实现)或仅仅为了简化非关键任务时。

代码示例

在我们的示例中,Windows 服务由 SimpleWinService 项目表示,而 IconClientTestClient 项目分别代表托盘目标客户端和有用客户端。

演示

要运行演示,我们必须执行 SimpleWinService Windows 服务的自注册。为此,请运行文件 SimpleWinService_Install.cmd(它以参数 /install 启动 SimpleWinService.exe)。然后启动 IconClient.exe,并使用托盘图标及其关联的弹出菜单来控制 SimpleWinService Windows 服务并观察其状态。

讨论

上述设计适用于许多实际的 Windows 服务使用场景。如果服务与其客户端之间存在防火墙,则可以使用双工 WCF 以外的其他方法,例如“智能轮询”技术(例如在 [2] 中描述),其中服务器在可等待对象上挂起客户端的可重复轮询调用,直到发送关于服务器事件的通知给客户端或超时发生。更复杂的报文队列和处理机制可以从 [3] 等处获取。

结论

本文通过提供一种统一的技术解决了 Windows 服务通用设计的问题,该技术实现了广泛期望的功能,例如作为控制台应用程序运行以方便调试、自安装和自卸载、提供描述、消息队列和处理,以及通过放在系统托盘中的图标和弹出菜单进行服务状态观察和控制。本文附带的代码几乎可以按原样重用(只需在标记有特定区域的少数几个地方进行少量更改),作为几乎任何 Windows 服务的模板。我希望我的 CodeProject 同仁在开发 Windows 服务时会觉得它很有用。

参考文献

[1] Philipp Sumi. WPF NotifyIcon. CodeProject.
[2] Igor Ladnik. Push Messages in RESTful WCF Web Application with Comet Approach. CodeProject.
[3] Igor Ladnik. Tiny Framework for Parallel Computing. CodeProject.

© . All rights reserved.