實現一個監控 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