本文内容为转载,供学习研究。如有侵权,请联系作者删除。
转载请注明本文出处:Professional C# 6 and .NET Core 1.0 - Chapter 39 Windows Services
-----------------------------------------------------------------------
What’s In This Chapter?
The architecture of a Windows Service
Creating a Windows Service program
Windows Services installation programs
Windows Services control programs
Troubleshooting Windows Services
Wrox.com Code Downloads for This Chapter
The wrox.com code downloads for this chapter are found at www.wrox.com/go/professionalcsharp6 on the Download Code tab. The code is in the Chapter 39 download and individually named according to the names throughout the chapter.
Quote Server
Quote Client
Quote Service
Service Control
What Is a Windows Service?
Windows Services are programs that can be started automatically at boot time without the need for anyone to log on to the machine. If you need to have programs start up without user interaction or need to run under a different user than the interactive user, which can be a user with more privileges, you can create a Windows Service. Some examples could be a WCF host (if you can’t use Internet Information Services (IIS) for some reason), a program that caches data from a network server, or a program that reorganizes local disk data in the background.
This chapter starts with looking at the architecture of Windows Services, creates a Windows Service that hosts a networking server, and gives you information to start, monitor, control, and troubleshoot your Windows Services.
As previously mentioned, Windows Services are applications that can be automatically started when the operating system boots. These applications can run without having an interactive user logged on to the system and can do some processing in the background.
For example, on a Windows Server, system networking services should be accessible from the client without a user logging on to the server; and on the client system, services enable you to do things such as get a new software version online or perform some file cleanup on the local disk.
You can configure a Windows Service to run from a specially configured user account or from the system user account—a user account that has even more privileges than that of the system administrator.
NOTE Unless otherwise noted, when I refer to a service, I am referring to a Windows Service.
Here are a few examples of services:
- Simple TCP/IP Services is a service program that hosts some small TCP/IP servers: echo, daytime, quote, and others.
- World Wide Web Publishing Service is a service of IIS.
- Event Log is a service to log messages to the event log system.
- Windows Search is a service that creates indexes of data on the disk.
- Superfetch is a service that preloads commonly used applications and libraries into memory, thus improving the startup time of these applications.
You can use the Services administration tool, shown in Figure 39.1, to see all the services on a system. You get to the program by entering Services on the Start screen.
Figure 39.1
NOTE You can’t create a Windows Service with .NET Core; you need the .NET Framework. To control services, you can use .NET Core.
Windows Services Architecture
Three program types are necessary to operate a Windows Service:
- A service program
- A service control program
- A service configuration program
The service program is the implementation of the service. With a service control program, it is possible to send control requests to a service, such as start, stop, pause, and continue. With a service configuration program, a service can be installed; it is copied to the file system, and information about the service needs to be written to the registry. This registry information is used by the service control manager (SCM) to start and stop the service. Although .NET components can be installed simply with an xcopy—because they don’t need to write information to the registry—installation for services requires registry configuration. You can also use a service configuration program to change the configuration of that service at a later point. These three ingredients of a Windows Service are discussed in the following subsections.
Service Program
In order to put the .NET implementation of a service in perspective, this section takes a brief look at the Windows architecture of services in general, and the inner functionality of a service.
The service program implements the functionality of the service. It needs three parts:
- A main function
- A service-main function
- A handler
Before discussing these parts, however, it would be useful to digress for a moment for a short introduction to the SCM, which plays an important role for services—sending requests to your service to start it and stop it.
Service Control Manager
The SCM is the part of the operating system that communicates with the service. Using a sequence diagram, Figure 39.2 illustrates how this communication works.
Figure 39.2
At boot time, each process for which a service is set to start automatically is started, and so the main function of this process is called. The service is responsible for registering the service-main function for each of its services. The main function is the entry point of the service program, and in this function the entry points for the service-main functions must be registered with the SCM.
Main Function, Service-Main, and Handlers
The main function of the service is the normal entry point of a program, the Main method. The main function of the service might register more than one service- main function. The service-main function contains the actual functionality of the service, which must register a service-main function for each service it provides. A service program can provide a lot of services in a single program; for example, <windows>system32services.exe is the service program that includes Alerter, Application Management, Computer Browser, and DHCP Client, among other items.
The SCM calls the service-main function for each service that should be started. One important task of the service-main function is registering a handler with the SCM.
The handler function is the third part of a service program. The handler must respond to events from the SCM. Services can be stopped, suspended, and resumed, and the handler must react to these events.
After a handler has been registered with the SCM, the service control program can post requests to the SCM to stop, suspend, and resume the service. The service control program is independent of the SCM and the service itself. The operating system contains many service control programs, such as the Microsoft Management Console (MMC) Services snap-in shown earlier in Figure 39.1. You can also write your own service control program; a good example of this is the SQL Server Configuration Manager shown in Figure 39.3 which runs within MMC.
Figure 39.3
Service Control Program
As the self-explanatory name suggests, with a service control program you can stop, suspend, and resume the service. To do so, you can send control codes to the service, and the handler should react to these events. It is also possible to ask the service about its actual status (if the service is running or suspended, or in some faulted state) and to implement a custom handler that responds to custom control codes.
Service Configuration Program
Because services must be configured in the registry, you can’t use xcopy installation with services. The registry contains the startup type of the service, which can be set to automatic, manual, or disabled. You also need to configure the user of the service program and dependencies of the service—for example, any services that must be started before the current one can start. All these configurations are made within a service configuration program. The installation program can use the service configuration program to configure the service, but this program can also be used later to change service configuration parameters.
Classes for Windows Services
In the .NET Framework, you can find service classes in the System.ServiceProcess namespace that implement the three parts of a service:
- You must inherit from the ServiceBase class to implement a service. The ServiceBase class is used to register the service and to answer start and stop requests.
- The ServiceController class is used to implement a service control program. With this class, you can send requests to services.
- The ServiceProcessInstaller and ServiceInstaller classes are, as their names suggest, classes to install and configure service programs.
Now you are ready to create a new service.
Creating a Windows Service Program
The service that you create in this chapter hosts a quote server. With every request that is made from a client, the quote server returns a random quote from a quote file. The first part of the solution uses three assemblies: one for the client and two for the server. Figure 39.4 provides an overview of the solution. The assembly QuoteServer holds the actual functionality. The service reads the quote file in a memory cache and answers requests for quotes with the help of a socket server. The QuoteClient is a WPF rich–client application. This application creates a client socket to communicate with the QuoteServer. The third assembly is the actual service. The QuoteService starts and stops the QuoteServer; the service controls the server.
Figure 39.4
Before creating the service part of your program, create a simple socket server in an extra C# class library that will be used from your service process. How this can be done is discussed in the following section.
Creating Core Functionality for the Service
You can build any functionality in a Windows Service, such as scanning for files to do a backup or a virus check or starting a WCF server. However, all service programs share some similarities. The program must be able to start (and to return to the caller), stop, and suspend. This section looks at such an implementation using a socket server.
With Windows 10, the Simple TCP/IP Services can be installed as part of the Windows components. Part of the Simple TCP/IP Services is a “quote of the day,” or qotd, TCP/IP server. This simple service listens to port 17 and answers every request with a random message from the file <windows>system32driversetcquotes. With the sample service, a similar server will be built. The sample server returns a Unicode string, in contrast to the qotd server, which returns an ASCII string.
First, create a class library called QuoteServer and implement the code for the server. The following walks through the source code of your QuoteServer class in the file QuoteServer.cs: (code file 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;
The constructor QuoteServer is overloaded so that a filename and a port can be passed to the call. The constructor where just the filename is passed uses the default port 7890 for the server. The default constructor defines the default filename for the quotes as 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 is a helper method that reads all the quotes from a file that was specified in the constructor. All the quotes are added to the List<string> quotes. In addition, you are creating an instance of the Random class that will be used to return random quotes:
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); } }
Another helper method is GetRandomQuoteOfTheDay. This method returns a random quote from the quotes collection:
protected string GetRandomQuoteOfTheDay() { int index = random.Next(0, _quotes.Count); return _quotes[index]; }
In the Start method, the complete file containing the quotes is read in the List<string> quotes by using the helper method ReadQuotes. After this, a new thread is started, which immediately calls the Listener method—similarly to the TcpReceive example in Chapter 25, “Networking.”
Here, a task is used because the Start method cannot block and wait for a client; it must return immediately to the caller (SCM). The SCM would assume that the start failed if the method didn’t return to the caller in a timely fashion (30 seconds). The listener task is a long-running background thread. The application can exit without stopping this thread:
public void Start() { ReadQuotes(); _listenerTask = Task.Factory.StartNew(Listener, TaskCreationOptions.LongRunning); }
The task function Listener creates a TcpListener instance. The AcceptSocketAsync method waits for a client to connect. As soon as a client connects, AcceptSocketAsync returns with a socket associated with the client. Next, GetRandomQuoteOfTheDay is called to send the returned random quote to the client using 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); } }
In addition to the Start method, the following methods, Stop, Suspend, and Resume, are needed to control the service:
public void Stop() => _listener.Stop(); public void Suspend() => _listener.Stop(); public void Resume() => Start();
Another method that will be publicly available is RefreshQuotes. If the file containing the quotes changes, the file is reread with this method:
public void RefreshQuotes() => ReadQuotes(); } }
Before you build a service around the server, it is useful to build a test program that creates just an instance of the QuoteServer and calls Start. This way, you can test the functionality without having to handle service-specific issues. You must start this test server manually, and you can easily walk through the code with a debugger.
The test program is a C# console application, TestQuoteServer. You need to reference the assembly of the QuoteServer class. After you create an instance of the QuoteServer, the Start method of the QuoteServer instance is called. Start returns immediately after creating a thread, so the console application keeps running until Return is pressed (code file TestQuoteServer/Program.cs):
static void Main() { var qs = new QuoteServer("quotes.txt", 4567); qs.Start(); WriteLine("Hit return to exit"); ReadLine(); qs.Stop(); }
Note that QuoteServer will be running on port 4567 on localhost using this program—you have to use these settings in the client later.
QuoteClient Example
The client is a simple WPF Windows application in which you can request quotes from the server. This application uses the TcpClient class to connect to the running server and receives the returned message, displaying it in a text box. The user interface contains two controls: a Button and a TextBlock. Clicking the button requests the quote from the server, and the quote is displayed.
With the Button control, the Click event is assigned to the method OnGetQuote, which requests the quote from the server, and the IsEnabled property is bound to the EnableRequest method to disable the button while a request is active. With the TextBlock control, the Text property is bound to the Quote property to display the quote that is set (code file 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}" />
The class QuoteInformation defines the properties EnableRequest and Quote. These properties are used with data binding to show the values of these properties in the user interface. This class implements the interface InotifyPropertyChanged to enable WPF to receive changes in the property values (code file 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; } }
NOTE Implementation of the interface INotifyPropertyChanged makes use of the attribute CallerMemberNameAttribute. This attribute is explained in Chapter 14, “Errors and Exceptions.”
An instance of the class QuoteInformation is assigned to the DataContext of the Window class MainWindow to allow direct data binding to it (code file 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; }
You can configure server and port information to connect to the server from the Settings tab inside the properties of the project (see Figure 39.5). Here, you can define default values for the ServerName and PortNumber settings. With the Scope set to User, the settings can be placed in user-specific configuration files, so every user of the application can have different settings. This Settings feature of Visual Studio also creates a Settings class so that the settings can be read and written with a strongly typed class.
Figure 39.5
The major functionality of the client lies in the handler for the Click event of the Get Quote button:
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('