多年以來,只要提到編寫Windows服務,就會想到用Visual C++編寫,同時,這也是其中一件C++程序員可以做,而VB程序員不可以做的事情。以前,我們只稱其為"服務"或"NT服務",現在,它們被命名為"Windows服務",而且用VB.NET或C#也可以很容易地編寫。
但是,如果你想用托管C++來編寫呢?畢竟,大多數有經驗的Visual C++程序員都會寫過一兩個服務,也會知道怎樣完成一個類似的工程。假設你有一個必須要一直運行以提供服務的程序,且連接著一些遠程電腦,如果不想編寫一本使用手冊,告訴客戶要記得在每次重啟電腦之後重新運行此程序,你就應該使它成為一個服務;又假設你有一個用於刪除過期數據庫記錄的便利維護工具,如果不想讓管理員每周都親手運行它一次,你就應該使它成為一個服務。看起來挺吸引人的,那就讓我們開始吧。
創建服務工程
以下要做的事情非常簡單:打開Visual Studio.NET,創建一個新的工程,在Visual C++工程下,選擇Windows服務(.NET)。接下來,為這個服務取一個方便在電腦的服務列表中查找到的名字,在此為CGNotifier。向導會創建一個繼承自System::ServiceProcess::ServiceBase的類並打開設計視圖,在此,你可放入一個計時器、一個數據庫連接,或其他不可見的組件。
讓我們轉到代碼視圖中看一下生成的代碼,在此有一個構造函數與一個Dispose方法,這兩個你都可以忽略,還有一對重載的方法:OnStart()和OnStop)。在OnStart()中,可編寫服務所需的代碼。服務中一個重要的范疇是使用"事件引發對象",例如System::IO::FileSystemWatcher的一個實例,一般可在OnStart()中創建這些對象,在本例中,你可為類加入事件方法,並處理在服務運行期間,由這些對象引發的事件。另有一種服務,它們對發生的事情不作反應,只在每天或每周的特定時間,執行一些特定的任務,這些服務平時通常處於休眠狀態,但因為它們的工作狀態是持續的,所以不應該停止它們,或者可以把它們放入一個循環中,在特定的時間檢查它們是否已被停止。
OnStart()方法是服務的開始之處,並且會在執行完後返回,在此方法完成之前,服務一般不會顯示為"已啟動"。這就意味著,不能在OnStart()中放入一個經常使用的循環,或從別處直接調用的任何方法。最直接的方法是設置好一個單獨的方法,並在一個新線程中調用它,如下所示:
private:
bool stopping;
int loopsleep; //毫秒
Threading::Thread* servicethread;
protected:
//設置好服務應做的工作
void OnStart(String* args[])
{
Threading::ThreadStart* threadStart =new Threading::ThreadStart(this,mainLoop);
servicethread = new Threading::Thread(threadStart);
servicethread->Start();
}
void mainLoop()
{
loopsleep = 1000; //毫秒
stopping = false;
while (!stopping)
{
Threading::Thread::Sleep(loopsleep);
}
}
這個循環將會一直運行,直到服務停止,因為OnStop()設置了停止標志:
void OnStop()
{
stopping = true;
}
如果你增加loopsleep值,則會在停止時,增加服務的響應時間。
安裝服務
盡管這個服務什麼也不做,但你仍可對它進行安裝、啟動和停止。為簡化安裝過程,可在工程中加入一個安裝程序,這可在設計視圖中完成(如果你喜歡,可在設計視圖中打開屬性窗口,並修改ServiceName屬性;而向導會在工程名後加上WinService,這最好在添加安裝程序之前完成,否則,就需要在多處修改服務名。),鼠標右鍵單擊設計視圖,選擇添加安裝程序。這將創建一個服務安裝程序和一個服務過程安裝程序,並顯示在設計視圖中,以供你設置它們的屬性。
如果已經閱讀了有關Windows服務的 .NET文檔,你可能會想為什麼要添加一個安裝程序呢?難道不可以自動添加嗎?實際上,如果是使用VB或C#,是可以自動添加的,而C++卻不行。
服務過程安裝程序只有一個比較讓人感興趣的屬性:服務所運行的賬戶。單擊serviceProcessInstaller1選擇它,打開其屬性窗口。默認情況下,賬戶屬性為User,這意味著在安裝服務時,將會提示輸入一個ID和密碼,而且服務將會運行於user權限下--這在服務運行於system賬戶時非常有用。通常有三個選項:LocalSystem是服務被安裝於未運行Windows 2003的電腦上時的唯一選擇;如果服務是面向Windows 2003的,那麼LocalService的權限更少,因為是更好的選擇;而NetworkService允許服務驗證另一台電腦,所以只在需要使用它(例如,一個服務加載了一個web頁),相反,在使用公共web服務時,就不需要作為NetworkService運行,因為它不需驗證遠程電腦。
而服務安裝程序中需要注意的屬性是StartType:手動、自動、禁用。在此例中為手動。
現在,可以生成服務,並准備安裝了。打開Visual Studio命令提示符,定位到工程的Debug文件夾,輸入以下命令:
InstallUtil CGNotifier.exe
以下是屏幕的輸出:
Microsoft (R) .NET Framework Installation utility
Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002.
All rights reserved.
Exception occurred while initializing the installation:
System.IO.FileLoadException: Unverifiable image 'CGNotifier.exe'
cannot be run.
這真是難以理解,不是嗎?在C++中編寫可驗證代碼向來都是不可能的,且非常難以實現。為什麼工程向導創建了一個服務,但卻沒有提示你代碼必須為可驗證的呢?其實不必使你的服務程序產生可驗證代碼。
打開解決方案資源管理器,找到並打開相應的 .cpp文件,你將會發現隱藏在此的一個main()函數--正是這個main()函數以一種"聰明"的方式為你調用了InstallUtil,並產生了整個的"可驗證代碼"問題。現在回到命令提示符窗口,像以下這樣安裝服務:
CGNotifier.exe -Install
你可看到服務輕松、流暢地安裝上去了。
為進行測試,現在打開"計算機管理",並展開"服務和應用程序"項,選擇"服務",你可看到新安裝上去的服務:右鍵單擊它選擇啟動。一旦服務啟動,切換回Visual Studio,選擇服務器資源管理器查看此服務:依次選擇視圖、服務器資源管理器,展開你的計算機名,再展開服務,你將看到一個新服務,而帶有的綠色三角形表明它正在運行。
在服務器資源管理器中右擊此服務,選擇停止。現在,請在"事件查看器"中查看事件記錄,可看到二個日志記錄:一個告訴你服務已啟動,而另一個告訴你服務已停止。如果你不想產生事件日志記錄,請在服務的設計視圖中修改AutoLog屬性為False。
卸載服務
如果你從Debug目錄中安裝此服務,在對它進行修改期間,並不需要卸載,把它停止,重新生成,再啟動就行了。但是,如果你想卸載它,請回到Visual Studio命令提示符窗口,定位到Debug目錄,輸入以下命令:
CGNotifier.exe -Install /u
現在,服務就會從"服務器資源管理器"和"計算機管理"的服務列表中消失了,也許,需要刷新列表才能看到變化。
喚醒後做一些事情
當然,以上所示的服務到目前為止並不能做任何事情,為把它變成一個"在設定時刻喚醒"的服務,第一步應在工程中加入一個配置文件,示例如下:
<configuration>
<appSettings>
<add key="runhour" value="22" />
</appSettings>
</configuration>
另外,還需要復制帶應用程序名如app.config文件到目標工程目錄(Debug或Release):
copy app.config $(ConfigurationName)\$(TargetFileName).config
為了讀取配置,可在OnStart()或mainLoop()中循環之前加入相應的代碼,在此傾向於盡可能地保持OnStart()為空,因此在mainLoop()中加入以下代碼:
String* sHour = Configuration::ConfigurationSettings::
AppSettings->get_Item("runhour");
int runHour = System::Int32::Parse(sHour);
bool rantoday = false;
而循環則如下所示:
stopping = false;
while (!stopping)
{
if (DateTime::Now.Hour == runHour && !rantoday)
{
//執行相應的任務
rantoday = true;
}
else
rantoday = false;
Threading::Thread::Sleep(loopsleep);
}
因為到了事先約定的時間,只想要上述代碼運行一次,因此,在服務執行完相應的任務之後,必須把rantoday標志設為true,只要在其他時間,都會被設為false。
你可以在服務中查找數據庫的新記錄、或查找過期的文件並刪除它們,當然,在服務中可以做的事情遠遠不只這些。但不管要執行的任務是什麼,都需要告訴其他人你做過什麼,因為服務不具備一個用戶界面,所以也不能彈出一個消息框,因此,使用事件日志是一個不錯的方法。
請在mainLoop()的循環之前加入以下代碼,以用於設置事件日志記錄:
Diagnostics::EventLog* log ;
if (! Diagnostics::EventLog::SourceExists("CGNotifierService") )
Diagnostics::EventLog::CreateEventSource("CGNotifierService",CGNotifierLog");
log = new Diagnostics::EventLog("CGNotifierLog");
log->Source = "CGNotifierService";
雖然不用同時設置日志和源代碼,但這樣做的話,消息會在服務器浏覽器的事件日志之下,創建它們自己的節點。
為向日志中寫入,通常只需一行代碼--可把它放在"執行相應任務"的注釋之後: log->WriteEntry("服務的運行時間到了。",
Diagnostics::EventLogEntryType::Information);
現在,我們大功告成:一個可以安裝、卸載、啟動、停止,並每天向事件日志中寫入一條信息的服務誕生了!從此以後,你將無堅不摧,用C++編寫的Windows服務可不像其他那些 .NET應用程序,它只局限於你的想像力。另外,在創建服務工程時,還要注意分清C++與VB及C#之間的細微差別。還等什麼呢,趕快動手啊!