用 Go 编写 Windows 服务
目录
介绍
各位开发者好,好久没写关于 Windows 的东西了。今天我想指导大家如何用 Go 语言编写 Windows 服务应用程序。没错,就是 Go 语言。在本教程博客中,我们将介绍一些关于 Windows 服务应用程序的基础知识,之后我会通过一个简单的代码演示,带领大家编写一个将信息记录到文件的 Windows 服务。废话不多说,让我们开始吧!
Windows“服务”究竟是什么?
Windows 服务应用程序(也称为 Windows 服务)是运行在后台的小型应用程序。与普通的 Windows 应用程序不同,它们没有图形用户界面 (GUI) 或任何形式的用户界面。这些服务应用程序在计算机启动时开始运行,并且无论使用哪个用户帐户运行,它们都会运行。它们的生命周期(启动、停止、暂停、继续等)由名为服务控制管理器 (SCM) 的程序控制。
因此,由此我们可以理解,我们应该以这样的方式编写我们的 Windows 服务:SCM 应该与我们的 Windows 服务进行交互并管理其生命周期。
为什么选择 Golang?
在编写 Windows 服务时,您可能会考虑使用 Go 语言。
并发性
Go 的并发模型能够实现更快、更高效的资源处理。它goroutines允许我们编写可以执行多任务而不会出现阻塞或死锁的应用程序。
简单
传统上,Windows 服务使用 C++ 或 C(有时也用 C#)编写,这不仅导致代码复杂,而且开发者体验 (DX) 很差。Go 语言实现的 Windows 服务简洁明了,每一行代码都清晰易懂。
静态二进制文件
你可能会问:“为什么不使用像 Python 这样更简单的语言呢?”原因在于 Python 的解释执行特性。Go 编译后会生成静态链接的单文件二进制文件,这对于 Windows 服务的高效运行至关重要。Go 二进制文件不需要任何运行时环境/解释器。Go 代码还可以进行交叉编译。
低层通道
尽管 Go 是一种垃圾回收语言,但它仍然提供了强大的底层交互支持。我们可以轻松地在 Go 中调用 Win32 API 和通用系统调用。
好了,信息足够了。开始写代码吧……
用 Go 编写 Windows 服务
本代码讲解假设您已具备 Go 语言的基本语法知识。如果您还不了解,可以参考《 Go 语言入门指南》(A Tour of Go)来学习。
- 首先,我们来给项目命名。我将其命名为
cosmic/my_service。创建一个 go.mod 文件,
PS C:\> go mod init cosmic/my_service
- 现在我们需要安装
golang.org/x/sys一个软件包。该软件包为 Windows 操作系统相关的应用程序提供 Go 语言支持。
PS C:\> go get golang.org/x/sys
注意:此软件包还包含对基于 UNIX 的操作系统(如 Mac OS 和 Linux)的操作系统级 Go 语言支持。
-
创建一个
main.go文件。main.go 文件包含main一个函数,该函数作为我们 Go 应用程序/服务的入口点。 -
要创建服务实例,我们需要编写一个名为Service Context的东西,它实现了
Handler接口golang.org/x/sys/windows/svc。
所以,接口定义看起来大概是这样的
type Handler interface {
Execute(args []string, r <-chan ChangeRequest, s chan<- Status) (svcSpecificEC bool, exitCode uint32)
}
Execute该函数将在服务启动时由包代码调用,服务将在函数Execute完成后退出。
我们从只读通道读取服务变更请求r并采取相应措施。我们还应该通过向只发通道发送信号来保持服务更新s。我们可以向args参数传递可选参数。
退出时,exitCode如果执行成功,我们可以返回值为 0。我们也可以用svcSpecificEC它来实现这一点。
- 现在,创建一个名为
myService“服务上下文”的类型。
type myService struct{}
- 创建类型后
myService,将上述内容Execute作为方法添加到该类型中,使其实现Handler接口。
func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
// to be filled
}
- 既然我们已经成功实现了
Handler接口,现在就可以开始编写实际逻辑了。
创建一个常量,其中包含我们的服务可以从 SCM 接收的信号。
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
我们的主要目标是每30秒记录一些数据。因此,我们需要为此定义一个线程安全的定时器。
tick := time.Tick(30 * time.Second)
初始化工作已经完成。现在是时候START向SCM发送信号了。我们这就去做。
status <- svc.Status{State: svc.StartPending}
status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
现在我们要编写一个循环,作为应用程序的主循环STOP。在循环中处理事件会使应用程序永不结束,只有当 SCM 发送信号时,我们才能跳出循环SHUTDOWN。
loop:
for {
select {
case <-tick:
log.Print("Tick Handled...!")
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
status <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
log.Print("Shutting service...!")
break loop
case svc.Pause:
status <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
case svc.Continue:
status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
default:
log.Printf("Unexpected service control request #%d", c)
}
}
}
这里我们使用了一个select语句来接收来自通道的信号。在第一种情况下,我们处理定时器的tick信号。正如我们之前声明的,这种情况下每 30 秒接收一次信号。在这种情况下,我们会记录字符串“Tick Handled...!”
其次,我们通过只读r通道处理来自 SCM 的信号。因此,我们将信号值赋给r一个变量c,并使用switch语句来处理我们服务的所有生命周期事件/信号。我们可以在下面看到每个生命周期的详细说明:
svc.Interrogate- SCM及时请求信号以检查服务的当前状态。svc.Stop以及svc.Shutdown- 当我们的服务需要停止或关闭时,SCM 发送的信号。svc.Pause- SCM 发送信号暂停服务执行,但不关闭服务。svc.Continue- SCM 发送信号以恢复服务暂停的执行状态。
因此,当收到svc.Stop任一信号时svc.Shutdown,我们就断开循环。需要注意的是,我们需要STOP向服务控制模块 (SCM) 发送信号,告知 SCM 我们的服务即将停止。
status <- svc.Status{State: svc.StopPending}
return false, 1
- 现在我们编写一个名为 `on` 的函数,
runService该函数允许我们的服务以调试模式或服务控制模式运行。
注意:在服务控制模式下调试 Windows 服务应用程序非常困难。因此,我们正在编写一个额外的调试模式。
func runService(name string, isDebug bool) {
if isDebug {
err := debug.Run(name, &myService{})
if err != nil {
log.Fatalln("Error running service in debug mode.")
}
} else {
err := svc.Run(name, &myService{})
if err != nil {
log.Fatalln("Error running service in Service Control mode.")
}
}
}
- 最后,我们可以
runService在函数中调用该函数main。
func main() {
f, err := os.OpenFile("debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalln(fmt.Errorf("error opening file: %v", err))
}
defer f.Close()
log.SetOutput(f)
runService("myservice", false) //change to true to run in debug mode
}
注意:我们会将日志记录到日志文件中。在高级场景下,我们会将日志记录到 Windows 事件日志记录器中。(呼,这听起来像个绕口令😂)
- 现在运行命令
go build生成二进制文件“.exe”。此外,我们还可以使用以下命令优化并减小二进制文件的大小:
PS C:\> go build -ldflags "-s -w"
安装并启动服务
我们使用一个名为“内置工具”的工具来安装、删除、启动和停止我们的服务。sc.exe
要安装我们的服务,请以管理员身份在 PowerShell 中运行以下命令:
PS C:\> sc.exe create MyService <path to your service_app.exe>
要启动我们的服务,请运行以下命令:
PS C:\> sc.exe start MyService
要删除我们的服务,请运行以下命令:
PS C:\> sc.exe delete MyService
您可以探索更多命令,只需输入sc.exe不带任何参数的命令即可查看可用命令。
结论
如我们所见,用 Go 语言实现 Windows 服务非常简单,只需极少的编写工作。您可以编写自己的 Windows 服务,使其充当 Web 服务器等等。感谢阅读,别忘了点个赞❤️。
完整代码
以下是完整的代码,供您参考。
// file: main.go
package main
import (
"fmt"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/debug"
"log"
"os"
"time"
)
type myService struct{}
func (m *myService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
tick := time.Tick(5 * time.Second)
status <- svc.Status{State: svc.StartPending}
status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
loop:
for {
select {
case <-tick:
log.Print("Tick Handled...!")
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
status <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
log.Print("Shutting service...!")
break loop
case svc.Pause:
status <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
case svc.Continue:
status <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
default:
log.Printf("Unexpected service control request #%d", c)
}
}
}
status <- svc.Status{State: svc.StopPending}
return false, 1
}
func runService(name string, isDebug bool) {
if isDebug {
err := debug.Run(name, &myService{})
if err != nil {
log.Fatalln("Error running service in debug mode.")
}
} else {
err := svc.Run(name, &myService{})
if err != nil {
log.Fatalln("Error running service in Service Control mode.")
}
}
}
func main() {
f, err := os.OpenFile("E:/awesomeProject/debug.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalln(fmt.Errorf("error opening file: %v", err))
}
defer f.Close()
log.SetOutput(f)
runService("myservice", false)
}