DELPHI編寫服務程序總結
一、服務程序和桌面程序的區別
Windows 2000/XP/2003等支持一種叫做“系統服務程序”的進程,系統服務和桌面程序的區別是:
系統服務不用登陸系統即可運行;系統服務是運行在System Idle Process/System/smss/winlogon/services下的,而桌面程序是運行在Explorer下的;系統服務擁有更高的權限,系統服務擁有Sytem的權限,而桌面程序只有Administrator權限;在Delphi中系統服務是對桌面程序進行了再一次的封裝,既系統服務繼承於桌面程序。因而擁有桌面程序所擁有的特性;系統服務對桌面程序的DoHandleException做了改進,會自動把異常信息寫到NT服務日志中;普通應用程序啟動只有一個線程,而服務啟動至少含有三個線程。(服務含有三個線程:TServiceStartThread服務啟動線程;TServiceThread服務運行線程;Application主線程,負責消息循環);
摘錄代碼:
procedure TServiceApplication.Run;
begin
.
.
.
StartThread := TServiceStartThread.Create(ServiceStartTable);
try
while not Forms.Application.Terminated do
Forms.Application.HandleMessage;
Forms.Application.Terminate;
if StartThread.ReturnValue <> 0 then
FEventLogger.LogMessage(SysErrorMessage(StartThread.ReturnValue));
finally
StartThread.Free;
end;
.
.
.
end;
procedure TService.DoStart;
begin
try
Status := csStartPending;
try
FServiceThread := TServiceThread.Create(Self);
FServiceThread.Resume;
FServiceThread.WaitFor;
FreeAndNil(FServiceThread);
finally
Status := csStopped;
end;
except
on E: Exception do
LogMessage(Format(SServiceFailed,[SExecute, E.Message]));
end;
end;
在系統服務中也可以使用TTimer這些需要消息的定時器,因為系統服務在后台使用TApplication在分發消息;
二、如何編寫一個系統服務
打開Delphi編輯器,選擇菜單中的File|New|Other...,在New Item中選擇Service Application項,Delphi便自動為你建立一個基於TServiceApplication的新工程,TserviceApplication是一個封裝NT服務程序的類,它包含一個TService1對象以及服務程序的裝卸、注冊、取消方法。
TService屬性介紹:
AllowPause:是否允許暫停;
AllowStop:是否允許停止;
Dependencies:啟動服務時所依賴的服務,如果依賴服務不存在則不能啟動服務,而且啟動本服務的時候會自動啟動依賴服務;
DisplayName:服務顯示名稱;
ErrorSeverity:錯誤嚴重程度;
Interactive:是否允許和桌面交互;
LoadGroup:加載組;
Name:服務名稱;
Password:服務密碼;
ServiceStartName:服務啟動名稱;
ServiceType:服務類型;
StartType:啟動類型;
事件介紹:
AfterInstall:安裝服務之后調用的方法;
AfterUninstall:服務卸載之后調用的方法;
BeforeInstall:服務安裝之前調用的方法;
BeforeUninstall:服務卸載之前調用的方法;
OnContinue:服務暫停繼續調用的方法;
OnExecute:執行服務開始調用的方法;
OnPause:暫停服務調用的方法;
OnShutDown:關閉時調用的方法;
OnStart:啟動服務調用的方法;
OnStop:停止服務調用的方法;
三、編寫一個兩棲服務
采用下面的方法,可以實現一個兩棲系統服務(既系統服務和桌面程序的兩種模式)
工程代碼:
program FleetReportSvr;
uses
SvcMgr,
Forms,
SysUtils,
Windows,
SvrMain in 'SvrMain.pas' {FleetReportService: TService},
AppMain in 'AppMain.pas' {FmFleetReport};
{$R *.RES}
const
CSMutexName = 'Global\Services_Application_Mutex';
var
OneInstanceMutex: THandle;
SecMem: SECURITY_ATTRIBUTES;
aSD: SECURITY_DESCRIPTOR;
begin
InitializeSecurityDescriptor(@aSD, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(@aSD, True, nil, False);
SecMem.nLength := SizeOf(SECURITY_ATTRIBUTES);
SecMem.lpSecurityDescriptor := @aSD;
SecMem.bInheritHandle := False;
OneInstanceMutex := CreateMutex(@SecMem, False, CSMutexName);
if (GetLastError = ERROR_ALREADY_EXISTS)then
begin
DlgError('Error, Program or service already running!');
Exit;
end;
if FindCmdLineSwitch('svc', True) or
FindCmdLineSwitch('install', True) or
FindCmdLineSwitch('uninstall', True) then
begin
SvcMgr.Application.Initialize;
SvcMgr.Application.CreateForm(TSvSvrMain, SvSvrMain);
SvcMgr.Application.Run;
end
else
begin
Forms.Application.Initialize;
Forms.Application.CreateForm(TFmFmMain, FmMain);
Forms.Application.Run;
end;
end.
然后在SvrMain注冊服務:
unit SvrMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs, MsgCenter;
type
TSvSvrMain = class(TService)
procedure ServiceStart(Sender: TService; var Started: Boolean);
procedure ServiceStop(Sender: TService; var Stopped: Boolean);
procedure ServiceBeforeInstall(Sender: TService);
procedure ServiceAfterInstall(Sender: TService);
private
{ Private declarations }
public
function GetServiceController: TServiceController; override;
{ Public declarations }
end;
var
SvSvrMain: TSvSvrMain;
implementation
const
CSRegServiceURL = 'SYSTEM\CurrentControlSet\Services\';
CSRegDescription = 'Description';
CSRegImagePath = 'ImagePath';
CSServiceDescription = 'Services Sample.';
{$R *.DFM}
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
SvSvrMain.Controller(CtrlCode);
end;
function TSvSvrMain.GetServiceController: TServiceController;
begin
Result := ServiceController;
end;
procedure TSvSvrMain.ServiceStart(Sender: TService;
var Started: Boolean);
begin
Started := dmPublic.Start;
end;
procedure TSvSvrMain.ServiceStop(Sender: TService;
var Stopped: Boolean);
begin
Stopped := dmPublic.Stop;
end;
procedure TSvSvrMain.ServiceBeforeInstall(Sender: TService);
begin
RegValueDelete(HKEY_LOCAL_MACHINE, CSRegServiceURL + Name, CSRegDescription);
end;
procedure TSvSvrMain.ServiceAfterInstall(Sender: TService);
begin
RegWriteString(HKEY_LOCAL_MACHINE, CSRegServiceURL + Name, CSRegDescription,
CSServiceDescription);
RegWriteString(HKEY_LOCAL_MACHINE, CSRegServiceURL + Name, CSRegImagePath,
ParamStr(0) + ' -svc');
end;
end.
這樣,雙擊程序,則以普通程序方式運行,若用服務管理器來運行,則作為服務運行。
例如公共模塊:
dmPublic,提供Start,Stop方法。
在主窗體中,調用dmPublic.Start,dmPublic.Stop方法。
同樣在Service中,調用dmPublic.Start,dmPublic.Stop方法。
一、如何限制系統服務和桌面程序只運行一個
如何限制系統服務和桌面程序只運行一個
在工程加入下列代碼可以設置系統服務和桌面程序只運行一個。
program FleetReportSvr;
uses
SvcMgr,
Forms,
SysUtils,
Windows,
SvrMain in 'SvrMain.pas' {FleetReportService: TService},
AppMain in 'AppMain.pas' {FmFleetReport};
{$R *.RES}
const
CSMutexName = 'Global\Services_Application_Mutex';
var
OneInstanceMutex: THandle;
SecMem: SECURITY_ATTRIBUTES;
aSD: SECURITY_DESCRIPTOR;
begin
InitializeSecurityDescriptor(@aSD, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(@aSD, True, nil, False);
SecMem.nLength := SizeOf(SECURITY_ATTRIBUTES);
SecMem.lpSecurityDescriptor := @aSD;
SecMem.bInheritHandle := False;
OneInstanceMutex := CreateMutex(@SecMem, False, CSMutexName);
if (GetLastError = ERROR_ALREADY_EXISTS)then
begin
DlgError('Error, Program or service already running!');
Exit;
end;
if FindCmdLineSwitch('svc', True) or
FindCmdLineSwitch('install', True) or
FindCmdLineSwitch('uninstall', True) then
begin
SvcMgr.Application.Initialize;
SvcMgr.Application.CreateForm(TSvSvrMain, SvSvrMain);
SvcMgr.Application.Run;
end
else
begin
Forms.Application.Initialize;
Forms.Application.CreateForm(TFmFmMain, FmMain);
Forms.Application.Run;
end;
end.
二、在系統服務和桌面程序之間共享內存
用於創建內核對象的函數幾乎都有一個指向SECURITY_ATTRIBUTES結構的指針作為其參數,在使用CreateFileMapping函數的時候,通常只是為該參數傳遞NULL,這樣就可以創建帶有默認安全性的內核對象。
默認安全性意味着對象的管理小組的任何成員和對象的創建者都擁有對該對象的全部訪問權,而其他所有人均無權訪問該對象。可以指定一個ECURITY_ATTRIBUTES結構,對它進行初始化,並為該參數傳遞該結構的地址。
它包含的與安全性有關的成員實際上只有一個,即lpSecurityDescriptor。當你想要獲得對相應的一個內核對象的訪問權(而不是創建一個新對象)時,必須設定要對該對象執行什么操作。如果想要訪問一個現有的文件映射內核對象,以便讀取它的數據,那么調用OpenfileMapping函數:通過將FILE_MAP_READ作為第一個參數傳遞給OpenFileMapping,指明打算在獲得對該文件映象的訪問權后讀取該文件, 該函數在返回一個有效的句柄值之前,首先
執行一次安全檢查。如果(已登錄用戶)被允許訪問現有的文件映射內核對象,就返回一個有效的句柄。但是,如果被拒絕訪問該對象,將返回NULL。
系統服務端核心代碼:
constructor TPublicVars.Create(ANew: Boolean);
var
SecMem: SECURITY_ATTRIBUTES;
aSD: SECURITY_DESCRIPTOR;
begin
inherited Create;
{ 創建一個任何用戶都可以訪問的內核對象訪問權 }
InitializeSecurityDescriptor(@aSD, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(@aSD, True, nil, False);
SecMem.nLength := SizeOf(SECURITY_ATTRIBUTES);
SecMem.lpSecurityDescriptor := @aSD;
SecMem.bInheritHandle := False;
FMapFile := CreateFileMapping($FFFFFFFF, @SecMem, PAGE_READWRITE, 0, CSharedMemSize, CSharedMemName);
FMapFile := OpenFileMapping(File_Map_All_Access, False, CSharedMemName);
if (FMapFile = 0) then
begin
raise Exception.Create(SysErrorMessage(GetLastError));
OutputDebugString(PChar(SysErrorMessage(GetLastError)));
end
else
begin // 成功
FShareMem := MapViewOfFile(FMapFile, File_Map_All_Access, 0, 0, CSharedMemSize);
OutputDebugString(PChar(SysErrorMessage(GetLastError) + ',Handle=' + IntToStr(Handle)));
end;
end;
destructor TPublicVars.Destroy;
begin
UnmapViewOfFile(FShareMem);
CloseHandle(FMapFile);
inherited;
end;
桌面程序核心源代碼:
constructor TPublicVars.Create(ANew: Boolean);
var
SecMem: SECURITY_ATTRIBUTES;
aSD: SECURITY_DESCRIPTOR;
begin
inherited Create;
{ 創建一個任何用戶都可以訪問的內核對象訪問權 }
InitializeSecurityDescriptor(@aSD, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(@aSD, True, nil, False);
SecMem.nLength := SizeOf(SECURITY_ATTRIBUTES);
SecMem.lpSecurityDescriptor := @aSD;
SecMem.bInheritHandle := False;
FMapFile := CreateFileMapping($FFFFFFFF, @SecMem, PAGE_READWRITE, 0, CSharedMemSize, CSharedMemName);
FMapFile := OpenFileMapping(File_Map_All_Access, False, CSharedMemName);
if (FMapFile = 0) then
begin
raise Exception.Create(SysErrorMessage(GetLastError));
OutputDebugString(PChar(SysErrorMessage(GetLastError)));
end
else
begin // 成功
FShareMem := MapViewOfFile(FMapFile, File_Map_All_Access, 0, 0, CSharedMemSize);
OutputDebugString(PChar(SysErrorMessage(GetLastError) + ',Handle=' + IntToStr(Handle)));
end;
end;
destructor TPublicVars.Destroy;
begin
UnmapViewOfFile(FShareMem);
CloseHandle(FMapFile);
inherited;
end;
詳細源代碼見報表服務和報表COM中的關於共享內存的源代碼。需要注意創建共享內存需要放在:ServiceStart中初始化,不能放在initialization,否則還會出現權限不足的信息,因為initialization是在應用程序初始化之前執行的代碼。
三、在服務中使用COM組件
在服務中調用COM組件不能像在桌面程序中直接創建,在每次創建之前先調用CoInitialize(nil),釋放的時候調用CoUninitialize。例如:調用ADO組件
var
Qry: TADOQuery;
begin
CoInitialize(nil);
Qry := TADOQuery.Create(nil);
try
...
finally
Qry.Free;
CoUninitialize;
一、提高DELPHI程序的穩定性
軟件質量是一個產品的生命線,也是關乎軟件開發者的幸福關鍵所在,每天有很多程序員都在因為軟件質量而通宵達旦的加班,經常遇到的情況是剛發布的程序不停的發布補丁包。軟件質量就像一個噩夢一樣,不停的在后面追趕着程序員,讓他們疲於奔命,甚至於在程序員中流傳着一句話:“生命不息,BUG不止”。
今天我們要探究的不是哪些可以重現的BUG,我們把哪些可以重現的BUG不定義為BUG,只有哪些不可重現的BUG,會讓你茶飯不思、坐立不安。我曾在一家公司開發服務器軟件,結果因為程序不穩定,而且都是一些不可重現的錯誤,導致我們需要不停的派人盯着服務器運行。不穩定就像一個惡鬼一樣終日縈繞在我們心頭,領導的不停催促,客戶的不停投訴,讓我們項目組個個疲於奔命,叫苦連天。我在查了無數個不可重現的BUG發現,主要是由於以下八種原因引起的:
1. 變量沒有初始化;
2. 函數返回值沒有初始化;
3. 編譯優化導致的錯誤;
4. 函數遞歸;
5. 消息重入;
6. 野指針;
7. 內存泄漏;
8. 並發;
你會發現都是一些細小問題,因此程序員在日常開發中一定要養成好的習慣。
二、變量沒有初始化
DELPHI默認初始化的變量是:全局變量、類成員,其它在函數體的變量都不會初始化,因此一些用於判斷或者循環的變量一定要記得初始化,另外枚舉類型、申請的內存都需要初始化,PCHAR一定要在末尾加#0。例如:下面的返回結果有可能會出現亂碼。
function TempPath: string;
begin
SetLength(Result, GetTempPath(0, PChar(Result)));
GetTempPath(Length(Result), PChar(Result));
Result := PChar(Result);
end;
正確的寫法應該
function TempPath: string;
begin
SetLength(Result, GetTempPath(0, PChar(Result)));
ZeroMemory(PChar(Result), Length(Result));
GetTempPath(Length(Result), PChar(Result));
Result := PChar(Result);
end;
這個程序就是典型的在申請內存的時候,沒有對PCHAR進行初始化,因此末尾有可能是隨機值,但是通過ZeroMemory就把末尾賦#0。
三、函數返回值沒有初始化
在DELPHI中退出函數是使用Exit函數的,有很多函數在退出的時候,沒有對函數返回值初始化,那么函數的返回值返回就是一個隨機值,對程序運行造成不可重現錯誤。例如:下面程序的執行結果會讓你大吃一驚。
procedure NotInitResult;
var
i: Integer;
function GetString(AValue: Integer): string;
begin
if AValue = 0 then
Result := 'True';
end;
begin
for i := -1 to 1 do
begin
ShowMessage(GetString(i));
end;
end;
你看到的運行結果是:‘’、‘True’、‘True’,正確的寫法應該是:
procedure NotInitResult;
var
i: Integer;
function GetString(AValue: Integer): string;
begin
if AValue = 0 then
Result := 'True'
else
Result := ‘’;
end;
begin
for i := -1 to 1 do
begin
ShowMessage(GetString(i));
end;
end;
因此針對if或者Case語句一定要賦初始值,上面的函數的寫法也可以寫為:
function GetString(AValue: Integer): string;
begin
Result := ‘’;
if AValue = 0 then
Result := 'True';
end;
function GetString(AValue: Integer): string;
begin
case AValue of
0: Result := ‘True’;
else Result := ‘’;
end;
end;
四、編譯優化導致的錯誤
現在的編譯器在編譯代碼的時候會優化掉一些可以不執行的代碼,例如:布爾類型優化是最常見的一種,下面的例子能很好的說明這個問題。
procedure TForm1.btn1Click(Sender: TObject);
var
s: string;
begin
if GetTrue or GetValue1(s) then
ShowMessage('Hello ' + s);
end;
procedure TForm1.btn2Click(Sender: TObject);
var
s: string;
begin
if GetTrue or GetValue2(s) then
ShowMessage('Hello ' + s);
end;
function TForm1.GetTrue: Boolean;
begin
Result := True;
end;
function TForm1.GetValue1(var s: string): Variant;
begin
Result := True;
s := 'World';
end;
function TForm1.GetValue2(var s: string): Boolean;
begin
Result := True;
s := 'World';
end;
你會發現單擊btn1時出現的結果是:“Hello Word”,但是單擊btn2的時候是:“Hello”,這個就是因為單擊btn2的時候由於GetTrue返回的是真,所以第二句不執行,但是btn1由於還要進行Variant到Boolean類型的轉換,因此肯定會執行。
五、函數遞歸
如果存在遞歸函數,就需要特別注意,是否會正常退出函數執行,如果一直執行下去,會把程序調用堆棧全部吃完,導致程序異常終止,如下例:只要一點btn1,程序就會無聲無息死掉,而且沒有LOG,這類代碼在以服務方式運行需要特別注意,因為你的服務是無人值守的情況下運行的,如果出現這種情況,你的服務會直接退出,而且沒有任何提示,對於查找問題無從下手。
procedure TForm1.btn1Click(Sender: TObject);
procedure Recursive;
begin
Recursive;
end;
begin
Recursive;
end;
六、消息重入
消息重入的概念是:有一個消息執行過程還沒有執行,相同的一個消息又進入相同的函數處理。消息重入很大原因是在很多軟件中調用Application.ProcessMessage來更新界面,如果是一個操作需要很長的時間,可以改為線程來執行,或者不調用Application.ProcessMessage函數。例如:下面的函數就很容易導致消息重入。
procedure TForm1.btn1Click(Sender: TObject);
var
i: Integer;
begin
for i := 0 to 10000000 do
begin
Application.ProcessMessages;
end;
end;
如果必須要用Application.ProcessMessage來更新界面,你應該確保在函數執行過程中,這個消息不會第二次投遞,如這個例子你可以通過把btn1的狀態禁用來防止消息重入,正確的寫法是:
procedure TForm1.btn1Click(Sender: TObject);
var
i: Integer;
begin
btn1.Enabled := False;
for i := 0 to 10000000 do
begin
Application.ProcessMessages;
end;
btn1.Enabled := True;
end;
另外在發送消息的時候,也需要特別注意SendMessage和PostMessage的區別,SendMessage是發送等待消息處理完成再返回,PostMessage是投遞到消息緩沖池排隊,立即返回(這時消息可能沒有處理),消息需要等到輪到它的時候再處理。
七、野指針
野指針在編譯時候是無法檢測的,只有在運行時候才會出現,出現野指針最常見的錯誤就是Access violation錯誤(簡稱AV錯誤),出現這種錯誤是你指向的物理內存不可用。出現野指針主要是由於以下四種引起:1、指針變量沒有初始化;2、指針被Free或Dispose之后再次使用;指針操作超越了變量的范圍;4、取string的地址,沒有判斷string是否已經分配內存。
代碼在判斷指針是否是空指針是通過判斷指針的值是否介於0x00000000和0x0000FFFF之間,如果在這之間用if語句是可以判斷,如果不介於這之間,則認為指針是有效的。因此指針在申請之后或者釋放之后,指向的地址是隨機值,因此用if語句是無法判斷。另外在DELPHI中,你把指針置為nil,翻譯成匯編代碼就是異或一下,可以打開CPU窗口查看,如:
Fm := nil;生成的匯編是:xor eax eax,即把指針置為0x00000000。
八、內存泄漏
內存泄漏指的是軟件在運行過程中對於申請的內存空間沒有釋放,導致內存占用越來越大,最后程序異常崩潰,而且此時也不會留下任何痕跡,沒有任何系統日志可查。內存泄漏也分為兩種,一種是程序一起動,然后占用了內存,不會隨着程序運行增長;一種是隨着程序運行不停增長的;如果是第一種可以放過,對二種一定要仔細檢查,檢查工具推薦用FastMM,並且把DELPHI的項目屬性Compiler->Use Debug DCUs和Linker->Map file->Detailed選中,這樣FastMM就可以把申請內存的調用堆棧和MAP地址打出來,非常利於查找內存泄漏。查找內存泄漏一般可以從以下幾個方面考慮:
1. 使用Dispose釋放內存的時候要加上定義信息,如果不加定義信息,對於一些指針或者string釋放不了,對於結構體內部有指針的應先釋放內部指針;
2. 使用FreeMem或FreeMemory釋放內存的時候,可以不加大小信息,這是因為DELPHI內存管理器內部知道指針大小信息;
3. Override函數一定要inherited來釋放父類申請的內存;
4. 申請的內存要確保釋放,可以用Try … finally … end來確保內存的釋放,但是應杜絕這種代碼風格try …申請內存…finally …釋放內存… end;
5. 系統內核對象要確保關閉;
6. 申請的指針如果在某些情況下分配空間,要記得初始化為nil,釋放的時候要判斷是否為空,因為釋放空指針也會導致內存泄漏;
7. 另外PostMessage也有可能導致內存泄漏,這種情況是通過PostMessage發送結構體,釋放內存放在消息處理函數中,這時如果頻繁的調用PostMessage,消息處理循環忙不過來,就會丟掉一些消息,造成內存泄漏,默認的Windows消息隊列長度是4000,如果說消息隊列有4000個,你這時再用PostMessage投遞消息,就會被丟掉,造成申請的結構體無法釋放,造成內存泄漏;
九、並發
如果程序涉及多線程,而且線程之間有協作關系,如果這時線程掛死了,就要查線程同步,一般這類問題比較難查,而且需要對代碼執行流程非常了解,屬於比較難以處理的一類問題。可以借助一些三方工具,比如“procexp.exe”就是一個非常優秀的工具,用他可以看到每個線程的狀態,如果一個線程停在哪不動,你就可以通過MAP地址和調用堆棧找到問題點。如Excel的線程狀態如下圖:
十、一些有效的建議
針對以上的這些問題,我們在日常的開發中,應該注意哪些問題呢,下面是我給出的一些建議:
1.探索需求,需求理解越深寫出代碼的質量、架構就越輕巧,可讀性和維護性大大提高;
2. 測試驅動開發;
3. 良好的代碼風格,良好的編碼習慣對於軟件質量有非常大的提高;
4. 變量(指針、數組)被創建之后應及時把他們初始化;
5. 檢查變量的初始值、缺省值錯誤,或者精度不夠;
6. 類型轉換,一定要善用as和is;
7. 檢查變量上溢或下溢,數組越界;
8. 檢查I/O錯誤,I/O不是總返回真的;
9. 數據結構夠用就好,不要設計面面俱到、非常靈活的數據結構;
10. 差勁的代碼,不要想着改改又可用了,應當重新編寫,因為極有可能導致按下葫蘆浮起瓢;
11. 對程序編譯出現的每一個告警,都認真對待,要編寫無警告的代碼;
12. 對於不需要修改的參數帶上const,不但可以提高效率,而且可以增強安全;
這個例子是我原來寫的一個完成端口演示程序,沒有經過嚴格的穩定性校驗,只是做為如何編寫的一個樣本,僅供大家參考,下面是完成端口的簡單介紹:
“完成端口”模型是迄今為止最為復雜的一種I/O模型,特別適合需要同時管理為數眾多的套接字,采用這種模型,往往可以達到最佳的系統性能。但是只適合Windows NT和Windows 2000及以上操作系統。因其設計的復雜性,只有在你的應用程序需要同時管理數百乃至上千套接字的時候,而且希望隨着系統內安裝的CPU數量增多,應用程序的性能也可以線性提升,才考慮采用“完成端口”模型。
重疊I/O(Overlapped I/O)模型使應用程序達到更佳的系統性能。重疊模型的基本設計原理便是讓應用程序使用一個重疊的數據結構,一次投遞一個或多個Winsock I/O請求。針對哪些提交的請求,在它們完成之后,應用程序可為它們提供服務。該模型適用於除Windows CE之外的各種Windows平台。
開發完成端口最具有挑戰是線程個數和管理內存,創建一個完成端口后,就需要創建一個或多個“工作者線程”,以便在I/O請求投遞給完成端口對象后,為完成端口提供服務。但是到底應創建多少個線程,這實際正是完成端口最為復雜的一個方面,一般采用的是為每一個CPU分配一個線程(有的是CPU個數加1,有的是CPU*2的線程個數)。內存分配效率低是因為應用程序在分配內存的時候,系統內核需要不停的Lock/UnLock,而且在多CPU的情況下,會成為整個程序性能的瓶頸,不能隨CPU的個數增加而性能提高,一種比較好的做法一個一次分配多塊內存。
下面是我寫一個的完成端口的演示程序,在我的電腦上測試可以達到鏈接5100個客服端,服務器性能還很好,由於我寫的客服端占用資源比較多,最后直接重啟了,具體見代碼。演示程序主要的瓶頸在於發消息的這一塊,在實際應用中應去掉。
代碼下載地址:http://download.csdn.net/source/1737865
