C# 6 与 .NET Core 1.0 高级编程 - 39 章 Windows 服务(上)

译文,个人原创,转载请注明出处(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>\system32\services.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>\system32\drivers\etc\quotes的随机消息来回答每个请求。示例服务将创建类似的服务器。示例服务器返回一个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('\0');
  }
  catch (SocketException ex)
  {
    MessageBox.Show(ex.Message,"Error Quote of the day",
        MessageBoxButton.OK, MessageBoxImage.Error);
  }
  finally
  {
    stream?.Close();
    if (client.Connected)
    {
      client.Close();
    }
  }
  this.Cursor = currentCursor;
  quoteInfo.EnableRequest = true;
}

启动测试服务器和此Windows应用程序客户端后就可以测试功能。图39.6显示了此应用程序的成功运行。

图39.6

此时需要在服务器中实现服务功能。程序已在运行,因此现在要确保服务器程序在开机还没有任何人登录到系统时自动启动。可以通过创建一个服务程序来检测,接下来会讨论这一点。

Windows服务程序

使用 Add Project 对话框中的C#Windows服务模板,可以创建Windows服务程序。新服务名称可以使用QuoteService。

单击 “确定”按钮创建Windows服务程序后,将显示设计器界面但无法插入任何UI组件,因为应用程序无法在屏幕上直接显示任何内容。本章后面将使用设计器界面来添加组件,如安装对象、性能计数器和事件日志记录。

选择服务的属性打开“属性”对话框,可以在其中配置以下值:

  • AutoLog - 指定将启动和停止服务事件自动写入事件日志。
  • CanPauseAndContinue,CanShutdown和CanStop - 指定暂停,继续,关闭和停止请求。
  • ServiceName - 写入注册表并用于控制服务的服务的名称。
  • CanHandleSessionChangeEvent - 定义服务是否可以处理来自终端服务器会话的更改事件。
  • CanHandlePowerEvent - 对于在笔记本电脑或移动设备上运行的服务,这是一个非常有用的选项。如果启用该选项,则服务可以对低功率事件做出反应,并相应地更改服务的行为。电源事件的示例 包括电池电量不足,电源状态变化(A/C电源切换),更改为暂停。

注意 默认服务名称为Service1,无论项目是什么名称。只能安装一个Service1服务。如果在测试过程中遇到安装错误,很可能是已经安装了Service1服务。因此请确保在服务开发开始时在属性对话框中将服务名称更改为更合适的名称。

在“属性”对话框中更改这些属性,将在InitializeComponent方法中设置ServiceBase派生类的值。我们已经从Windows窗体应用程序中了解此方法。它以类似的方式用于服务。

用向导生成代码,将文件名更改为QuoteService.cs,将命名空间的名称改为Wrox.ProCSharp.WinServices,类名改为QuoteService。稍后将详细讨论服务的代码。

ServiceBase类

ServiceBase类是.NET Framework开发的所有Windows服务的基类。类QuoteService是从ServiceBase派生的,该类使用未说明的辅助类System.ServiceProcess.NativeMethods与SCM通信,它仅是Windows API调用的包装类。 NativeMethods类属于内部的,所以不能在代码中使用。
图39.7中的序列图显示了SCM,类QuoteService和System.ServiceProcess命名空间中的类之间的交互。可以从纵向查看对象的生命周期,水平方向查看通信。通信按时间顺序从上到下。

图39.7

SCM启动要启动的服务的进程。启动时调用Main方法。在示例服务的Main方法中,将调用基类ServiceBase的Run方法。运行使用SCM中的NativeMethods.StartServiceCtrlDispatcher注册方法ServiceMainCallback,将记录写入事件日志。

接下来,SCM调用服务程序中的注册方法ServiceMainCallback。 ServiceMainCallback本身使用NativeMethods.RegisterServiceCtrlHandler [Ex]在SCM中注册处理事件,并在SCM中设置服务的状态。然后调用OnStart方法。在OnStart中,需要实现启动代码。如果OnStart成功,则将“Service started successfully”字符串写入事件日志。

处理程序在ServiceCommandCallback方法中实现。当从服务请求更改时,SCM调用该方法。 ServiceCommandCallback方法将请求进一步路由到OnPause,OnContinue,OnStop,OnCustomCommand和OnPowerEvent。

主函数

本节研究应用程序模板生成的主要功能服务过程。在main函数中,声明了一组 ServiceBase类和ServicesToRun。一旦QuoteService类的实例被创建,将作为第一个元素传递到ServicesToRun数组。如果在该服务进程内运行多个服务,则需要向数组中添加更多指定服务类的实例。然后将该数组传递到ServiceBase类的静态Run方法。使用ServiceBase的Run方法,将SCM引用到服务的入口点。然后服务进程的主线程被阻塞,并等待服务终止。
这里是自动生成的代码(代码文件QuoteService/Program.cs):

static void Main()
{
   ServiceBase[] servicesToRun = new ServiceBase[]
   {
      new QuoteService()
   };
   ServiceBase.Run(servicesToRun);
}

如果在进程中只有一个服务,可以删除数组;Run方法接受从ServiceBase类派生的单个对象,因此Main方法可以简化为:

ServiceBase.Run(new QuoteService());

服务程序Services.exe包含多个服务。如果有类似的服务,其中多个服务在单个进程中运行,而且需要初始化多个服务的一些共享状态,那么共享初始化必须在Run方法之前完成。使用Run方法,主线程会被阻塞直到服务进程停止,并且在服务结束之前不接收任何后续指令。

初始化不应超过30秒。如果初始化代码需要的时间比30秒长,SCM将认为服务启动失败。需要考虑此服务应在30秒内运行的最慢的计算机。如果初始化花费过长时间,可以在不同的线程中启动初始化,以便主线程及时调用Run方法。然后可以使用事件对象来表示线程已完成其工作。

服务开始

在服务启动时,将调用OnStart方法。在此方法中,可以启动先前创建的套接字服务器。必须引用QuoteServer程序集以使用QuoteService。调用OnStart的线程不能被阻塞;这个方法必须返回调用者,这是ServiceBase类的ServiceMainCallback方法。 ServiceBase类注册处理程序并通知SCM,服务在调用OnStart后成功启动(代码文件QuoteService / QuoteService.cs):

protected override void OnStart(string[] args)
{
  _quoteServer = new QuoteServer(Path.Combine(
                    AppDomain.CurrentDomain.BaseDirectory,"quotes.txt"),
                        5678);
  _quoteServer.Start();
}

_quoteServer变量在类中被声明为私有成员:

namespace Wrox.ProCSharp.WinServices
{
  public partial class QuoteService: ServiceBase
  {
    private QuoteServer _quoteServer;

处理事件方法

当服务停止时,将调用OnStop方法。应该在该方法中停止服务功能(代码文件QuoteService/QuoteService.cs):

protected override void OnStop() => _quoteServer.Stop();

除了OnStart和OnStop之外,还可以重写服务类中的以下事件:

  • OnPause - 在服务应暂停时调用。
  • OnContinue - 在服务暂停后应返回正常操作时调用。为了能够调用被重写的方法OnPause和OnContinue,必须将CanPauseAndContinue属性设置为true。
  • OnShutdown - 当Windows正在进行系统关机时调用。通常该方法的行为应该类似于OnStop实现,如果需要更多的时间关闭,可以请求更多。与OnPause和OnContinue类似,必须将属性设置为启用该行为:CanShutdown必须设置为true。
  • OnPowerEvent - 当系统的电源状态更改时调用。有关电源状态更改的信息位于PowerBroadcastStatus类型的参数中。 PowerBroadcastStatus是一个枚举值,例如Battery Low和PowerStatusChange。这里还将获得系统要挂起(QuerySuspend)的信息,可批准或拒绝。可以在本章后面阅读有关电源事件的更多信息。
  • OnCustomCommand - 这是一个处理事件,可以服务由服务控制程序发送的自定义命令。OnCustomCommand的方法签名有一个int参数,可以从中检索自定义命令编号。该值可以在128到256的范围内,低于128的值是系统保留的值。在下面的服务中,将使用自定义命令128重新阅读引用文件:
protected override void OnPause() => _quoteServer.Suspend();
protected override void OnContinue() => _quoteServer.Resume();
public const int CommandRefresh = 128;
protected override void OnCustomCommand(int command)
{
  switch (command)
  {
    case CommandRefresh:
      quoteServer.RefreshQuotes();
      break;
    default:
      break;
  }
}

线程和服务

如本章前面所述,如果初始化过长,SCM认为服务失败。要处理这个问题,可以创建一个线程。

服务类中的OnStart方法必须及时返回。如果从TcpListener类中调用阻塞方法(如AcceptSocket),则需要启动一个线程去做。如果网络服务器要处理多个客户端,线程池也非常有用。 AcceptSocket接收调用和处理池中的另一个线程。这样没有人等待代码的执行,系统也可以及时响应。

服务安装

服务必须在注册表中进行配置。所有服务的键都可以在注册表HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services 路径下找到。可以使用“regedit”命令查看注册表项。在这里可以找到服务类型、显示的名称、可执行文件的路径、启动的配置等。图39.8显示了W3SVC服务的注册表配置。

图39.8

可以使用System.ServiceProcess命名空间中的安装程序类来执行此配置,接下来将会详细描述。

安装程序

使用Visual Studio切换到设计视图,然后从上下文菜单中选择“Add Installer”选项,为服务添加安装程序。使用该选项可以创建一个新的ProjectInstaller类以及ServiceInstaller实例和ServiceProcessInstaller实例。

图39.9显示了服务的安装程序类的类图。

请记住此图,它是用"Add Installer"选项创建的,接下来将会详细讲解 ProjectInstaller.cs文件中的源代码。

安装程序类

ProjectInstaller类派生自System.Configuration.Install.Installer,这是所有自定义安装程序的基类。Installer类可以构建基于事务的安装程序。使用基于事务的安装程序,如果安装失败,可以回滚到先前的状态,此时安装程序所做的任何更改都会被撤消。如图39.9所示,Installer类有Install,Uninstall,Commit和Rollback方法,它们从安装程序中调用。

图39.9

属性[RunInstaller(true)]表示在安装程序集时应调用ProjectInstaller类。自定义操作安装程序,以及installutil.exe(本章后面计划)时检查该属性。
InitializeComponent 在 ProjectInstaller 类的构造函数中调用(代码文件QuoteService/ProjectInstaller.cs):

using System.ComponentModel;
using System.Configuration.Install;
namespace Wrox.ProCSharp.WinServices
{
  [RunInstaller(true)]
  public partial class ProjectInstaller: Installer
  {
    public ProjectInstaller()
    {
      InitializeComponent();
    }
  }
}

接下来了解项目安装程序调用的安装程序中的其他安装项。

进程安装程序和服务安装程序

在InitializeComponent的实现中,创建了 ServiceProcessInstaller类和ServiceInstaller类的实例。这两个类都派生自ComponentInstaller 类,而 ComponentInstaller 本身是从Installer派生的。

从ComponentInstaller派生的类可以与安装过程一起使用。记住服务进程可以包含多个服务。 ServiceProcessInstaller类用于线程在该过程中所有服务定义的值的配置,ServiceInstaller类用于服务的配置,因此每个服务都需要一个ServiceInstaller实例。如果进程内有三个服务,则需要添加三个ServiceInstaller对象:

partial class ProjectInstaller
{
  private System.ComponentModel.IContainer components = null;
  private void InitializeComponent()
  {
    this.serviceProcessInstaller1 =
        new System.ServiceProcess.ServiceProcessInstaller();
    this.serviceInstaller1 =
        new System.ServiceProcess.ServiceInstaller();
    this.serviceProcessInstaller1.Password = null;
    this.serviceProcessInstaller1.Username = null;
    this.serviceInstaller1.ServiceName ="QuoteService";
    this.serviceInstaller1.Description ="Sample Service for Professional 
C#";
    this.serviceInstaller1.StartType = 
System.ServiceProcess.ServiceStartMode.Manual;
    this.Installers.AddRange(
      new System.Configuration.Install.Installer[]
          {this.serviceProcessInstaller1,
           this.serviceInstaller1});
  }
  private System.ServiceProcess.ServiceProcessInstaller
      serviceProcessInstaller1;
  private System.ServiceProcess.ServiceInstaller serviceInstaller1;
}

ServiceProcessInstaller类安装一个可执行文件,其中包含从基类ServiceBase派生的类。 ServiceProcessInstaller有全部服务进程的属性。下表描述了进程内所有服务共享的属性。

属性

描述

用户名,密码

如果帐户属性设置为ServiceAccount.User,代表着服务运行所在的用户帐户。

帐户

使用此属性可以指定服务的帐户类型。

帮助文本

只读属性返回用于设置的用户名和密码的帮助文本。

运行服务的进程可以使用ServiceAccount枚举类型指定ServiceProcessInstaller类的Account属性。下表介绍了“帐户”属性的不同值。

描述

LocalSystem

设置该值指定服务使用本地系统上较高特权的用户帐户,在网络上可以代表本机

NetworkService

与LocalSystem类似,该值指定计算机的凭证传递到远程服务器,但不像LocalSystem,这样的服务作为本地系统上的非特权用户。顾名思义,该帐户应仅用于服务需要来自网络的资源。

LocalService

该帐户类型表示任意远程服务的匿名凭据,它在本地具有与NetworkService相同的权限。

User

将Account属性设置为ServiceAccount.User意味着可以定义服务使用的帐户。

ServiceInstaller是每个服务都需要的类,它对于进程内的每个服务具有以下属性:StartType,DisplayName,ServiceName和ServicesDependentOn,如下表所述。

属性

描述

StartType

StartType属性指示服务为手动或自动启动。可能的值为ServiceStartMode.Automatic,ServiceStartMode.Manual和ServiceStartMode.Disabled。如果是最后一个,服务将无法启动。该选项对不能在系统上启动的服务很有用。可能需要设置选项设置为Disabled(禁用),例如,所需的硬件控制器不可用。

DelayedAutoStart

如果StartType设置为Automatic,该属性将被忽略。这里可以指定那些服务不应在系统开机时立即启动而在开机之后启动的服务。

DisplayName

DisplayName是服务显示给用户的名称。管理工具也用该名称来控制和监视服务。

ServiceName

ServiceName是服务的名称。必须是服务程序中 ServiceBase类的ServiceName属性一致的值。此名称关联配置ServiceInstaller到所需的服务程序。

ServicesDependentOn

指定在服务启动前必须启动的服务组。当服务启动时,所有这些依赖服务将自动启动,该服务才能启动。

注意 如果更改ServiceBase派生类中的服务名称,请务必也更改ServiceInstaller 对象中的ServiceName属性。

注意 在测试阶段将StartType设置为Manual。这样,如果不能停止服务(例如它有一个错误),仍然有可能重新启动系统,但如果将StartType设置为自动,服务将在重启时自动启动!确定它可以工作时,再更改该配置。

ServiceInstallerDialog类

System.ServiceProcess.Design命名空间中的另一个安装程序类是
ServiceInstallerDialog。如果希望系统管理员在安装期间通过分配用户名和密码来输入服务应使用的帐户,则可以使用此类。

如果将ServiceProcessInstaller类的Account属性设置为ServiceAccount.User,并将Username和Password属性设置为null,则在安装时将看到“设置服务登录”对话框(参见图39.10)。也可以在此时取消安装。

图39.10

installutil

将安装程序类添加到项目后,可以使用 installutil.exe 实用程序来安装和卸载服务。可以使用该工具来安装具有Installer类的任何程序集。 installutil.exe实用程序调用从 Install 类派生的类的 Install 方法安装,调用Uninstall 方法来卸载。

示例服务中用于安装和卸载的命令行输入如下:

installutil quoteservice.exe
installutil / u quoteservice.exe

 

注意 如果安装失败,请务必检查安装日志文件InstallUtil.InstallLog和<servicename>.InstallLog。通常可以在此找到非常有用的信息,例如“指定的服务已存在“。

服务成功安装后,您可以从服务MMC手动启动服务(有关详细信息,请参阅下一部分),然后可以启动客户端应用程序。

 

-------------------未完待续

(本章内容过长,分为上下篇)

posted @ 2017-02-14 10:37  沐汐Vicky  阅读(2511)  评论(9编辑  收藏  举报