在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
本文内容为转载,供学习研究。如有侵权,请联系作者删除。 转载请注明本文出处: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:
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:
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:
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>\system32\services.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:
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>\system32\drivers\etc\quotes. 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('\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; } After starting the test server and this Windows application client, you can test the functionality. Figure 39.6 shows a successful run of this application.
Figure 39.6 At this point, you need to implement the service functionality in the server. The program is already running, so now you want to ensure that the server program starts automatically at boot time without anyone logged on to the system. You can do that by creating a service program, which is discussed next. Windows Service Program Using the C# Windows Service template from the Add New Project dialog, you can now create a Windows Service program. For the new service, use the name QuoteService. After you click the OK button to create the Windows Service program, the designer surface appears but you can’t insert any UI components because the application cannot directly display anything on the screen. The designer surface is used later in this chapter to add components such as installation objects, performance counters, and event logging. Selecting the properties of this service opens the Properties dialog, where you can configure the following values:
NOTE The default service name is Service1, regardless of what the project is called. You can install only one Service1 service. If you get installation errors during your testing process, you might already have installed a Service1 service. Therefore, ensure that you change the name of the service in the Properties dialog to a more suitable name at the beginning of the service’s development. Changing these properties within the Properties dialog sets the values of your ServiceBase-derived class in the InitializeComponent method. You already know this method from Windows Forms applications. It is used in a similar way with services. A wizard generates the code but changes the filename to QuoteService.cs, the name of the namespace to Wrox.ProCSharp.WinServices, and the class name to QuoteService. The code of the service is discussed in detail shortly. The ServiceBase Class The ServiceBase class is the base class for all Windows Services developed with the .NET Framework. The class QuoteService is derived from ServiceBase; this class communicates with the SCM using an undocumented helper class, System.ServiceProcess.NativeMethods, which is just a wrapper class to the Windows API calls. The NativeMethods class is internal, so it cannot be used in your code. The sequence diagram in Figure 39.7 shows the interaction of the SCM, the class QuoteService, and the classes from the System.ServiceProcess namespace. You can see the lifelines of objects vertically and the communication going on horizontally. The communication is time-ordered from top to bottom.
Figure 39.7 The SCM starts the process of a service that should be started. At startup, the Main method is called. In the Main method of the sample service, the Run method of the base class ServiceBase is called. Run registers the method ServiceMainCallback using NativeMethods.StartServiceCtrlDispatcher in the SCM and writes an entry to the event log. Next, the SCM calls the registered method ServiceMainCallback in the service program. ServiceMainCallback itself registers the handler in the SCM using NativeMethods.RegisterServiceCtrlHandler[Ex] and sets the status of the service in the SCM. Then the OnStart method is called. In OnStart, you need to implement the startup code. If OnStart is successful, the string “Service started successfully” is written to the event log. The handler is implemented in the ServiceCommandCallback method. The SCM calls this method when changes are requested from the service. The ServiceCommandCallback method routes the requests further to OnPause, OnContinue, OnStop, OnCustomCommand, and OnPowerEvent. Main Function This section looks into the application template–generated main function of the service process. In the main function, an array of ServiceBase classes, ServicesToRun, is declared. One instance of the QuoteService class is created and passed as the first element to the ServicesToRun array. If more than one service should run inside this service process, it is necessary to add more instances of the specific service classes to the array. This array is then passed to the static Run method of the ServiceBase class. With the Run method of ServiceBase, you are giving the SCM references to the entry points of your services. The main thread of your service process is now blocked and waits for the service to terminate. Here is the automatically generated code (code file QuoteService/Program.cs): static void Main() { ServiceBase[] servicesToRun = new ServiceBase[] { new QuoteService() }; ServiceBase.Run(servicesToRun); } If there is only a single service in the process, the array can be removed; the Run method accepts a single object derived from the class ServiceBase, so the Main method can be reduced to this: ServiceBase.Run(new QuoteService());
The service program Services.exe includes multiple services. If you have a similar service, where more than one service is running in a single process in which you must initialize some shared state for multiple services, the shared initialization must be done before the Run method. With the Run method, the main thread is blocked until the service process is stopped, and any subsequent instructions are not reached before the end of the service. The initialization shouldn’t take longer than 30 seconds. If the initialization code were to take longer than this, the SCM would assume that the service startup failed. You need to take into account the slowest machines where this service should run within the 30-second limit. If the initialization takes longer, you could start the initialization in a different thread so that the main thread calls Run in time. An event object can then be used to signal that the thread has completed its work. Service Start At service start, the OnStart method is called. In this method, you can start the previously created socket server. You must reference the QuoteServer assembly for the use of the QuoteService. The thread calling OnStart cannot be blocked; this method must return to the caller, which is the ServiceMainCallback method of the ServiceBase class. The ServiceBase class registers the handler and informs the SCM that the service started successfully after calling OnStart (code file QuoteService/QuoteService.cs): protected override void OnStart(string[] args) { _quoteServer = new QuoteServer(Path.Combine( AppDomain.CurrentDomain.BaseDirectory,"quotes.txt"), 5678); _quoteServer.Start(); } The _quoteServer variable is declared as a private member in the class: namespace Wrox.ProCSharp.WinServices { public partial class QuoteService: ServiceBase { private QuoteServer _quoteServer; Handler Methods When the service is stopped, the OnStop method is called. You should stop the service functionality in this method (code file QuoteService/QuoteService.cs): protected override void OnStop() => _quoteServer.Stop(); In addition to OnStart and OnStop, you can override the following handlers in the service class:
protected override void OnPause() => _quoteServer.Suspend(); protected override void OnContinue() => _quoteServer.Resume(); public const int CommandRefresh = 128; protected override void OnCustomCommand(int command) { 全部评论
专题导读
上一篇:C#获取网页源代码发布时间:2022-07-13下一篇:Android:跟我学Binder---(6)JAVA实现Android:跟我学Binder---(1)什么是BinderIPC?为 ...发布时间:2022-07-13热门推荐
热门话题
阅读排行榜
|
请发表评论