通过


教程:创建 Windows 服务应用

本文演示如何在 Visual Studio 中创建 .NET Framework Windows 服务应用。 服务只需将消息写入事件日志。

注释

本文不适用于 .NET 中的托管服务。 有关使用 Microsoft.Extensions.Hosting.BackgroundService 和工作服务模板的 Windows 服务的最新内容,请参阅:

创建服务

若要开始,请创建项目并设置服务正常运行所需的值。

  1. 在 Visual Studio 文件 菜单中,选择“ 新建>项目 ”(或按 Ctrl+Shift+N)打开 “新建项目 ”窗口。

  2. 查找并选择 Windows 服务(.NET Framework) 项目模板。

    注释

    如果未看到 Windows 服务 模板,可能需要使用 Visual Studio Installer 安装 .NET 桌面开发 工作负载。

  3. 对于 项目名称,请输入 MyNewService,然后选择“ 创建”。

    将显示 “设计 ”选项卡(Service1.cs[设计]Service1.vb[设计])。

    项目模板包括一个名为Service1并继承自System.ServiceProcess.ServiceBase的组件类。 它包括许多基本服务代码,例如启动服务的代码。

重命名服务

将服务从 Service1 重命名为 MyNewService

  1. 解决方案资源管理器中,选择 Service1.csService1.vb,然后从快捷菜单中选择“ 重命名 ”。 将文件重命名为 MyNewService.csMyNewService.vb,然后按 Enter

    此时会显示一个弹出窗口,询问是否要重命名对代码元素 Service1 的所有引用。

  2. 在弹出窗口中,选择“ ”。

    重命名提示

  3. “文件”菜单中选择“全部保存”。

向服务添加功能

在本部分中,将自定义事件日志添加到 Windows 服务。 该 EventLog 组件是可以添加到 Windows 服务的组件类型的示例。

添加自定义事件日志功能

  1. “工具箱” 窗口中,展开 “组件”,然后将 EventLog 组件拖到 Service1.cs [设计]Service1.vb [设计] 设计器。

    小窍门

    如果未看到“工具箱”窗口,请选择“ 视图>工具箱”。

  2. 解决方案资源管理器中,从 MyNewService.csMyNewService.vb的快捷菜单中,选择 “查看代码”。

  3. 定义自定义事件日志。

    对于 C#,请编辑现有 MyNewService() 构造函数,如以下代码片段所示。 对于 Visual Basic,请添加 New() 构造函数,如以下代码片段所示。

    public MyNewService()
    {
        InitializeComponent();
        eventLog1 = new EventLog();
        if (!EventLog.SourceExists("MySource"))
        {
            EventLog.CreateEventSource("MySource", "MyNewLog");
        }
        eventLog1.Source = "MySource";
        eventLog1.Log = "MyNewLog";
    }
    
    ' To access the constructor in Visual Basic, select New from the
    ' method name drop-down list. 
    Public Sub New()
        MyBase.New()
        InitializeComponent()
        Me.EventLog1 = New System.Diagnostics.EventLog
        If Not System.Diagnostics.EventLog.SourceExists("MySource") Then
            System.Diagnostics.EventLog.CreateEventSource("MySource",
            "MyNewLog")
        End If
        EventLog1.Source = "MySource"
        EventLog1.Log = "MyNewLog"
    End Sub
    
  4. 如果尚不存在,请为命名空间添加using指令到MyNewService.cs,或添加Imports语句到MyNewService.vb

    using System.Diagnostics;
    
    Imports System.Diagnostics
    
  5. “文件”菜单中选择“全部保存”。

定义服务启动时发生的情况

MyNewService.csMyNewService.vb的代码编辑器中,找到该方法 OnStart 。 创建项目时,Visual Studio 会自动创建一个空的方法定义。 添加在服务启动时将条目写入事件日志的代码:

protected override void OnStart(string[] args)
{
    eventLog1.WriteEntry("In OnStart.");
}
' To access the OnStart in Visual Basic, select OnStart from the
' method name drop-down list. 
Protected Overrides Sub OnStart(ByVal args() As String)
    EventLog1.WriteEntry("In OnStart")
End Sub

投票

由于服务应用程序被设计为长时间运行,因此通常会轮询或监控在 OnStart 方法中设置的系统。 该方法 OnStart 必须在服务开始运行后返回到操作系统,以便系统不会被阻止。

若要设置简单的轮询机制,请使用组件 System.Timers.Timer 。 计时器会定期引发 Elapsed 事件,此时服务可以执行监控操作。 使用 Timer 组件,如下所示:

  • MyNewService.OnStart方法中设置Timer组件的属性。
  • 通过调用 Start 该方法启动计时器。
设置轮询机制
  1. 为了命名空间,在 MyNewService.cs 中添加using指令,或者在 MyNewService.vb 中添加Imports语句:

    using System.Timers;
    
    Imports System.Timers
    
  2. MyNewService.OnStart 事件中添加以下代码以设置轮询机制:

    // Set up a timer that triggers every minute.
    Timer timer = new Timer
    {
        Interval = 60000 // 60 seconds
    };
    timer.Elapsed += new ElapsedEventHandler(this.OnTimer);
    timer.Start();
    
    ' Set up a timer that triggers every minute.
    Dim timer As Timer = New Timer()
    timer.Interval = 60000 ' 60 seconds
    AddHandler timer.Elapsed, AddressOf Me.OnTimer
    timer.Start()
    
  3. 在类中添加 MyNewService 成员变量。 它包含要写入事件日志的下一个事件的标识符:

    private int eventId = 1;
    
    Private eventId As Integer = 1
    
  4. 在类中 MyNewService ,添加 OnTimer 用于处理 Timer.Elapsed 事件的方法:

    public void OnTimer(object sender, ElapsedEventArgs args)
    {
        // TODO: Insert monitoring activities here.
        eventLog1.WriteEntry("Monitoring the System", EventLogEntryType.Information, eventId++);
    }
    
    Private Sub OnTimer(sender As Object, e As Timers.ElapsedEventArgs)
       ' TODO: Insert monitoring activities here.
       eventLog1.WriteEntry("Monitoring the System", EventLogEntryType.Information, eventId)
       eventId = eventId + 1
    End Sub
    

可以使用后台工作线程运行任务,而不是在主线程上运行所有工作。 有关详细信息,请参阅 System.ComponentModel.BackgroundWorker

定义停止服务时发生的情况

OnStop 方法中插入一行代码,在停止服务时向事件日志中添加一个条目:

protected override void OnStop()
{
    eventLog1.WriteEntry("In OnStop.");
}
Protected Overrides Sub OnStop()
    EventLog1.WriteEntry("In OnStop.")
End Sub

定义服务的其他操作

可以重写OnPauseOnContinueOnShutdown方法,以便为组件定义其他处理。

以下代码演示如何重写 OnContinue 类中的 MyNewService 方法:

protected override void OnContinue()
{
    eventLog1.WriteEntry("In OnContinue.");
}
Protected Overrides Sub OnContinue()
    EventLog1.WriteEntry("In OnContinue.")
End Sub

设置服务状态

服务将其状态报告给 服务控制管理器 ,以便用户可以判断服务是否正常运行。 默认情况下,从 ServiceBase 继承的服务会报告一组有限的状态设置,其中包括 SERVICE_STOPPEDSERVICE_PAUSEDSERVICE_RUNNING。 如果服务需要一段时间才能启动,则报告 SERVICE_START_PENDING 状态非常有用。

可以通过添加调用 Windows SERVICE_START_PENDING 函数的代码来实现SERVICE_STOP_PENDING状态设置。

  1. 为了命名空间,在 MyNewService.cs 中添加using指令,或者在 MyNewService.vb 中添加Imports语句:

    using System.Runtime.InteropServices;
    
    Imports System.Runtime.InteropServices
    
  2. 将以下枚举和结构添加到 MyNewService.csMyNewService.vb,以声明 ServiceState 值并为状态添加结构,你将在平台调用中使用:

    public enum ServiceState
    {
        SERVICE_STOPPED = 0x00000001,
        SERVICE_START_PENDING = 0x00000002,
        SERVICE_STOP_PENDING = 0x00000003,
        SERVICE_RUNNING = 0x00000004,
        SERVICE_CONTINUE_PENDING = 0x00000005,
        SERVICE_PAUSE_PENDING = 0x00000006,
        SERVICE_PAUSED = 0x00000007,
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct ServiceStatus
    {
        public int dwServiceType;
        public ServiceState dwCurrentState;
        public int dwControlsAccepted;
        public int dwWin32ExitCode;
        public int dwServiceSpecificExitCode;
        public int dwCheckPoint;
        public int dwWaitHint;
    };
    
    Public Enum ServiceState
        SERVICE_STOPPED = 1
        SERVICE_START_PENDING = 2
        SERVICE_STOP_PENDING = 3
        SERVICE_RUNNING = 4
        SERVICE_CONTINUE_PENDING = 5
        SERVICE_PAUSE_PENDING = 6
        SERVICE_PAUSED = 7
    End Enum
    
    <StructLayout(LayoutKind.Sequential)>
    Public Structure ServiceStatus
        Public dwServiceType As Long
        Public dwCurrentState As ServiceState
        Public dwControlsAccepted As Long
        Public dwWin32ExitCode As Long
        Public dwServiceSpecificExitCode As Long
        Public dwCheckPoint As Long
        Public dwWaitHint As Long
    End Structure
    

    注释

    服务控制管理器使用dwWaitHintdwCheckpoint成员来确定等待 Windows 服务启动或关闭的时间。 如果OnStartOnStop方法运行时间较长,可以通过再次调用SetServiceStatus并使用递增的dwCheckPoint值来请求服务更多的时间。

  3. 在类中MyNewService,使用平台调用声明 SetServiceStatus 函数:

    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool SetServiceStatus(System.IntPtr handle, ref ServiceStatus serviceStatus);
    
    Declare Auto Function SetServiceStatus Lib "advapi32.dll" (ByVal handle As IntPtr, ByRef serviceStatus As ServiceStatus) As Boolean
    
  4. 若要实现 SERVICE_START_PENDING 状态,请将以下代码添加到方法的 OnStart 开头:

    // Update the service state to Start Pending.
    ServiceStatus serviceStatus = new ServiceStatus
    {
        dwCurrentState = ServiceState.SERVICE_START_PENDING,
        dwWaitHint = 100000
    };
    SetServiceStatus(this.ServiceHandle, ref serviceStatus);
    
    ' Update the service state to Start Pending.
    Dim serviceStatus As ServiceStatus = New ServiceStatus()
    serviceStatus.dwCurrentState = ServiceState.SERVICE_START_PENDING
    serviceStatus.dwWaitHint = 100000
    SetServiceStatus(Me.ServiceHandle, serviceStatus)
    
  5. 将代码添加到方法末尾 OnStart ,将状态设置为 SERVICE_RUNNING

    // Update the service state to Running.
    serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING;
    SetServiceStatus(this.ServiceHandle, ref serviceStatus);
    
    ' Update the service state to Running.
    serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING
    SetServiceStatus(Me.ServiceHandle, serviceStatus)
    
  6. (可选)如果是 OnStop 长时间运行的方法,则在 OnStop 该方法中重复此过程。 在方法退出前SERVICE_STOP_PENDING实现状态并返回SERVICE_STOPPED状态OnStop

    例如:

    // Update the service state to Stop Pending.
    ServiceStatus serviceStatus = new ServiceStatus
    {
        dwCurrentState = ServiceState.SERVICE_STOP_PENDING,
        dwWaitHint = 100000
    };
    SetServiceStatus(this.ServiceHandle, ref serviceStatus);
    
    // Update the service state to Stopped.
    serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED;
    SetServiceStatus(this.ServiceHandle, ref serviceStatus);
    
    ' Update the service state to Stop Pending.
    Dim serviceStatus As ServiceStatus = New ServiceStatus()
    serviceStatus.dwCurrentState = ServiceState.SERVICE_STOP_PENDING
    serviceStatus.dwWaitHint = 100000
    SetServiceStatus(Me.ServiceHandle, serviceStatus)
    
    ' Update the service state to Stopped.
    serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED
    SetServiceStatus(Me.ServiceHandle, serviceStatus)
    

将安装程序添加到服务

在运行 Windows 服务之前,需要安装它,它将注册到服务控制管理器。 将安装程序添加到项目以处理注册详细信息。

  1. 解决方案资源管理器中,从 MyNewService.csMyNewService.vb的快捷菜单中,选择 “视图设计器”。

  2. “设计 ”视图中,选择背景区域,然后从快捷菜单中选择 “添加安装程序 ”。

    默认情况下,Visual Studio 会向项目添加一个名为“包含两个安装程序”的 ProjectInstaller组件类。 这些安装程序适用于你的服务和服务的关联进程。

  3. ProjectInstaller的设计视图中,为 C# 项目选择 serviceInstaller1,或者为 Visual Basic 项目选择 ServiceInstaller1,然后从快捷菜单中选择“属性”。

  4. “属性” 窗口中,将 ServiceName 属性设置为 MyNewService

  5. 向属性添加文本 Description ,例如 示例服务

    此文本显示在“服务”窗口的“说明”列中,并向用户描述服务。

    服务窗口中的服务说明。

  6. DisplayName 属性添加文本。 例如 MyNewService 显示名称

    此文本显示在“服务”窗口的“显示名称”列中。 此名称可以不同于 ServiceName 属性,这是系统使用的名称(例如,用于 net start 启动服务的命令的名称)。

  7. StartType 属性从下拉列表中设置为 Automatic

  8. 完成后, “属性” 窗口应如下图所示:

    Windows 服务的安装程序属性

  9. ProjectInstaller的设计视图中,为 C# 项目选择 serviceProcessInstaller1,或者为 Visual Basic 项目选择 ServiceProcessInstaller1,然后从快捷菜单中选择“属性”。 将 Account 属性设置为 LocalSystem,从下拉列表中选择。

    此设置安装该服务并使用本地系统帐户运行该服务。

    重要

    LocalSystem 帐户具有广泛的权限,包括写入事件日志的功能。 请谨慎使用此帐户,因为它可能会增加来自恶意软件的攻击风险。 对于其他任务,请考虑使用 LocalService 帐户,该帐户充当本地计算机上的非特权用户,并向任何远程服务器提供匿名凭据。 但是,如果尝试使用该 LocalService 帐户,此示例将失败,因为它需要写入事件日志的权限。

有关安装程序的详细信息,请参阅 如何:将安装程序添加到服务应用程序

(可选)设置启动参数

注释

在决定添加启动参数之前,请考虑是否是将信息传递给服务的最佳方式。 尽管它们易于使用和分析,并且用户可以轻松替代它们,但用户可能很难在没有文档的情况下发现和使用它们。 通常,如果服务只需要几个启动参数,则应改用注册表或配置文件。

Windows 服务可以接受命令行参数,也称为 启动参数。 添加代码来处理启动参数时,用户可以在服务属性窗口中使用自己的自定义启动参数启动服务。 但是,下次服务启动时,这些启动参数不会持久化。

若要永久设置启动参数,请在注册表中设置它们。 每个 Windows 服务在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services 子项下都有一个注册表项。 在每个服务的子项下,使用 Parameters 子项来存储服务可以访问的信息。 可以将应用程序配置文件用于 Windows 服务,就像对其他类型的程序一样。 有关示例代码,请参阅 ConfigurationManager.AppSettings

添加启动参数

  1. MyNewService.csMyNewService.vb中,更改 MyNewService 构造函数以接受和处理输入参数:

    public MyNewService(string[] args)
    {
        InitializeComponent();
    
        string eventSourceName = "MySource";
        string logName = "MyNewLog";
    
        if (args.Length > 0)
        {
            eventSourceName = args[0];
        }
    
        if (args.Length > 1)
        {
            logName = args[1];
        }
    
        eventLog1 = new EventLog();
    
        if (!EventLog.SourceExists(eventSourceName))
        {
            EventLog.CreateEventSource(eventSourceName, logName);
        }
    
        eventLog1.Source = eventSourceName;
        eventLog1.Log = logName;
    }
    
    Public Sub New(ByVal cmdArgs() As String)
        InitializeComponent()
        Dim eventSourceName As String = "MySource"
        Dim logName As String = "MyNewLog"
        If (cmdArgs.Count() > 0) Then
            eventSourceName = cmdArgs(0)
        End If
        If (cmdArgs.Count() > 1) Then
            logName = cmdArgs(1)
        End If
        eventLog1 = New EventLog()
        If (Not EventLog.SourceExists(eventSourceName)) Then
            EventLog.CreateEventSource(eventSourceName, logName)
        End If
        eventLog1.Source = eventSourceName
        eventLog1.Log = logName
    End Sub
    

    此代码根据用户提供的启动参数设置事件源和日志名称。 如果未提供任何参数,则使用默认值。

  2. 选择 Program.csMyNewService.Designer.vb,然后从快捷菜单中选择 “查看代码 ”。 在方法中 Main ,更改代码以添加输入参数并将其传递给服务构造函数:

    static void Main(string[] args)
    {
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[]
        {
            new MyNewService(args)
        };
        ServiceBase.Run(ServicesToRun);
    }
    
    Shared Sub Main(ByVal cmdArgs() As String)
        Dim ServicesToRun() As System.ServiceProcess.ServiceBase = New System.ServiceProcess.ServiceBase() {New MyNewService(cmdArgs)}
        System.ServiceProcess.ServiceBase.Run(ServicesToRun)
    End Sub
    
  3. 若要指定命令行参数,请将以下代码添加到 ProjectInstallerProjectInstaller.cs 中的类,或 ProjectInstaller.vb

    protected override void OnBeforeInstall(IDictionary savedState)
    {
        string parameter = "MySource1\" \"MyLogFile1";
        Context.Parameters["assemblypath"] = "\"" + Context.Parameters["assemblypath"] + "\" \"" + parameter + "\"";
        base.OnBeforeInstall(savedState);
    }
    
    Protected Overrides Sub OnBeforeInstall(ByVal savedState As IDictionary)
        Dim parameter As String = "MySource1"" ""MyLogFile1"
        Context.Parameters("assemblypath") = """" + Context.Parameters("assemblypath") + """ """ + parameter + """"
        MyBase.OnBeforeInstall(savedState)
    End Sub
    

    通常,此值包含 Windows 服务的可执行文件的完整路径。 若要使服务正确启动,用户必须提供路径和每个单独参数的引号。 用户可以更改 ImagePath 注册表项中的参数,以更改 Windows 服务的启动参数。 但是,更好的方法是以编程方式更改值,以用户友好的方式公开功能,例如使用管理或配置实用工具。

构建服务

  1. 解决方案资源管理器中,从 MyNewService 项目的快捷菜单中选择“属性”。

  2. “应用程序 ”选项卡上的 “启动对象 ”列表中,选择 “MyNewService.Program ”(或 Visual Basic 项目的 Sub Main )。

  3. 若要生成项目,请在 解决方案资源管理器中,从项目的快捷菜单中选择 “生成 ”(或按 Ctrl+Shift+B)。

安装服务

生成 Windows 服务后,即可安装它。 若要安装 Windows 服务,必须在安装了 Windows 服务的计算机上拥有管理员凭据。

  1. 使用管理凭据 打开 Visual Studio 的开发人员命令提示符

  2. Visual Studio 开发人员命令提示符中,导航到包含项目输出的文件夹(默认情况下,项目 \bin\Debug 子目录)。

  3. 输入以下命令:

    installutil MyNewService.exe
    

    如果服务成功安装,命令将报告成功。

    如果系统找不到 installutil.exe,请确保计算机上存在它。 此工具随 .NET Framework 一起安装到 \Microsoft.NET\Framework[64]\<framework 版本>的文件夹%windir%

    如果 installutil.exe 进程失败,请检查安装日志以了解原因。 默认情况下,日志与服务可执行文件位于同一文件夹中。 安装可能会失败,如果:

    • RunInstallerAttribute 类不存在于 ProjectInstaller 该类中。
    • 属性未设置为 true.
    • ProjectInstaller 类未定义为 public.
    • 未以管理员身份打开 VS 开发人员命令提示符。

有关详细信息,请参阅 如何:安装和卸载服务

启动并运行服务

  1. 在 Windows 中,打开 Services 桌面应用:按 Windows+R 打开 “运行 ”框,输入 services.msc,然后按 Enter 或选择“ 确定”。

    应会看到 服务中列出的服务,按为其设置的显示名称按字母顺序显示。

    服务窗口中的 MyNewService。

  2. 若要启动服务,请从服务的快捷菜单中选择 “开始 ”。

  3. 若要停止服务,请从服务的快捷菜单中选择 “停止 ”。

  4. (可选)在命令行中,使用命令 net start <服务名称和>net stop <服务名称> 启动和停止服务。

验证服务的事件日志输出

  1. 在 Windows 中,打开 事件查看器 桌面应用:在 Windows 搜索栏中输入 事件查看器 ,然后从搜索结果中选择 事件查看器

    小窍门

    在 Visual Studio 中,可以通过从“视图”菜单(或按 Ctrl++)打开服务器资源管理器并展开本地计算机的事件日志节点来访问事件日志

  2. 事件查看器中,展开 “应用程序和服务日志”。

  3. 如果遵循了添加命令行参数的过程,请找到 MyNewLog (或 MyLogFile1 )的列表并展开它。 你应该能看到你的服务执行的两个动作(启动和停止)的条目。

    使用事件查看器查看事件日志条目

清理资源

如果不再需要 Windows 服务应用,可以将其删除。

  1. 使用管理凭据 打开 Visual Studio 的开发人员命令提示符

  2. Visual Studio 开发人员命令提示符 窗口中,导航到包含项目的可执行文件的文件夹。

  3. 输入以下命令:

    installutil.exe /u MyNewService.exe
    

    如果服务成功卸载,命令将报告已成功删除服务。 有关详细信息,请参阅 如何:安装和卸载服务

后续步骤

创建服务后,可以:

  • 创建独立安装程序供其他人用来安装 Windows 服务。 使用 WiX 工具集 为 Windows 服务创建安装程序。 有关其他想法,请参阅 “创建安装程序包”。

  • 浏览组件 ServiceController ,使你能够将命令发送到已安装的服务。

  • 安装应用程序时,使用安装程序创建事件日志,而不是在应用程序运行时创建事件日志。 卸载应用程序时,安装程序会删除事件日志。 有关详细信息,请参阅 EventLogInstaller

另请参阅