本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#是Windows平台应用开发的重要语言,尤其适用于Windows服务的构建。本DEMO项目“C#开发Windows服务DEMO”通过实际案例讲解如何使用C#创建、安装、控制和部署Windows后台服务。内容涵盖ServiceBase类的继承与方法重写、ServiceController类的服务控制机制、使用InstallUtil工具进行服务安装与卸载,以及通过批处理脚本自动化部署。配套的详细说明文档帮助开发者掌握在Visual Studio中配置、调试和发布服务的全流程。该项目为希望实现系统级后台任务管理的C#开发者提供了实用的学习资源和开发模板。

Windows 服务开发从零到上线:C# 全流程实战指南

哎呀,又一个开发者在凌晨三点对着“Error 1053”抓耳挠腮了 😅。这场景太熟悉不过啦——你辛辛苦苦写完了一个后台任务,满怀期待地运行 installutil ,结果系统冷冰冰地告诉你:“服务未能及时响应启动请求”。别急,咱们今天就来把这套“玄学”变成科学!✨

Windows 服务看似神秘,其实就像一只默默耕耘的蜜蜂🐝——它不露面、不打扰用户,却在系统深处持续工作。但一旦出问题,排查起来简直让人头大。本文将带你彻底搞懂从 设计 → 编码 → 安装 → 调试 → 部署 的完整链条,手把手教你避开那些年我们都踩过的坑。

准备好了吗?Let’s dive in!👇


核心基石:理解 Windows 服务的本质

我们先别急着敲代码。想象一下:你的程序如果是一个人,那普通桌面应用就像是个爱说话的前台接待员;而 Windows 服务呢?它是那个深藏功与名、24小时值班的保安大叔 👮♂️。

它到底是什么?

简单说, Windows 服务是一种无需用户登录即可自动启动并在后台长期运行的进程 。它的生命周期由操作系统内核级组件—— 服务控制管理器(SCM, Service Control Manager) 统一调度。

// 看似简单的几行代码,背后可有不少门道哦~
public class MyService : ServiceBase
{
    protected override void OnStart(string[] args) => 
        EventLog.WriteEntry("服务已启动", EventLogEntryType.Information);

    protected override void OnStop() => 
        EventLog.WriteEntry("服务已停止", EventLogEntryType.Information);
}

⚠️ 注意:这段代码虽然能编译通过,但如果你真这么写,恭喜你,马上就会遇到经典的 Error 1053

为什么?因为 SCM 认为你的服务“卡住了”,没按时告诉它“我已经跑起来了”。这就引出了第一个核心原则:

OnStart 方法必须快速返回,不能阻塞主线程!

你可以把它看作是 SCM 打了个电话给你:“喂,该上班了!”
你只需要回答一句:“收到,我这就开始干活。”然后挂电话,自己去忙正事。而不是拿着话筒一直讲个不停,让对方等得不耐烦直接挂断。

常见应用场景有哪些?

场景 示例
日志监控 实时扫描日志文件并报警
数据同步 每隔几分钟从数据库拉取新订单
定时作业 清理临时文件、备份数据
网络通信 WebSocket 长连接心跳保活
硬件交互 读取串口传感器数据

这些任务都需要“无人值守 + 高可用”,正是 Windows 服务的用武之地。

和普通程序有啥区别?

特性 普通桌面应用 Windows 服务
用户界面 通常有 GUI ❌ 无界面
启动时机 用户双击或开机自启 系统启动时自动加载
运行账户 当前登录用户 LocalSystem / NetworkService 等高权限账户
生命周期 用户关闭即退出 受 SCM 控制,支持自动恢复
权限等级 中等 可拥有系统最高权限

所以你看,服务更像是一位“体制内”的员工——纪律严明、责任重大、权限不小,但也得守规矩。


构建骨架:深入剖析 ServiceBase 类的设计哲学

接下来我们要聊的是 .NET 开发中最关键的一个基类: System.ServiceProcess.ServiceBase 。这家伙可不是普通的抽象类,它是连接你的业务逻辑和操作系统的桥梁 🌉。

设计模式揭秘:模板方法模式登场!

ServiceBase 使用了经典的 模板方法(Template Method) 模式。什么意思呢?

就好比公司给你发了一份《员工手册》,里面规定了:
- 上班打卡 → 对应 OnStart
- 下班打卡 → 对应 OnStop
- 请假流程 → 对应 OnPause
- 销假复工 → 对应 OnContinue

你自己要做的,就是在这些规定的流程节点上填写具体内容。整个框架已经搭好,你只需专注实现细节。

核心成员一览表
成员类型 名称 描述
虚方法 OnStart(string[] args) 必须重写!启动主逻辑
虚方法 OnStop() 必须重写!释放资源
虚方法 OnPause() 可选,处理暂停请求
虚方法 OnContinue() 可选,恢复运行
虚方法 OnShutdown() 系统关机前调用
虚方法 OnCustomCommand(int command) 自定义指令(128~255)
属性 CanStop 是否允许停止?默认 false ❌
属性 CanPauseAndContinue 是否支持暂停/继续?默认 false ❌
属性 ServiceName 必须设置!注册表唯一标识

重点来了:如果你不显式设置 CanStop = true ,就算你在服务管理器里点“停止”,也会被 SCM 直接忽略!

🚨 小贴士:很多新手以为只要写了 OnStop 就行,殊不知还得手动开启开关才行。

一个完整的最小可运行示例

using System.ServiceProcess;
using System.Timers;

public class MyBackgroundService : ServiceBase
{
    private Timer _timer;

    public MyBackgroundService()
    {
        this.ServiceName = "MyHeartbeatService";
        this.CanStop = true;                    // 允许停止
        this.CanPauseAndContinue = true;       // 支持暂停
    }

    protected override void OnStart(string[] args)
    {
        _timer = new Timer(60_000); // 毎分钟一次
        _timer.Elapsed += (sender, e) => 
        {
            WriteToEventLog($"心跳信号发送于 {DateTime.Now:HH:mm:ss}");
        };
        _timer.Start();
    }

    protected override void OnStop()
    {
        _timer?.Stop();
        _timer?.Dispose();
        _timer = null;
        WriteToEventLog("服务已安全停止。");
    }

    private void WriteToEventLog(string message)
    {
        try
        {
            if (!EventLog.SourceExists(this.ServiceName))
                EventLog.CreateEventSource(this.ServiceName, "Application");

            EventLog.WriteEntry(this.ServiceName, message, EventLogEntryType.Information);
        }
        catch { /* 失败也不抛异常 */ }
    }
}

💡 解析几个易错点:
- _timer 必须声明为字段,否则会被 GC 回收;
- WriteToEventLog 加了异常捕获,避免日志失败导致服务崩溃;
- EventLog.SourceExists 判断防止重复创建源。


生命之舞:服务的五大状态流转机制

SCM 是个严格的监工,它只认五种状态,并且对每种状态之间的转换都有明确规则。不了解这个机制,你就没法写出健壮的服务。

五种状态全解析

状态 编码 含义
Stopped 0x00000001 服务未运行
Start Pending 0x00000002 正在启动中
Running 0x00000004 已正常运行
Stop Pending 0x00000003 正在停止
Paused 0x00000007 被暂停

它们之间的转换关系如下图所示:

stateDiagram-v2
    [*] --> Stopped
    Stopped --> StartPending : Start()
    StartPending --> Running : OnStart returns
    StartPending --> Stopped : Timeout or exception
    Running --> StopPending : Stop()
    Running --> Paused : Pause()
    Paused --> Running : Continue()
    StopPending --> Stopped : OnStop completes

看到没? 只有 OnStart 成功返回后,状态才会变为 Running 。这就是为什么不能在里面放死循环的原因!

如何应对超时问题?

SCM 默认给 OnStart OnStop 30秒 时间完成任务。超过这个时间,就会报错。

解决办法就是主动“汇报进度”,告诉 SCM:“老大别急,我还在努力呢!”

[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool SetServiceStatus(IntPtr hServiceStatus, ref ServiceStatus status);

[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
{
    STOPPED = 0x00000001,
    START_PENDING = 0x00000002,
    STOP_PENDING = 0x00000003,
    RUNNING = 0x00000004
}

protected override void OnStart(string[] args)
{
    var status = new ServiceStatus
    {
        dwCurrentState = ServiceState.START_PENDING,
        dwWaitHint = 60_000,  // 我预计还要等一分钟
        dwCheckPoint = 1      // 第一步:初始化数据库
    };
    SetServiceStatus(this.ServiceHandle, ref status);

    InitializeDatabase(); // 耗时操作

    status.dwCheckPoint = 2;
    status.dwWaitHint = 30_000;
    SetServiceStatus(this.ServiceHandle, ref status);

    LoadCache(); // 再来一个耗时操作

    status.dwCurrentState = ServiceState.RUNNING;
    status.dwCheckPoint = 0;
    status.dwWaitHint = 0;
    SetServiceStatus(this.ServiceHandle, ref status);
}

👏 这样一来,即使初始化花了 80 秒,SCM 也知道你是“正在路上”,不会误判为无响应。


关键实现:如何正确编写 OnStart OnStop

这部分是整篇文章的灵魂所在。无数人在这里栽过跟头,今天我们一次性把坑填平。

OnStart 的三大黄金法则

  1. 绝不阻塞主线程
  2. 尽快返回
  3. 启动异步任务
❌ 错误示范:阻塞式无限循环
protected override void OnStart(string[] args)
{
    while (true) // ⛔ 千万别这么干!
    {
        DoWork();
        Thread.Sleep(5000);
    }
}

后果: OnStart 永远不会返回 → SCM 等待超时 → Error 1053。

✅ 正确姿势一:使用 Timer
private Timer _workerTimer;

protected override void OnStart(string[] args)
{
    _workerTimer = new Timer(ExecuteWork, null, TimeSpan.FromSeconds(5), TimeSpan.FromMinutes(1));
}

private void ExecuteWork(object state)
{
    try { /* 你的业务逻辑 */ }
    catch (Exception ex) { LogError(ex); }
}

优点:轻量、高效,适合定时任务。

✅ 正确姿势二:使用 Task.Run + CancellationToken
private CancellationTokenSource _cts;

protected override void OnStart(string[] args)
{
    _cts = new CancellationTokenSource();
    Task.Run(() => BackgroundWorker(_cts.Token), _cts.Token);
}

private async Task BackgroundWorker(CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        await DoWorkAsync(ct);
        await Task.Delay(TimeSpan.FromMinutes(1), ct);
    }
}

protected override void OnStop()
{
    _cts.Cancel();
    _cts.Dispose();
}

优势:完全支持取消、异常处理更优雅,推荐用于复杂逻辑。


OnStop 的艺术:优雅退出才是高手

你以为 OnStop 就是简单 stop timer 吗?Too young too simple!

黄金准则:
  • 30秒内 完成所有清理;
  • 停止所有线程/任务;
  • 关闭数据库连接、文件流、Socket;
  • 最后写一条日志,证明你“善始善终”。
protected override void OnStop()
{
    _cts?.Cancel();

    // 给任务最多10秒时间优雅退出
    if (_runningTask != null && !_runningTask.IsCompleted)
    {
        _runningTask.Wait(TimeSpan.FromSeconds(10));
    }

    _timer?.Change(Timeout.Infinite, 0);
    _timer?.Dispose();

    // 关闭资源(顺序很重要!)
    _tcpListener?.Stop();
    _dbConnection?.Close();
    _logStream?.Flush();
    _logStream?.Dispose();

    base.OnStop();
}

📌 提醒:关闭顺序建议为 网络 → 数据 → 日志 ,不然你会发现日志都写不了了还怎么记录错误?


安装部署:用 InstallUtil.exe 把服务注册进系统

终于到了激动人心的时刻——让你的服务正式“入职”Windows 系统!

工具在哪?怎么用?

InstallUtil.exe 是 .NET Framework 自带的安装工具,路径通常是:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe

📌 记住:一定要选对版本!32位程序用 Framework ,64位用 Framework64

安装命令
InstallUtil.exe "C:\MyApp\MyService.exe"
卸载命令
InstallUtil.exe /u "C:\MyApp\MyService.exe"

注册表验证:看看服务有没有“落户成功”

安装完成后,打开注册表编辑器,查看以下路径:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MyService

你应该能看到类似这样的键值:

键名 示例值 说明
ImagePath "C:\MyApp\MyService.exe" 可执行文件位置
DisplayName My Background Service 服务管理器显示名
Start 2 2=自动启动,3=手动,4=禁用
ObjectName LocalSystem 运行账户

可以用 PowerShell 快速检查:

Get-Service MyService
# 或者
Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\MyService"

常见安装错误及解决方案

错误 原因 解法
Access is denied 权限不足 以管理员身份运行 CMD
No installers found 缺少 [RunInstaller(true)] 检查 ProjectInstaller 类
BadImageFormatException 32/64位不匹配 使用对应版本的 InstallUtil
FileNotFoundException 依赖项缺失 把 DLL 放在同一目录

安装器配置: ServiceInstaller ServiceProcessInstaller

光有服务本体还不够,你还得告诉系统:“我要怎么运行?”这就靠两个神器:

  • ServiceInstaller :管服务本身的属性(名字、描述、启动方式)
  • ServiceProcessInstaller :管运行账户(谁来跑这个服务)

可视化设计器 vs 手动编码

Visual Studio 提供了拖拽式设计器,但建议你了解底层代码逻辑:

[RunInstaller(true)]
public partial class ProjectInstaller : Installer
{
    private ServiceProcessInstaller processInstaller;
    private ServiceInstaller serviceInstaller;

    public ProjectInstaller()
    {
        processInstaller = new ServiceProcessInstaller();
        serviceInstaller = new ServiceInstaller();

        // 设置运行账户
        processInstaller.Account = ServiceAccount.LocalSystem;

        // 配置服务信息
        serviceInstaller.ServiceName = "MyDataService";
        serviceInstaller.DisplayName = "Acme Data Sync Agent";
        serviceInstaller.Description = "同步ERP与CRM系统的客户数据";
        serviceInstaller.StartType = ServiceStartMode.Automatic;

        Installers.Add(processInstaller);
        Installers.Add(serviceInstaller);
    }
}

🎯 小技巧:可以用配置文件动态设置:

serviceInstaller.ServiceName = ConfigurationManager.AppSettings["ServiceName"] ?? "DefaultSvc";

自动化部署:一键安装/卸载脚本

手动敲命令太 low 了,咱们搞个 .bat 脚本,双击就能搞定!

install.bat

@echo off
:: 请求管理员权限
set UAC=Admin
net session >nul 2>&1 || (
    echo 正在请求管理员权限...
    powershell -Command "Start-Process '%~f0' -Verb RunAs"
    exit /b
)

cd /d "%~dp0"

"%windir%\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe" MyService.exe

if %errorlevel%==0 (
    echo ✅ 安装成功!正在启动服务...
    net start MyService
) else (
    echo ❌ 安装失败,请查看日志。
)

pause

uninstall.bat

@echo off
set UAC=Admin
net session >nul 2>&1 || (
    powershell -Command "Start-Process '%~f0' -Verb RunAs"
    exit /b
)

cd /d "%~dp0"

net stop MyService >nul 2>&1
"%windir%\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe" /u MyService.exe

if %errorlevel%==0 (
    echo ✅ 卸载完成!
) else (
    echo ❌ 卸载失败,尝试使用 sc delete:
    sc delete MyService
)

pause

🎉 效果:非技术人员也能轻松部署!


调试大法:如何给“看不见”的服务找 Bug?

没有 UI 怎么调试?难道只能靠猜?

当然不是!这里有三种神仙级调试技巧👇

方法一:DEBUG 条件编译(最常用)

static void Main()
{
#if DEBUG
    var svc = new MyService();
    Console.WriteLine("【调试模式】按任意键停止...");
    svc.OnStart(null);
    Console.ReadKey();
    svc.OnStop();
#else
    ServiceBase.Run(new ServiceBase[] { new MyService() });
#endif
}

这样在本地开发时就可以当控制台程序跑了,爽歪歪~

方法二:动态附加调试器

#if DEBUG
    if (!Debugger.IsAttached)
        Debugger.Launch(); // 弹出 VS 选择框
#endif

部署到服务器后,可以通过事件日志触发断点,远程调试也不是梦!

方法三:外部测试驱动

写个独立的 Console App,引用服务 DLL,手动调用 OnStart 测试逻辑。

flowchart TD
    A[Test Console App] --> B[Load Service DLL]
    B --> C[Call OnStart Manually]
    C --> D[Observe Logs & Exceptions]
    D --> E[Fix Issues]

配合 Serilog 或 NLog 输出结构化日志,排查效率翻倍🚀


实战全流程:从创建项目到上线部署

最后来一波完整的 SOP,照着做就行!

Step 1:创建项目

  • Visual Studio → 新建项目 → “Windows Service (.NET Framework)”
  • 添加引用: System.ServiceProcess , System.Configuration.Install

Step 2:编写服务逻辑

  • 继承 ServiceBase
  • 重写 OnStart / OnStop
  • 使用 Timer Task 执行周期任务

Step 3:配置安装器

  • 添加 ProjectInstaller.cs
  • 设置 ServiceName , DisplayName , Account

Step 4:生成安装包

  • 编译 Release 版本
  • 打包 .exe + .config + .dll

Step 5:运行安装脚本

  • 右键 install.bat → 以管理员身份运行
  • 查看服务是否出现在 services.msc

Step 6:监控与维护

  • 查看事件日志是否有报错
  • 使用 ServiceController 编程控制
  • 设置自动重启策略以防崩溃

总结:打造企业级可靠服务的终极 checklist ✅

项目 是否达标
OnStart 不阻塞
OnStop 正确释放资源
✅ 设置 CanStop = true
✅ 使用 CancellationToken 协调任务取消
✅ 主动上报 SetServiceStatus 防止超时
✅ 安装脚本自动提权
✅ 日志记录到位
✅ 支持 DEBUG 模式调试
✅ 配置文件外置可修改
✅ 遵循最小权限原则

只要你把这些都打上勾,那你写的 Windows 服务,绝对稳如老狗🐶!


现在回头看看开头那个“Error 1053”,是不是觉得它也没那么可怕了?其实只要理解了背后的机制,一切难题都会迎刃而解。

希望这篇文章能成为你通往“服务大师”之路的钥匙 🔑。下次再遇到服务启动失败,记得深呼吸,打开日志,一步一步排查——你一定行!

有任何疑问欢迎留言讨论~我们一起进步 💪

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C#是Windows平台应用开发的重要语言,尤其适用于Windows服务的构建。本DEMO项目“C#开发Windows服务DEMO”通过实际案例讲解如何使用C#创建、安装、控制和部署Windows后台服务。内容涵盖ServiceBase类的继承与方法重写、ServiceController类的服务控制机制、使用InstallUtil工具进行服务安装与卸载,以及通过批处理脚本自动化部署。配套的详细说明文档帮助开发者掌握在Visual Studio中配置、调试和发布服务的全流程。该项目为希望实现系统级后台任务管理的C#开发者提供了实用的学习资源和开发模板。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐