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

译文,我的原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 39 章 Windows 服务(上)),不对的地方欢迎指出与交流。  html

章节出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位阅读时仔细分辨,惟望莫误人子弟。 编程

附英文版原文:Professional C# 6 and .NET Core 1.0 - Chapter 39 Windows Serviceswindows

----------------------------------------------------- 数组

本章主要内容浏览器

  • 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服务是能够在操做系统引导时自动启动的应用程序。这些应用程序能够在没有交互式用户登陆到系统的状况下运行,而且能够在后台进行一些处理。app

例如,在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手动启动服务(有关详细信息,请参阅下一部分),而后能够启动客户端应用程序。

 

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

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

相关文章
相关标签/搜索