jk's notes
  • Windows 服务

Windows 服务

注意服务这个概念:

  1. 有时表示服务这个抽象名词
  2. 有时表示服务程序
  3. 有时表示所提供的一个服务 ( 功能 ).

1. Windows 服务

介绍什么是 win 服务, 及其特性.

并列举一些案例.

介绍服务资源管理器.

2. Windows 服务体系结构

操作 win 服务需要三种程序:

  1. 服务程序
  2. 服务控制程序
  3. 服务配置程序
  • 服务程序
    • 提供需要的实际功能
  • 服务控制程序
    • 将控制请求发送给服务, 例如开始, 暂停与继续等.
  • 服务配置程序
    • 用于安装服务
      • 需要将程序复制到文件系统中
      • 需要将服务信息写入注册表 ( 注册信息由服务控制管理器 ( 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: 注册处理程序

如设置服务自动启动:

  1. 系统启动, 启动该服务每一个进程
  2. 调用进程的主函数
  3. 该服务应用为每一项服务功能注册 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.

image-20201111102539158

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 描述:

  1. 创建的服务驻留在引用服务器内.
  2. 对于客户端的请求, 引用服务器会返回引用文件的随机引用.

解决方案包含 3 个程序集.

  1. 一个用于客户端
  2. 两个用于服务器
  • 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 创建核心服务功能

步骤:

  1. 创建 QuoteServer 类库. 实现服务器代码.
  2. 重载 QuoteServer() 构造函数, 传入文件名与端口号.
  3. 提供 ReadQuotes() 辅助方法, 用于从文件中读取所有引用, 创建 Readom 实例.
  4. 提供 GetRandomQuoteOfTheDay(), 来返回随机引用.
  5. 提供 ListenerAsync() 任务.
  6. 提供 Start() 方法, Stop(), Suspend(), Resume() 方法.
  7. 提供 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 ). 核心逻辑为:

  1. 点击按钮, 获得服务地址 ( localhost ), 与 端口号 ( 7890 ).
  2. 创建 TcpClient 类. 连接服务器 ( ConnectAsync() ).
  3. 获得数据 ( 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 常用与移动设备, 用于支持电源事件.

image-20201111134005072

模板创建的服务名均为 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()


  1. SCM 启动服务进程. 启动时, 调用 Main() 方法.
  2. 在示例服务的 Main() 方法中, 调用 ServiceBase 基类的 Run() 方法.
  3. Run() 方法使用 SCM 中的 NativeMethods.StartServiceCtrlDispatcher() 方法注册 ServiceMainCallback() 方法. 并将记录写入日志.
  4. SCM 在服务程序中调用注册的 ServiceMainCallback().
  5. ServiceMainCallback() 使用 NativeMethods.RegisterServiceCtrlHandler[Ex]() 在 SCM 中注册处理程序. 并在 SCM 中设置服务状态.
  6. 然后调用 OnStart() 方法. 因此必须在 OnStart() 方法中实现启动代码.
  7. 如果 OnStart() 启动成功, 将 "Service started successfully" 写入到事件日志中.
  8. 处理程序是在 ServiceCommandCallback() 方法中实现的.
  9. 然后再调用 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 类:

  1. 注册处理程序 ( 通过 ServiceMainCallback() )
  2. 调用 OnStart() 方法.
  3. 将服务成功启动的消息传递给 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 ).

image-20201111154249285

使用 System.ServiceProcess 中的安装程序类.

3.6 安装程序

步骤:

  1. 切换到 VS 的 设计视图.

  2. 从上下文菜单中选择 Add Installer ( 添加安装程序 ) 选项.

    image-20201111155056639

  3. 新建一个 ProjectInstaller 类, 一个 ServiceInstaller 实例, 以及一个 ServiceProcessInstaller 实例.

    image-20201111154936053

image-20201111155330854

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 中维护.

4. Windows 服务的监控和控制

Last Updated:
Contributors: jk