C#开发Windows服务完整DEMO实战项目
简介: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 的三大黄金法则
- 绝不阻塞主线程
- 尽快返回
- 启动异步任务
❌ 错误示范:阻塞式无限循环
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”,是不是觉得它也没那么可怕了?其实只要理解了背后的机制,一切难题都会迎刃而解。
希望这篇文章能成为你通往“服务大师”之路的钥匙 🔑。下次再遇到服务启动失败,记得深呼吸,打开日志,一步一步排查——你一定行!
有任何疑问欢迎留言讨论~我们一起进步 💪
简介:C#是Windows平台应用开发的重要语言,尤其适用于Windows服务的构建。本DEMO项目“C#开发Windows服务DEMO”通过实际案例讲解如何使用C#创建、安装、控制和部署Windows后台服务。内容涵盖ServiceBase类的继承与方法重写、ServiceController类的服务控制机制、使用InstallUtil工具进行服务安装与卸载,以及通过批处理脚本自动化部署。配套的详细说明文档帮助开发者掌握在Visual Studio中配置、调试和发布服务的全流程。该项目为希望实现系统级后台任务管理的C#开发者提供了实用的学习资源和开发模板。
更多推荐



所有评论(0)