譯文,個人原創,轉載請注明出處(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手動啟動服務(有關詳細信息,請參閱下一部分),然后可以啟動客戶端應用程序。
-------------------未完待續
(本章內容過長,分為上下篇)