Windows 服务
注意服务这个概念:
- 有时表示服务这个抽象名词
- 有时表示服务程序
- 有时表示所提供的一个服务 ( 功能 ).
1. Windows 服务
介绍什么是 win 服务, 及其特性.
并列举一些案例.
介绍服务资源管理器.
2. Windows 服务体系结构
操作 win 服务需要三种程序:
- 服务程序
- 服务控制程序
- 服务配置程序
- 服务程序
- 提供需要的实际功能
- 服务控制程序
- 将控制请求发送给服务, 例如开始, 暂停与继续等.
- 服务配置程序
- 用于安装服务
- 需要将程序复制到文件系统中
- 需要将服务信息写入注册表 ( 注册信息由服务控制管理器 ( SCM ) 来使用 )
2.1 服务程序
主要讨论服务的结构
服务程序需要 3 个部分:
- 主函数
service-main
函数- 处理程序
1. 服务控制管理器
SCM ( Service Control Manager ), 是 OS 的组成部分. 作用是与服务进行通信.
participant SCM AS scm
participant 服务 AS service
scm -> service: 启动服务进程
service -> scm: 注册 service-main
scm -> service: service-main
service -> scm: 注册处理程序
如设置服务自动启动:
- 系统启动, 启动该服务每一个进程
- 调用进程的主函数
- 该服务应用为每一项服务功能注册
service-main
主函数. 主函数是服务程序的入口.
service-main
主函数必须用 SCM 注册.
2. 主函数, service-main
和处理程序
服务的主函数为程序的入口点, 即 Main
方法.
它可以注册多个 service-main
函数. service-main
函数包含实际的功能.
服务程序可以在一个程序中提供多项服务.
SCM 为每一个应该启动的服务调用 service-main
函数.
service-main
函数的一个重要任务是用 SCM 注册一个处理程序.
处理程序响应来自 SCM 的事件: 停止, 暂停, 重新开始等.
使用 SCM 注册处理程序后, 服务控制程序 可以将控制消息 ( 开启, 暂停等 ) 发送给 SCM.
服务控制程序独立于 SCM 与 服务本身.
典型的服务控制程序是 MMC 中的
SQL Server Configuration Manager
.
2.2 服务控制程序
服务控制程序用于控制服务 ( 服务程序 )
participant 用户 as U
participant 服务控制程序 as A
participant 服务(服务程序) as B
U -> A : 操作
A --> B : 启动
A --> B : 暂停
A --> B : 继续服务
A --> B : 询问当前状态
A -> U : 响应状态
2.3 服务配置程序
不能使用 xcpoy 安装服务, 服务必须在注册表中配置.
注册表中包含了:
- 服务的启动类型 ( 自动, 手动, 禁用 )
- 配置服务程序的用户, 与其他服务的依赖关系 ( 启动顺序 ).
这些都在服务配置程序中进行.
除了用于安装时配置, 还可以用于安装后修改配置.
2.4 Windows 服务的类
在 .NET Framework 中的命名空间 System.ServiceProcess
中提供了三部分的服务类:
- 必须从
ServiceBase
类继承. 该类用于注册服务, 响应开始与停止请求. ServiceController
类用于实现服务控制程序. 该类允许将请求发送给服务 ( 服务应用 ).ServiceProcessInstaller
类与ServiceInstaller
类用于安装和配置服务程序.
3. 创建 Window 服务
QuoteServer
描述:
- 创建的服务驻留在引用服务器内.
- 对于客户端的请求, 引用服务器会返回引用文件的随机引用.
解决方案包含 3 个程序集.
- 一个用于客户端
- 两个用于服务器
QuoteServer
包含实际功能. 可以在内存缓存中读取引用. 基于socket
服务响应请求.QuoteClient
是 WFP 富客户端, 创建用户socket
, 与QuoteServer
通信.QuoteServices
为实际的服务. 用于开始和停止QuoteServer
.
participant QuoteClient as a
participant QuoteServer as b
participant QuoteService as c
participant Windows 服务 as d
a -> b : 通信
b -> a : 通信
c -> b :
d --> c :
3.1 创建核心服务功能
步骤:
- 创建
QuoteServer
类库. 实现服务器代码. - 重载
QuoteServer()
构造函数, 传入文件名与端口号. - 提供
ReadQuotes()
辅助方法, 用于从文件中读取所有引用, 创建Readom
实例. - 提供
GetRandomQuoteOfTheDay()
, 来返回随机引用. - 提供
ListenerAsync()
任务. - 提供
Start()
方法,Stop()
,Suspend()
,Resume()
方法. - 提供
RefreshQuotes()
方法来重置.
最后创建测试程序, 来测试 QuoteServer
.
public class QuoteServer
{
private TcpListener _listener;
private int _port;
private string _filename;
private List<string> _quotes;
private Random _random;
private Task _listenerTask;
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("非法端口号 ", nameof(port));
_filename = filename;
_port = port;
}
protected void ReadQuotes()
{
try
{
_quotes = File.ReadAllLines(_filename).ToList();
if (_quotes.Count == 0)
{
throw new QuoteException("文件为空");
}
_random = new Random();
}
catch (IOException ex)
{
throw new QuoteException("I/O 异常", ex);
}
}
protected string GetRandomQuoteOfTheDay()
{
int index = _random.Next(0, _quotes.Count);
return _quotes[index];
}
public void Start()
{
ReadQuotes();
_listenerTask = Task.Factory.StartNew(ListenerAsync, TaskCreationOptions.LongRunning);
}
protected async Task ListenerAsync()
{
try
{
// 实例化 TCP Listener
IPAddress ipAddress = IPAddress.Any;
_listener = new TcpListener(ipAddress, _port);
_listener.Start();
// 阻塞监听
while (true)
{
using (Socket clientSocket = await _listener.AcceptSocketAsync())
{
Console.WriteLine("接收到请求");
string message = GetRandomQuoteOfTheDay();
byte[] buffer = Encoding.Unicode.GetBytes(message);
clientSocket.Send(buffer, buffer.Length, SocketFlags.None);
}
}
}
catch (SocketException ex)
{
Trace.TraceError($"QuoteServer Error: {ex.Message}");
throw new QuoteException("Socket Error", ex);
}
}
public void Stop() => _listener.Stop();
public void Suspend() => _listener.Stop();
public void Resume() => _listener.Start();
public void Refresh() => ReadQuotes();
}
3.2 QuoteClient 示例
实际上为一个窗体应用程序 ( 技术上可以使用 WinForm 也可以使用 WPF ). 核心逻辑为:
- 点击按钮, 获得服务地址 (
localhost
), 与 端口号 (7890
). - 创建
TcpClient
类. 连接服务器 (ConnectAsync()
). - 获得数据 (
GetStream()
)
const int bufferSize = 1024;
string serverName = ...;
int port = ...;
TcpClient client = new TcpClient();
NetworkStream stream = null;
try
{
await client.ConnectAsync(serverName, port);
stream = client.GetStream();
byte[] buffer = new byte[bufferSize];
int reciveed = await stream.ReadAsync(buffer, 0, bufferSize);
if (reciveed <= 0)
{
return;
}
// => Encoding.Unicode.GetString(buffer).Trim('\0');
}
catch (SocketException ex)
{
...
}
finally
{
stream?.Close();
if (client.Connected)
{
client.Close();
}
}
3.3 Windows 服务程序
新建服务项目 ( 命名为 QuoteService
).
点击创建后, 进入设计界面. 但是无 UI 组件可用. 因为服务不是以可视化界面的形式来运行.
但是使用设计界面可以添加安装对象, 性能计数器, 以及事件日志等其他组件.
选择服务属性, 可以打开 Properties
对话框. 可配置:
AutoLog
表示将停止与启动的事件自动写入事件日志中.CanPauseAndContinue
,CanShutdown
,CanStop
用于配置可用的请求.ServiceName
表示写入注册表的服务名称. 使用这个名称可以控制服务.CanHandleSessionChangeEvent
确定服务是否可以处理终端服务器会话中的改变事件.CanHandlePowerEvent
常用与移动设备, 用于支持电源事件.
模板创建的服务名均为
Server1
. 记得将其修改为合适的名字.因为 Windows 中服务不允许重名.
服务的代码结构与 WinForm 的代码结构类似. 也是用了 InitializeComponent()
方法来进行用户定义初始化数据 ( 构造时触发 ).
// QuoteServer 继承自 ServiceBase
public partial class QuoteService : ServiceBase
{
public QuoteService()
{
InitializeComponent();
}
protected override void OnStart(string[] args) { }
protected override void OnStop() { }
}
1. ServiceBase
类
ServiceBase
类为 .NET Fromework 开发 Windows 服务的基类. 即所有的服务均需继承于该类. QuoteService
就需要派生于它.
QuoteService
类派生于ServiceBase
类QuoteService
类使用未归档的辅助类System.ServiceProcess.NativeMethods
与 SCM 通信.System.ServiceProcess.NativeMethods
为 WindowsAPI 调用的包装类. 为内部类, 无法在代码中使用.
participant SCM as a
participant QuoteService as b
participant ServiceBase as c
participant NativeMethods as d
a -> b : Main()
b -> c : Run()
note right of d: 注册 ServiceMainCallback() 方法
c -> d : StartServiceCtrlDispatcher()
b -> c : ServiceMainCallback()
note right of d : 在 SCM 中注册处理程序\n在 SCM 中设置服务状态
c -> d : RegisterServiceCtrlHandler[Ex]()
c -> b : OnStart()
note left of a : 对服务请求停止
a -> c : ServiceCommandCallbacl()
c --> b : OnPause()
c --> b : OnContinue()
c --> b : OnCustomCommand()
c --> b : OnPowerEvent()
c --> b : OnStop()
- SCM 启动服务进程. 启动时, 调用
Main()
方法. - 在示例服务的
Main()
方法中, 调用ServiceBase
基类的Run()
方法. Run()
方法使用 SCM 中的NativeMethods.StartServiceCtrlDispatcher()
方法注册ServiceMainCallback()
方法. 并将记录写入日志.- SCM 在服务程序中调用注册的
ServiceMainCallback()
. ServiceMainCallback()
使用NativeMethods.RegisterServiceCtrlHandler[Ex]()
在 SCM 中注册处理程序. 并在 SCM 中设置服务状态.- 然后调用
OnStart()
方法. 因此必须在OnStart()
方法中实现启动代码. - 如果
OnStart()
启动成功, 将"Service started successfully"
写入到事件日志中. - 处理程序是在
ServiceCommandCallback()
方法中实现的. - 然后再调用
OnStop()
,OnPause()
,OnContinue()
,OnCustomCommand()
, 或OnPowerEvent()
.
2. 主函数
该代码初期有模板生成:
/// <summary>
/// 应用程序的主入口点。
/// </summary>
static void Main()
{
// 如果需要执行多个服务, 则创建多个 `:ServiceBase` 实例.
ServiceBase[] ServicesToRun = new ServiceBase[] { new QuoteService() };
// 该方法可以将 SCM 引用提供给服务的入口点
ServiceBase.Run(ServicesToRun); // 进程现处于阻塞状态, 等待服务结束.
}
如果只有一个服务, 可以使用:
ServiceBase.Run(new QuoteService());
- 如果在服务应用程序中需要提供多个服务, 并提供共享数据, 必须在执行
Run()
方法之前完成初始化. Run()
方法会阻塞当前主线程. 直到服务执行结束. 即其后的代码无法运行.- 初始化的时间需要限制在 30 秒以内. 如果超时 SCM 会认定服务启动失败.
- 对于需要耗时的初始化:
- 使用单独线程进行初始化, 以便及时调用
Run()
方法. - 初始化完成后使用信号通信的方式通知初始化完成.
- 使用单独线程进行初始化, 以便及时调用
3. 服务的启动
启动服务调用 OnStart()
方法. 即可启动 Socket
服务 ( QuoteServer
).
OnStart()
不能阻塞, OnStart()
方法必须返回给调用者 ( ServiceMainCallback()
方法 )
ServiceBase
类:
- 注册处理程序 ( 通过
ServiceMainCallback()
) - 调用
OnStart()
方法. - 将服务成功启动的消息传递给 SCM.
4. 处理程序方法
控制启动的方法 OnStart()
方法. 在方法中实现启动的逻辑.
控制结束的方法 OnStop()
.
除此之外还可以重写 OnPause()
, OnContinue()
, OnShutDown()
, OnPowerEvent()
, OnCustomCommand()
等.
OnPause()
- 服务暂停时调用该方法.OnContinue()
- 服务从暂停状态返回到正常操作时触发该方法. 需要在属性对话框中设置CanPauseAndContinue
为true
.OnShutdown()
- 关闭 win 系统时触发. 通常逻辑与OnStop()
一致, 必要时可以申请更多时间. 需要设置CanShutdown
属性.OnPowerEvent()
- 电源信息可以访问PowerBroadcastStatus
枚举.OnCustomCommand()
- 该处理程序可以为访问控制程序发送过来的自定义命令提供响应. 该方法接收一个int
参数. 取值为128 ~ 256
(0 ~ 127
保留 )
protected override void OnStop() => _quoteServer.Stop();
protected override void OnPause() => _quoteServer.Suspend();
protected override void OnContinue() => _quoteServer.Resume();
const int CommandRefresh = 128;
protected override void OnCustomCommand(int command)
{
switch (command)
{
case CommandRefresh: _quoteServer.Refresh(); break;
default: break;
}
}
5. 线程化和服务
必要时在 OnStart()
中开启线程来初始化.
3.5 服务的安装
服务必须在注册表中配置 ( HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services
).
使用 System.ServiceProcess
中的安装程序类.
3.6 安装程序
步骤:
切换到 VS 的 设计视图.
从上下文菜单中选择
Add Installer
( 添加安装程序 ) 选项.新建一个
ProjectInstaller
类, 一个ServiceInstaller
实例, 以及一个ServiceProcessInstaller
实例.
1. 安装程序类
ProjectInstaller
派生自 System.Configuration.Install.Installer
( 所有自定义安装程序的基类, 可构建基于事务的安装程序 ).
Installer
类中提供:
Install()
方法UnInstall()
方法Commit()
方法Rollback()
方法
这些方法都会从安装程序中调用.
如果
RunInstaller
特性的值为true
, 则在安装程序集时调用ProjectInstaller
类.自定义安装程序, 与 installutil.exe 均可检查该特性.
[RunInstaller(true)]
public partial class ProjectInstaller : System.Configuration.Install.Installer
{
public ProjectInstaller()
{
InitializeComponent();
}
}
2. ServiceProcessInstaller 类和 ServiceInstaller 类
在 ProjectInstaller
的 InitializeComponent()
方法中初始化了
ServiceProcessInstaller
类型的serviceProcessInstaller1
. 该类型派生自ComponentInstaller
.ServiceInstaller
类型的serviceInstaller1
. 该类型也派生自ComponentInstaller
.
而 ComponentInstaller
又派生自 Installer
.
ComponentInstaller
类的派生类可以用做安装进程的一部分.
ServiceProcessInstaller
用于配置进程, 为这个进程中的服务定义值.ServiceInstaller
用于服务 ( 功能 ) 的配置. 因此每一个服务 ( 功能 ) 都需要提供该类的实例.- 一个服务进程 ( 一个服务应用 ) 可以包含多个服务 ( 功能 ). 即有几个 服务 ( 功能 ) 就需要提供几个
ServiceInstaller
实例.
ServiceProcessInstaller
ServiceProcessInstaller
类安装一个实现 ServiceBase
类的可执行文件. 该类包含用于整个进程的属性.
由进程中所有服务 ( 功能 ) 共享属性有:
属性 | 描述 |
---|---|
Username , Password | 若将 Account 设置为 ServiceAccount.User , 则 Username 和 Password 用于指出服务在哪一个账户下运行. |
Account | 该属性用于指定服务的账户类型. |
HelpText | 只读属性, 返回帮助文本, 用于设置用户名和密码. |
Account
属性为 ServiceAccount
枚举:
public enum ServiceAccount
{
LocalService,
NetworkService,
LocalSystem,
User
}
LocalSystem
- 指定服务在本地系统上使用权限很高的用户账户, 并用作网络上的计算机.NetworkService
- 将计算机证书传递给远程服务器. 该服务可以以非授权用户身份登录本地系统. 该账户只能用于需要从网络上获得资源的服务.LocalService
- 该账户给任意远程服务器提供计算机的匿名证书, 本地权限与NetworkService
相同.User
- 表示可以指定从服务中使用的账户.
ServiceInstaller
ServiceInstaller
是每一个服务 ( 功能 ) 都需要使用的类. 其属性有:
属性 | 描述 |
---|---|
StartType | 用于描述服务是手动启动, 还是自动启动. 其值可以是: - ServiceStartMode.Automatic - ServiceStartMode.Manual - ServiceStartMode.Disabled 若使用 Disabled 表示服务不允许启动. 例如在硬件不支持的时候可以使用. |
DelayedAutoStart | 指明服务是在系统启动时立即启动, 还是以后启动. 若使用了 ServiceStartMode.Automatic , 则忽略该设置. |
DisplayName | 友好名称. 也用于管理工具. |
ServiceName | 服务的名称. 该名必须与 ServiceBase 类中的 ServiceName 一致. 该名称将 ServiceInstaller 的配置与服务程序关联起来. |
ServiceDependentOn | 指定在服务启动前必须启动的一组服务. |
若在
ServiceBase
的派生类中修改了服务的名字. 则必须修改ServiceName
属性值.建议开发阶段将启动类型设置为手动, 这样即使服务中有 bug 也不会影响系统的启动.
3. ServiceInstallerDialog
类
该类位于 System.ServiceProcess.Design
命名空间中.
如果在安装服务过程中需要输入用户名与密码, 则使用该类.
如果将
Account
设置为ServiceAccount.User
, 并将Username
和Password
设置为null
. 在安装时就会自动显示Set Service Login
对话框.
4. installutil
把安装程序添加到项目后. 可以使用 installutil.exe 来安装和卸载服务.
它可以安装 包含 Installer
类的所有程序集. 它会:
- 调用
Installer
派生类的Install()
方法来进行安装. - 调用
UnInstall()
方法来卸载.
其命令为:
> installutil quoteservice.exe
> installutil /u quoteservice.exe
如果安装失败, 可以检查日志.
安装成功后, 即可在 MCC 中维护.