译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 39 章 Windows 服务(上)),不对的地方欢迎指出与交流。
章节出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位阅读时仔细分辨,唯望莫误人子弟。
附英文版原文:Professional C# 6 and .NET Core 1.0 - Chapter 39 Windows Services
-----------------------------------------------------
本章主要内容
- Windows服务的体系结构
- 创建Windows服务程序
- Windows服务安装程序
- Windows服务控制程序
- 疑难解答Windows服务
Wrox.com网站中本章源代码下载
本章的wrox.com代码下载位于 www.wrox.com/go/professionalcsharp6 下载代码选项卡。代码在"Chapter 39",以下名称的项目贯穿整个章节。
- Quote Server
- Quote Client
- Quote Service
- Service Control
什么是Windows服务?
Windows服务是可以在开机时自动启动的程序,而无须任何人登录到计算机。如果需要在没有用户交互的情况下启动程序,或者在不是交互式用户的用户下运行程序 - 这种用户可能需要更多的权限,则可以创建Windows服务。一些示例可能是WCF 主宿程序(如果由于某种原因不能使用Internet信息服务(IIS)),这种程序可以从网络服务器获取缓存数据,或者在后台重新组织本地磁盘数据。
本章从查看Windows服务的架构开始,创建托管网络服务器的Windows服务,并提供有关启动、监视、控制和解决Windows服务故障的信息。
如上所述,Windows服务是可以在操作系统引导时自动启动的应用程序。这些应用程序可以在没有交互式用户登录到系统的情况下运行,并且可以在后台进行一些处理。
例如,在Windows Server上,应该可以从客户端访问系统网络服务,而无需用户登录到服务器;在客户端系统上,Windows服务使您能够执行诸如获取在线新软件版本或在本地磁盘上清除文件等操作。
可以将Windows服务配置为从特殊配置的用户帐户或系统用户帐户下运行 - 该用户帐户需具有比系统管理员更多的特权。
注意 除非另有说明,提到服务时,指的是Windows服务。
以下是几个Windows服务的例子:
- Simple TCP/IP服务是一种承载一些小型TCP/IP服务器的服务程序:echo,daytime,quote及其他。
- 万维网发布服务是 IIS的一种服务。
- 事件日志是将消息记录到事件日志系统的服务。
- Windows搜索是一种在磁盘上创建数据索引的服务。
- 超级预取是将常用的应用程序和库预装载到内存中的服务,从而提高这些应用程序的启动时间。
可以使用服务管理工具(如图39.1所示)查看系统上的所有服务。通过在“开始”菜单输入“Services”(译者注:如果Services无响应,可尝试输入"services.msc")访问该程序。
图39.1
注意 不能使用.NET Core创建Windows服务,必须要.NET Framework才可以。但控制Windows服务可以使用.NET Core。
Windows服务架构
操作Windows服务需要三种类型的程序:
- 服务程序
- 服务控制程序
- 服务配置程序
服务程序是服务的实现。利用服务控制程序,可以向服务发送控制请求,例如开始、停止、暂停和继续。通过服务配置程序,可以安装服务:把服务程序复制到文件系统,同时将有关服务的信息写入注册表。此注册表信息由服务控制管理器(SCM)用于启动和停止服务。.NET组件可以简单地使用xcopy安装,那是因为它们不需要将信息写入注册表 - 但安装服务需要配置注册表。也可以使用服务配置程序稍后更改该服务的配置。 Windows服务的三个组成部分将在以下小节中讨论。
服务程序
为了大体了解 .NET实现的服务,本节从总体上简要介绍服务的Windows体系结构以及服务的内部功能。
服务程序实现服务的功能需要三个部分:
- 主函数
- 主服务函数
- 处理事件
在讨论这些部分之前,有必要暂时岔开主题去简单介绍SCM,它在向服务发送启动和停止的请求中起了重要作用。
服务控制管理器
SCM是操作系统中服务通信的一部分。序列图39.2说明了通信的工作原理。
图39.2
开机时会启动所有设置为自动启动服务的进程,因此该进程的主函数会被调用。Windows服务负责为其每个服务注册主服务函数。主函数是服务程序的入口点,在该功能中,主服务函数的入口点service-main在SCM中注册。
主函数,主服务和处理事件
服务的主函数Main方法是程序的普遍入口点。服务的主函数可能注册多个主服务函数。 service-main函数包含服务的实际功能,必须为提供的每个服务注册一个service-main函数。服务程序可以在单个程序中提供大量服务;例如,<windows>system32services.exe是包括 Alerter,应用程序管理,计算机浏览器和DHCP客户端等服务程序。
SCM为每个要启动的服务调用service-main函数。service-main函数的一个重要任务是向SCM注册处理事件。
处理事件是服务程序的第三部分。处理事件必须响应来自SCM的事件。服务可以是停止,挂起和恢复事件,但处理事件必须对这些事件做出反应。
SCM注册处理事件之后,服务控制程序可以向SCM发布请求去停止,暂停和恢复服务。服务控制程序独立于SCM和服务本身。操作系统包含许多服务控制程序,例如图39.1中所示的Microsoft管理控制台(MMC)服务管理单元。也可以编写自己的服务控制程序,一个很好的例子是图39.3中所示的SQL Server配置管理器,它在MMC下运行。
图39.3
服务控制程序
顾名思义,通过服务控制程序,可以停止,挂起和恢复服务。通过服务控制程序可以向服务发送控制代码,处理事件会对这些事件做出反应。还可以向服务询问其实际状态(如果服务正在运行或暂停,或处于某种故障状态),并实现响应自定义控制代码的自定义处理事件。
服务配置程序
由于必须在注册表中配置服务,因此不能用xcopy去安装服务。注册表包含服务的启动类型,启动类型可以设置为自动、手动或禁用。还需要配置服务程序的用户和服务的依赖项,例如,在当前服务启动之前必须全部启动的服务。所有这些配置都在服务配置程序中完成。安装程序可以使用服务配置程序来配置服务,同时该程序也可以稍后用于更改服务配置参数。
Windows服务类
在.NET Framework中,可以在System.ServiceProcess命名空间中找到实现服务的三个服务类:
- 必须继承ServiceBase类才能实现服务。 ServiceBase类用于注册服务和回应启动和停止的请求。
- ServiceController类用于实现服务控制程序。使用该类可以向服务发送请求。
- ServiceProcessInstaller和ServiceInstaller类,顾名思义,是用来安装和配置服务程序的类。
至此已准备好去创建一个新的服务了。
创建Windows服务程序
本章中创建的服务托管于引用服务器(引用服务器 原文是 quote server)。随着从客户端发出的每个请求,引用服务器从引用文件返回随机引用。解决方案的第一部分使用三个程序集:一个用于客户端,两个用于服务器。图39.4提供了解决方案的概览。QuoteServert程序集保存实际的功能。该服务读取内存缓存中的引用文件,并在套接字服务器的帮助下解答引用请求。 QuoteClient是一个WPF富客户端应用程序。此应用程序创建一个客户端套接字与QuoteServer通信。第三个程序集是实际的服务, QuoteService启动和停止QuoteServer,即控制服务器。
图39.4
创建程序的服务部分之前,在一个额外的C#类库中创建一个简单的套接字服务器,该库将在服务过程中使用。接下来将会讨论如何做到这一点。
为服务创建核心功能
在Windows服务中可以创建任何功能,例如扫描文件以执行备份或检查病毒或启动WCF服务器。但是,所有服务程序都有一些相似之处。程序必须能够启动(并返回调用句柄)、停止和挂起。本节使用socket server 来查看这种实现。
Windows 10中Simple TCP/IP服务可以作为Windows组件的其中一部分去安装。Simple TCP/IP服务的一部分是“一天的引用”或当天引用(当天引用 原文 qotd , 网上有解释为 quotation of the day),TCP/IP服务器。这个简单的服务侦听端口17,并用来自文件<windows>system32driversetcquotes的随机消息来回答每个请求。示例服务将创建类似的服务器。示例服务器返回一个Unicode字符串,在qotd服务器则相反,它返回一个ASCII字符串。
首先,创建一个名为QuoteServer的类库,并实现服务器的代码。以下在源代码文件QuoteServer.cs中的QuoteServer类:(代码文件QuoteServer/QuoteServer.cs):
using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace Wrox.ProCSharp.WinServices { public class QuoteServer { private TcpListener _listener; private int _port; private string _filename; private List<string> _quotes; private Random _random; private Task _listenerTask;
构造函数QuoteServer被重载以便文件名和端口可以传递调用。只传递文件名的构造函数使用服务器的默认端口7890。默认构造函数将引用的文件名默认定义为quotes.txt:
public QuoteServer() : this ("quotes.txt") { } public QuoteServer(string filename) : this (filename, 7890) { } public QuoteServer(string filename, int port) { if (filename == null) throw new ArgumentNullException(nameof(filename)); if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort) throw new ArgumentException("port not valid", nameof(port)); _filename = filename; _port = port; }
ReadQuotes是一个帮助方法,它从构造函数指定的文件中读取所有引用。所有引用都添加到List <string>quotes 中。此外创建一个将用于返回随机引用的Random类的实例:
protected void ReadQuotes() { try { _quotes = File.ReadAllLines(filename).ToList(); if (_quotes.Count == 0) { throw new QuoteException("quotes file is empty"); } _random = new Random(); } catch (IOException ex) { throw new QuoteException("I/O Error", ex); } }
另一个帮助方法是GetRandomQuoteOfTheDay。此方法从引用集合返回一个随机引用:
protected string GetRandomQuoteOfTheDay() { int index = random.Next(0, _quotes.Count); return _quotes[index]; }
在Start方法中,使用辅助方法ReadQuotes在List<string> quotes 中读取包含引用的完整文件。此后,将启动一个新线程,它立即调用Listener方法 - 类似于第25章“网络”中的TcpReceive示例。
这里使用任务是因为Start方法不能阻塞和等待客户端,它必须立即返回到调用句柄(SCM)。如果方法没有及时返回到调用句柄(30秒),则SCM认为启动失败。监听器任务是一个长期运行的后台线程。应用程序可以退出而不停止此线程:
public void Start() { ReadQuotes(); _listenerTask = Task.Factory.StartNew(Listener, TaskCreationOptions.LongRunning); }
任务函数 Listener 创建TcpListener实例。 AcceptSocketAsync方法等待客户端连接。一旦客户端连接,AcceptSocketAsync返回一个与客户端关联的套接字。接下来,调用GetRandomQuoteOfTheDay来使用clientSocket.Send将返回的随机引用发送到客户端:
protected async Task ListenerAsync() { try { IPAddress ipAddress = IPAddress.Any; _listener = new TcpListener(ipAddress, port); _listener.Start(); while (true) { using (Socket clientSocket = await _listener.AcceptSocketAsync()) { string message = GetRandomQuoteOfTheDay(); var encoder = new UnicodeEncoding(); byte[] buffer = encoder.GetBytes(message); clientSocket.Send(buffer, buffer.Length, 0); } } } catch (SocketException ex) { Trace.TraceError($"QuoteServer {ex.Message}"); throw new QuoteException("socket error", ex); } }
除了Start方法,还需要以下方法,Stop,Suspend和Resume来控制服务:
public void Stop()=> _listener.Stop(); public void Suspend()=> _listener.Stop(); public void Resume()=> Start();
另一种可以公开获取的方法是RefreshQuotes。如果包含引用的文件被修改了,则使用此方法重新读取文件:
public void RefreshQuotes()=> ReadQuotes(); } }
在围绕服务器构建服务之前,创建一个只有QuoteServer实例并调用Start的测试程序是非常有用的。这样可以测试功能又无需处理服务特定的问题。但必须手动启动此测试服务器,可以使用调试器轻松遍历代码。
测试程序是一个C#控制台应用程序TestQuoteServer。需要引用QuoteServer类的程序集。创建QuoteServer的实例后,调用用QuoteServer实例的Start方法。Start 方法在创建线程后立即返回,因此控制台应用程序保持运行,直到按下Return(代码文件TestQuoteServer/Program.cs):
static void Main() { var qs = new QuoteServer("quotes.txt", 4567); qs.Start(); WriteLine("Hit return to exit"); ReadLine(); qs.Stop(); }
请注意QuoteServer将在本机端口4567上运行此程序,但以后在客户端中必须使用配置。
QuoteClient示例
客户端是一个简单的WPF Windows应用程序,可以在其中请求来自服务器的引用。此应用程序使用TcpClient类连接到正在运行的服务器并接收返回的消息,显示在文本框中。用户界面包含两个控件:一个Button和一个TextBlock。单击按钮从服务器请求引用,并显示引用。
使用Button控件,Click事件分配方法OnGetQuote,该方法从服务器请求引用,并且IsEnabled属性绑定到EnableRequest方法以在请求处于活动状态时禁用该按钮。使用TextBlock控件,Text属性绑定到Quote属性以显示设置的引用(代码文件QuoteClientWPF/MainWindow.xaml):
<Button Margin="3" VerticalAlignment="Stretch" Grid.Row="0" IsEnabled="{Binding EnableRequest, Mode=OneWay}" Click="OnGetQuote"> Get Quote</Button> <TextBlock Margin="6" Grid.Row="1" TextWrapping="Wrap" Text="{Binding Quote, Mode=OneWay}" />
类QuoteInformation定义属性EnableRequest和Quote。这些属性与数据绑定一起使用,以在用户界面中显示这些属性的值。这个类实现接口 InotifyPropertyChanged 以使WPF能够接收属性值的更改(代码文件QuoteClientWPF/QuoteInformation.cs):
using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; namespace Wrox.ProCSharp.WinServices { public class QuoteInformation: INotifyPropertyChanged { public QuoteInformation() { EnableRequest = true; } private string _quote; public string Quote { get { return _quote; } internal set { SetProperty(ref _quote, value); } } private bool _enableRequest; public bool EnableRequest { get { return _enableRequest; } internal set { SetProperty(ref _enableRequest, value); } } private void SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null) { if (!EqualityComparer<T>.Default.Equals(field, value)) { field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; } }
注意 接口 INotifyPropertyChanged 的实现使用属性CallerMemberNameAttribute。此属性在第14章“错误和异常”中进行了说明。
类QuoteInformation的实例被分配给Window类MainWindow的DataContext,以允许直接数据绑定到它(代码文件QuoteClientWPF/MainWindow.xaml.cs):
using System; using System.Net.Sockets; using System.Text; using System.Windows; using System.Windows.Input; namespace Wrox.ProCSharp.WinServices { public partial class MainWindow: Window { private QuoteInformation _quoteInfo = new QuoteInformation(); public MainWindow() { InitializeComponent(); this.DataContext = _quoteInfo; }
可以从项目属性中的“设置”选项卡配置服务器和端口信息以连接到服务器(参见图39.5)。这里可以为ServerName和PortNumber设置定义默认值。将“范围”设置为“User”时,配置文件可以放在用户指定的配置文件中,因此应用程序的每个用户都可以有不同的设置。 Visual Studio的配置功能还创建一个Settings类,以便可以使用强类型读取和写入配置。
图39.5
客户端的主要功能在于Get Quote按钮的Click事件的处理程序:
protected async void OnGetQuote(object sender, RoutedEventArgs e) { const int bufferSize = 1024; Cursor currentCursor = this.Cursor; this.Cursor = Cursors.Wait; quoteInfo.EnableRequest = false; string serverName = Properties.Settings.Default.ServerName; int port = Properties.Settings.Default.PortNumber; var client = new TcpClient(); NetworkStream stream = null; try { await client.ConnectAsync(serverName, port); stream = client.GetStream(); byte[] buffer = new byte[bufferSize]; int received = await stream.ReadAsync(buffer, 0, bufferSize); if (received <= 0) { return; } quoteInfo.Quote = Encoding.Unicode.GetString(buffer).Trim('