Windows XP Windows 7 Windows 2003 Windows Vista Windows 綜合教學 Linux 系統教學
Windows 10 Windows 8 Windows 2008 Windows NT Windows Server 電腦軟體教學

利用 Windows 服務實現網站自動更新:.NET Framework 服務架構與 XML 排程實作

編輯:XP 系統應用技巧

前言

網站內容更新 (如部落格靜態頁生成、快取重建、搜尋索引重建) 常需定時觸發。傳統作法使用 Windows 工作排程器呼叫 URL,但受限於登入會話、網路權限、錯誤處理。本文介紹開發一個 .NET Framework 2.0 Windows Service,透過 XML 設定檔定義排程 URL,每 20 秒輪詢執行 HTTP 請求,實現穩定、背景、無人值守的網站自動更新機制。

架構設計

  • 執行環境: .NET Framework 2.0+ (相容 XP/Server 2003 至最新版)。
  • 核心元件: System.Timers.Timer (20 秒間隔) + System.Net.WebClient / HttpWebRequest + System.Xml.XmlDocument 解析設定。
  • 設定檔: urls.xml 獨立於程式碼,支援熱更新 (服務偵測檔案變更可自動重載)。
  • 日誌記錄: 寫入文字檔 / 事件檢視器 / 資料庫,記錄每次請求時間、URL、HTTP 狀態碼、耗時、錯誤詳情。

XML 設定檔結構 (urls.xml)

<?xml version="1.0" encoding="utf-8"?>
<root>
  <Urls>
    <item url="https://www.example.com/cron/generate_static" time="03:59" />
    <item url="https://www.example.com/cron/rebuild_index" time="11:59" />
    <item url="https://www.example.com/cron/clear_cache" time="13:59" />
    <item url="https://www.example.com/cron/daily_report" time="15:59" />
    <item url="https://www.example.com/cron/backup_db" time="16:59" />
  </Urls>
</root>
  • url:完整 HTTP/HTTPS 端點,可攜帶查詢字串 (如 ?action=rebuild&type=full)。
  • time:每日觸發時間 (HH:mm),精確到分鐘。同一分鐘內僅允許一個任務。

核心程式碼實作 (C#)

using System;
using System.Configuration;
using System.Net;
using System.Timers;
using System.Xml;

namespace WebsiteAutoUpdater
{
    public partial class UpdaterService : ServiceBase
    {
        private Timer _timer;
        private XmlNodeList _urlNodes;
        private string _configPath;

        public UpdaterService()
        {
            InitializeComponent();
            _configPath = AppDomain.CurrentDomain.BaseDirectory + ConfigurationManager.AppSettings["ConfigFile"];
        }

        protected override void OnStart(string[] args)
        {
            LoadConfig();
            _timer = new Timer(20000); // 20 秒
            _timer.Elapsed += Timer_Elapsed;
            _timer.AutoReset = true;
            _timer.Enabled = true;
            Log("Service started.");
        }

        protected override void OnStop()
        {
            _timer.Enabled = false;
            _timer.Dispose();
            Log("Service stopped.");
        }

        private void LoadConfig()
        {
            var xml = new XmlDocument();
            xml.Load(_configPath);
            _urlNodes = xml.SelectNodes("//root/Urls/item");
        }

        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            var now = DateTime.Now;
            var currentTime = now.ToString("HH:mm");

            foreach (XmlNode node in _urlNodes)
            {
                var taskTime = node.Attributes["time"]?.Value;
                if (taskTime == currentTime)
                {
                    var url = node.Attributes["url"]?.Value;
                    if (!string.IsNullOrEmpty(url))
                    {
                        TriggerUrl(url);
                    }
                }
            }

            // 熱重載設定檔 (可選)
            if (File.GetLastWriteTime(_configPath) > _lastConfigLoad)
                LoadConfig();
        }

        private void TriggerUrl(string url)
        {
            try
            {
                using (var client = new WebClient())
                {
                    client.Headers.Add("User-Agent", "WebsiteAutoUpdater/1.0");
                    var response = client.DownloadString(url);
                    Log($"SUCCESS {DateTime.Now:yyyy-MM-dd HH:mm:ss} | {url} | {response.Substring(0, Math.Min(200, response.Length))}");
                }
            }
            catch (WebException ex)
            {
                var statusCode = ((HttpWebResponse)ex.Response)?.StatusCode ?? HttpStatusCode.InternalServerError;
                Log($"ERROR {DateTime.Now:yyyy-MM-dd HH:mm:ss} | {url} | HTTP {(int)statusCode} {statusCode} | {ex.Message}");
            }
            catch (Exception ex)
            {
                Log($"EXCEPTION {DateTime.Now:yyyy-MM-dd HH:mm:ss} | {url} | {ex.Message}");
            }
        }

        private void Log(string message)
        {
            var logPath = AppDomain.CurrentDomain.BaseDirectory + "updater_log.txt";
            File.AppendAllText(logPath, message + Environment.NewLine);
        }
    }
}

部署與安裝步驟

  1. 編譯專案為 WebsiteUpdater.exe。
  2. 建立資料夾 C:\Services\WebsiteUpdater\,放入 exe 與 urls.xml。
  3. 修改 App.config 設定 ConfigFile 指向 urls.xml。
  4. 以管理員身分開啟 CMD,安裝服務:
    sc create WebsiteUpdater binPath= "C:\Services\WebsiteUpdater\WebsiteUpdater.exe" start= auto obj= "NT AUTHORITY\LocalSystem" displayname= "Website Auto Updater"
  5. 啟動服務:sc start WebsiteUpdater 或 services.msc 中啟動。
  6. 驗證:查看 updater_log.txt 確認心跳記錄 (Heart Beat) 與任務觸發記錄。

進階增強功能

功能實作方式
設定檔熱重載FileSystemWatcher 監聽 urls.xml 變更 → 重新載入 XmlDocument
失敗重試機制記錄失敗次數,指數退避重試 (1分、5分、15分、1時),超過閾值發送郵件告警
執行狀態儀表板內建輕量 HTTP 端點 (HttpListener) 提供 JSON 狀態 API,供 Grafana/Prometheus 抓取
支援 POST/JSON 請求XML 擴充 method、headers、body 屬性,HttpWebRequest 完整控制
多執行緒並行Task.Run / Parallel.ForEach 處理同分鐘多任務 (需修改同一分鐘單任務限制)
Windows 事件日誌整合EventLog.WriteEntry 寫入 Application Log,支援事件檢視器篩選

與替代方案比較

方案優點缺點適用場景
Windows Service (本文)背景無人值守、開機自啟、完整 .NET 生態、狀態持久化、安全性高需開發/編譯/部署、安裝需管理員權限企業正式環境、長期穩定運行、需複雜邏輯 (重試、告警、儀表板)
工作排程器 + PowerShell/curl零開發、圖形化設定、支援觸發條件豐富受限於使用者登入會話、網路認證處理複雜、錯誤重試弱簡單定時任務、臨時需求、開發環境
Linux Cron + curl/wget輕量、原生、極度穩定需 Linux 主機、跨平台部署複雜網站部署在 Linux、團隊熟悉 Shell
第三方排程服務 (Hangfire/Quartz.net)功能豐富、儀表板內建、分散式支援學習曲線、額外相依套件.NET 應用程式內建排程需求

總結

自訂 Windows Service 實現網站自動更新,雖需初期開發投入,但提供了 穩定性、可維護性、擴充性 的最佳平衡。XML 設定檔分離業務邏輯與排程配置,配合 20 秒輪詢心跳機制,可滿足分鐘級精度的定時觸發需求。建議從最小可行版本 (核心 Timer + WebClient + XML + Log) 開始,逐步加入熱重載、重試、告警、儀表板等企業級功能。

Copyright © Windows教程網 All Rights Reserved