为服务创建核心功能
在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认为启动失败。监听器任务是一个长期运行的后台线程。应用程序可以退出而不停止此线程:
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上运行此程序,但以后在客户端中必须使用配置。
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显示了此应用程序的成功运行。
using System.ComponentModel;
using System.Configuration.Install;
namespace Wrox.ProCSharp.WinServices
{
[RunInstaller(true)]
public partial class ProjectInstaller: Installer
{
public ProjectInstaller()
{
InitializeComponent();
}
}
}
接下来了解项目安装程序调用的安装程序中的其他安装项。