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手動啓動服務(有關詳細信息,請參閱下一部分),而後能夠啓動客戶端應用程序。

 

-------------------未完待續

(本章內容過長,分爲上下篇)

相關文章
相關標籤/搜索