实现一个监控 IP 的 windows 服务
Intro
我们公司的 VPN 用自己的电脑连公司的台式机的时候需要用 IP 地址,有一次尝试去连的时候发现连不上,第二天到公司发现 IP 变掉了,不是之前连的 IP 了,于是就想写一个简单 Windows 服务来监控台式机的 IP 变化
Overview
在 C# 里我们可以使用 Dns.GetHostAddresses()
方法来获取 IP 地址,我们可以每隔一段时间就判断一下当前的 IP 地址,为了方便测试,可以把这个时间定义在配置里,这样本地开发的时候比较方便
为了避免消息太多,我们可以做一个简单的检查,如果 IP 地址不变,就不发消息了,只有当 IP 信息变化的时候再发消息
我们办公使用的是 Google Chat, 所以打算使用 Google Chat 来发消息,也可以根据需要改成自己想用的通知方式
Implement
首先我们可以新建一个 worker 服务,使用 dotnet cli 新建即可
dotnet new worker -n IpMonitor
如果不习惯没有解决方案文件,也可以新建一个解决方案文件并将项目添加到解决方案文件中
cd IpMonitor
dotnet new sln
dotnet sln add ./IpMonitor.csproj
然后我们来改造我们的 Worker
, Worker
其实就是一个后台服务,我们的服务比较简单就直接在上面改了
public sealed class Worker : BackgroundService
{private readonly TimeSpan _period;private readonly INotification _notification;private readonly ILogger<Worker> _logger;private volatile string _previousIpInfo = string.Empty;public Worker(IConfiguration configuration, INotification notification, ILogger<Worker> logger){_notification = notification;_logger = logger;_period = configuration.GetAppSetting<TimeSpan>("MonitorPeriod");if (_period <= TimeSpan.Zero){_period = TimeSpan.FromMinutes(10);}}protected override async Task ExecuteAsync(CancellationToken stoppingToken){using var timer = new PeriodicTimer(_period);while (await timer.WaitForNextTickAsync(stoppingToken)){try{var host = Dns.GetHostName();var ips = await Dns.GetHostAddressesAsync(host, stoppingToken);var ipInfo = $"{Environment.MachineName} - {host}\n {ips.Order(new IpAddressComparer()).Select(x => x.MapToIPv4().ToString()).StringJoin(", ")}";if (_previousIpInfo == ipInfo){_logger.LogDebug("IpInfo not changed");continue;}_logger.LogInformation("Ip info: {IpInfo}", ipInfo);await _notification.SendNotification(ipInfo);_previousIpInfo = ipInfo;}catch (Exception e){_logger.LogError(e, "GetIp exception");}}}
}
这里我们使用了 .NET 6 引入的 PeriodicTimer
来实现定时任务,自定义了一个 IpAddressComparer
来对 IP 地址做一个排序,实现如下:
public sealed class IpAddressComparer: IComparer<IPAddress>
{public int Compare(IPAddress? x, IPAddress? y){if (ReferenceEquals(x, y)) return 0;if (ReferenceEquals(null, y)) return 1;if (ReferenceEquals(null, x)) return -1;var bytes1 = x.MapToIPv4().ToString().SplitArray<byte>(new []{ '.' });var bytes2 = y.MapToIPv4().ToString().SplitArray<byte>(new []{ '.' });for (var i = 0; i < bytes1.Length; i++){if (bytes1[i] != bytes2[i]){return bytes1[i].CompareTo(bytes2[i]);}}return 0;}
}
通知使用了 Google Chat 的 webhook API,可以自定义一个 Space
,添加一个 webhook 即可,添加成功即可获取一个 webhook URL, 发送消息 API 可以参考文档:https://developers.google.com/chat/api/guides/message-formats/basic
实现如下:
public sealed class GoogleChatNotification: INotification
{private readonly HttpClient _httpClient;private readonly string _webhookUrl;public GoogleChatNotification(HttpClient httpClient, IConfiguration configuration){_httpClient = httpClient;_webhookUrl = Guard.NotNullOrEmpty(configuration.GetAppSetting("GChatWebHookUrl"));}public async Task<bool> SendNotification(string text){using var response = await _httpClient.PostAsJsonAsync(_webhookUrl, new { text });return response.IsSuccessStatusCode;}
}
在 Program
文件中注册我们新加的服务就可以了
然后我们进行一些改造来发布和部署 Windows 服务,可以按照文档的提示将项目发布为单文件,部署我比较喜欢 powershell,写了两个简单的 powershell script 来安装和卸载 Windows 服务
首先我们可以在项目里添加 Microsoft.Extensions.Hosting.WindowsServices
的引用,并添加一些发布属性
<PropertyGroup><PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile><RuntimeIdentifier>win-x64</RuntimeIdentifier><PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
在 Program
中注册 windows 服务相关配置
using IpMonitor;Host.CreateDefaultBuilder(args).ConfigureServices(services =>{services.AddHostedService<Worker>();services.AddSingleton<HttpClient>();services.AddSingleton<INotification, GoogleChatNotification>();})
#if !DEBUG// https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service.UseWindowsService(options =>{options.ServiceName = "IpMonitor";})
#endif.Build().Run();
安装服务 powershell 脚本:
$serviceName = "IpMonitor"
Write-Output "serviceName: $serviceName"dotnet publish -c Release -o out
$destDir = Resolve-Path ".\out"
$ipMonitorPath = "$destDir\IpMonitor.exe"Write-Output "Installing service... $ipMonitorPath $destDir"
New-Service $serviceName -BinaryPathName $ipMonitorPath
Start-Service $serviceName
Write-Output "Service $serviceName started"
卸载服务 powershell 脚本:
$serviceName = "IpMonitor"
Stop-Service $serviceName
Write-Output "Service $serviceName stopped"
Remove-Service $serviceName
Write-Output "Service $serviceName removed"
运行效果如下(脚本运行需要以管理员权限运行):
我们可以使用 Get-Service IpMonitor
来查看服务状态
也可以在任务管理器和服务中查看
最后再把我们的服务卸载掉
More
发布为 Windows 服务时如果有问题可以通过 event log 来排查,在 event log 里可以看到我们服务的日志
References
https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service
https://github.com/WeihanLi/SamplesInPractice/tree/master/IpMonitor
https://developers.google.com/chat/api/guides/message-formats/basic