以Delphi建立通訊與資料交換伺服器— Transceiver技術剖析(下)作者:火鳥[email protected]二、 Transceiver Service詳解1. Transceiver Service分析概要Transceiver Service是Transceiver系統的核心組成,Transceiver Kernel負責從系統組態庫讀取Transceiver Console設定的Port、Channel定義與參數,執行階段動態建立與管控通訊Port及其關聯關係,對資料的收、發、緩衝進行調度、對日誌、佇列進行管理等。 Transceiver Shell則是支援全部類型的用於資料收發的Port的實作。 2. Transceiver Service設計概要Transceiver Service是由Delphi中Service application開發而成,Service Application可運行於系統態而非用戶態,由操作系統Service Control Manager (SCM)負責程序的運行管理,Service沒有用戶界面,屬於系統的後台程式。 Transceiver Kernel是Transceiver類別的一系列對Transceiver Shell建立和控制的方法,而Transceiver Shell則是一系列負責通訊的物件集合。註:由於效能和負載的考慮,Transceiver Kernel只是從邏輯上實現上架構圖中的功能劃分,構成模組並未以完全物件化的方式實現。 3. Transceiver Service實作摘要i. 建立一個Service Application從Delphi主選單File中選擇NEW|Other…在彈出的New Items對話框中選擇NEW|Service Application ,可以看到產生的程式框架如下:PRogram Project1;uses SvcMgr, Unit1 in 'Unit1.pas' {Service1: TService};{$R *.RES}begin Application.Initialize; Application.CreateForm(TService1, Service1); Application.Run;end.unit Unit1;interfaceuses Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs; de TuncService1 = class(TService) private { : TServiceController; override; { Public declarations } end;var Service1: TService1;implementation{$R *.DFM}procedure ServiceController(CtrlCode: DWord); stdcall;begin Service1.Controller(CtrlCode);end;function TService1.GetServiceController: TService:; ServiceController;end;end.可以看到除了在uses單元引用了用於服務管理的SvcMgr、TService1繼承自TServiced而非TForm及一個重載的GetServiceController函數和以stdcall方式調用的ServiceController過程之外,用Delphi建立一個服務程序並沒有太多特別之處,Delphi Fans也許又要歡呼了,這就是Delphi RAD的強大迷人之處。另外,Service Application由於無法直接在執行時偵錯,也沒有使用者介面,開發時應考慮偵錯資訊的無介面輸出以利除錯排錯。 ii. 創始滿足特定需求的Port類別要使用運行處理機制統一的Transceiver Kernel,就要求Transceiver Shell中的Port有統一的處理規則,Shell中有些Port是Delphi開發環境中已有的元件類別(如TCP、 FTP等),而有些則不是(如MSMQ、File等)這時就需要自己動手建立一個可以滿足需求的類別。如:type//由於沒有使用者介面,所以繼承自TComponent而非TControl TFilePort=class(TComponent) private FilePath:string;//取得或儲存檔案的資料夾位置Prefix:string;//檔案前綴suffix:string; //檔案後綴end;建立TFilePort類別以後,Transceiver Kernel就可以使用統一的類別處理方式來引用和管理對象,達到從FilePath指定的資料夾下存取特定檔案的目的。如果用於信源(Source),將從特定資料夾下取得符合條件的文件,如果用於信宿(Target),將把從相應信源(Source)得到的資料寫入到指定文件(事實上每一個Port物件的實際參數都來自於系統配置庫中Port表的定義)。另一個例子:type TCOMPort=class(TComponent) private ComFace:string;//取得或提交資料的COM介面end;TCOMPort將用於從指定COM元件介面中取得資料或將資料提交到指定的COM元件介面上進行後續處理。在Delphi中OleVariant類別是實作COM元件呼叫的途徑之一,使用TCOMPort類別的必要性在於,Transceiver在必要的資料存取時才會將TCOMPort定義的COM介面實例化為OleVariant對象,使用結束即釋放對象,這樣能減少Transceiver和COM伺服器的負載壓力。其它類似組件也有相同考慮。作者此處的類別舉例只是一種模型,必要時應加入適當的方法與事件。在開發中作者實作的類別有:TCOMPort、TMSMQPort、TDBPort、TFilePort等iii. 多Channel的支援—宣告Port的物件陣列Transceiver把一個通訊過程看作是來源(Source)到目標(Target)的資料流過程,這樣一個過程是Transceiver中的一個Channel,而這個Channel又是由至少兩個Port構成的(一個用於Source,一個用於Target),所以要定義不定數量並且Source、Target自由組合的多個Channel ,必須分別聲明用於Source和Target 的多種Port類別的物件陣列(並為他們建立對應的關聯關係,稍後您將看到)。如: private { Private declarations }TCPSource:array of TServerSocket;// 用於TCP Source的物件陣列TCPTarget:array of TClientSocket;//用於TCP Target的物件陣列MailSource:array of TIdPOP3; //用於 Source的物件陣列MailTarget:array of TIdSMTP; //用於Mail Target的物件陣列fileSource:array of TFilePort; //用於File Source的物件陣列fileTarget:array of TFilePort; //用於File Target的物件陣列comSource:array of TCOMPort;//用於COM Source的物件陣列comTarget:array of TCOMPort; // 用於COM Target的物件陣列註:由於相同類型的用於Source和Target的Port運作規則的也完全不同,在Transceiver概念中被視為是完全不同且無直接關係的物件。所以同一類型的Port,物件陣列也依Source和Target分別建立。 iv. 執行時期實例化物件陣列每個物件陣列的元素數量由Port Builder在執行時管理,如果使用者透過Transceiver Console定義了一些某種類型的Port,Port Builder將依照其個數和各自參數實例化該物件數組。否則,該物件數組將不會被實例化。在Source類型的Port物件中,Name屬性被設定為'Receive'+Port ID 的形式,在之後的資料接收觸發中,這將有助於Data Dispatcher定位物件和對不同類型的Port物件進行統一調度。 Tag屬性被用來向Channel Controller提供其所在Channel的target ID資訊。以下是Port Builder中對comSource物件陣列的實例化部分begin //Create COM/ Receive Port itmp:=high(comSource)+1;// 取得comSource的目前最大個數,itmp為integer變數SetLength(comSource,itmp +1); // 新增一個comSource陣列成員comSource [itmp]:=TCOMPort.Create(self);//實例化成員comSource[itmp].Name:= 'Receive'+inttostr(isource); //設定Name屬性為'Receive'+Port ID,isource為整型的目前PortID comSource [itmp].Tag:= itarget; //設定為其所在Channel的target ID NullTest:=rece.Fields['Address'].value;//得到系統配置COMFace的值,NullTest為Variant變數if (NullTest <>null) and (trim(NullTest)<>'') then begincomSource [itmp] .ComFace:=NullTest; //將有效值賦與ComFaceNullTest:=rece.Fields['interval'].value;//得到系統配置中COM物件取得資料的觸發時間間隔SetTimer(application.handle,isource,NullTest*60000,nil); //為目前Port建立用於定時收取資料的觸發時脈, isource為Port IDendelsecomSource [itmp].Tag:=-1;//初始化失敗,標識為無效Port end;comSource是用於在一定的時間間隔後對ComFace中定義的接口進行調用並獲取資料的Source類Port,對應comTarget的實作與其類似,只是因為向comTarget的ComFace提交資料是一個即時過程,所以不需要用到觸發間隔,省略建立時脈的兩個語句即可。其它類型的Port物件創建和初始化大同小異。如,另一個MailTarget實作片段: begin //Create SMTP/Send Port itmp:=high(MailTarget)+1; SetLength(MailTarget,itmp+1); MailTarget[itmp]:=TIdSMTP.Create(self); MailTarget[ itmp].Name:='send'+ inttostr(itarget); MailTarget[itmp].Tag:=3;// 設定為Target Port型別標識NullTest:=rece.Fields['Address'].value; //郵件伺服器位址if (NullTest <>null) and (trim(NullTest)<>'') then MailTarget[itmp].Host :=NullTest else bValid:=false; NullTest:=rece.Fields['Port'].value; //郵件伺服器連接埠if NullTest <>null then(if NullTest<>0 then MailTarget[itmp].Port :=NullTest)else bValid:=false; NullTest:=rece.Fields['user'].value;//登入使用者名稱if NullTest <> null thenMailTarget[itmp].UserId :=NullTest else bValid:=false; NullTest:=rece.Fields['password'].value;//登入口令………… …………… end;或許你會有這樣的疑惑,大量的Transceiver Shell通訊元件在運行時被Port Builder創建,Transceiver Service的效能會高嗎?事實上,Port Builder的使命是在ServiceCreate事件發生時一次性完成的,Shell Port的數量只會影響Transceiver Service的初始化速度,Shell Port的通訊速度和Transceiver Servicer的整體效能將不受影響,當然系統資源可能會佔用更多一些。 v. 事件的動態分配與處理在Transceiver Shell所支援的若干種通訊Port當中,使用TServerSocket(可能您更傾向於使用Indy的通訊元件,但這並不違背Transceiver Service的設計思想,只是Shell層面的修改或增加而已)實現的TCPSource是比較有特點的一種,因為TServerSocket作為一種Source Port,不同於COM或POP3之類需要定時觸發的對象,它是在Transceiver Service啟動後時刻處於監聽狀態,當有ClientSocket連線並傳送資料時產生對應事件的元件。以下是TCPSource的實例化片段:begin //Create TCP/Receive Port itmp:=high(TCPSource)+1;SetLength(TCPSource,itmp+1); TCPSource [itmp]:=TServerSocket.Create(self); TCPSource [itmp].OnClientRead:=TCPServersClientRead;//分配OnClientRead事件的處理過程為TCPServersClientRead TCPSource [itmp].OnClientError:=TCPServerClientError;//分配ClientError事件的處理過程為TCPServerver:c. inttostr(isource); //設定Name屬性為'Receive'+Port ID TCPSource [itmp].Tag:=itarget; //設定為其所在Channel的target IDTCPSource [itmp].Socket.Data:=@ TCPSource [itmp].Tag;/ /將此Port物件的target ID作為指標資料附於Socket物件上………… ……………end;回來接著看我們的comSource的處理,在實例化時我們為其建立了觸發時鐘,但如何來處理時鐘觸發時的事件呢?同理,也是事件處理的動態分配。 comSource的時鐘的處理定義可在ServiceCreate事件處理中加入: application.OnMessage:=Timer;實現對訊息處理的重載,當有Application的訊息產生時,Timer就會被觸發,在Timer事件中我們過濾處理時鐘觸發的WM_TIMER訊息,就可以按Port ID和型別實作特定Source Port的資料取得方法的呼叫:Procedure TCarrier.Timer(var Msg: TMsg; var Handled: Boolean);var stmp:string; Obj:TComponent;begin if Msg.message =WM_TIMER then//處理時鐘訊息begin//根據觸發訊息的Port ID找到定義此訊息的物件Obj:=FindComponent('Receive' +inttostr(Msg.WParam)); if obj=nil then exit;//沒有找到就退出處理stmp:=obj.ClassName;//反射取得此Port物件的型別資訊if stmp='TIdPOP3' then GetPOP3(TIdPOP3(Obj)); if stmp='TIdFTP' then GetFTP( TIdFTP(obj)); if stmp='TFilePort' then GetFile(TFilePort(Obj));if stmp='TCOMPort' then GetCOM(TCOMPort(Obj));//呼叫COMSource的資料取得過程…………………… end;end; vi. 取得資料以下是COMSource的資料擷取處理procedure TCarrier. GetCOM(COMObj: TCOMPort);var stmp:string; COMInterface:OleVariant;begin try//根據ComFace的值建立COM元件物件COMInterface:=CreateOleObject(COMObj.ComFace); stmp:=COMInterface.GetData; //呼叫約定的介面方法,取得資料while stmp<>#0 do // #0為約定的資料擷取結束標誌begin DataArrive(stmp,COMObj.Tag);//交由data Dispatcher統一處理, COMObj.Tag為物件所在Channel的Target Port ID stmp:=COMInterface.GetData; end; COMInterface:= Unassigned; except COMInterface:= Unassigned; end;end;// 完成資料擷取操作,釋放元件對象,直到下次觸發呼叫以下是TCPSource的資料取得處理:procedure TCarrier.TCPServersClientRead(Sender: TObject; Socket:TCustomWinSocket);beginDataArrive(socket.ReceiveText,integer(TServerWinSocket(sender).data^));//交由data Dispatcher統一處理, 第二個參數為附於Socket物件sender上的Target Port ID指標值, end;不同類型的Source Port物件其接收資料的方式也不盡相同,但最終都會將所接收到的資料交由data Dispatcher做統一處理。從實作層面講,每加入一種資料接收物件並實現其資料接收,就為Transceiver Shell實現了一種新的Source Port。註:此處作者只是實現了接收文本數據,可能用戶需要接收的是內存對象、數據流或二進制數據,對接收代碼稍做更改即可。 vii. 資料調度Transceiver Service的資料調度是由data Dispatcher邏輯單元完成的,Data Dispatcher的主要任務是對從不同的Source Port接收到的資料進行統一的管理與控制、與Channel Controller協同工作,按Channel的定義向不同的Target Port進行資料分發、監視其發送結果成功與否,並根據發送結果和系統配置庫的設定決定資料是否需要提交到Queue Manager和Log Recorder進行緩衝和日誌處理等等。接下來看看Source Port提交資料的DataArrive方法:procedure TCarrier.DataArrive(sData:String;PortID:Integer);var dTime:Datetime; iLogID:integer; bSendSeccess:Boolean;begin if sData='' then exit;///如資料為空則跳出iLogID:=-1; dTime:= now; //接收時間if sData[length(sdata)]=#0 then sdata:=copy(sdata,1,length(sdata)-1);//用於相容C語言的字串格式bSendSeccess:=DataSend(sdata,PortID) ; //呼叫Data Dispatcher傳送調度方法,PortID為Target Port IDif (TSCfg.LogOnlyError=false) or (bSendSeccess=false) theniLogID:=writeLog(dTime, now,sData, PortID, bSendSeccess);//根據系統設定資訊中的日誌處理規則和傳送結果記錄日誌if (TSCfg.Queueing=True) and (bSendSeccess=false ) then PutQueue(dTime, now,sData, PortID, bSendSeccess, iLogID); //根據封裝系統配置資訊中Queue配置定義決定Queue處理end;以上是Data Dispatcher的DataArrive方法,其中Queue的處理是按照系統配置資訊和發送狀態決定的,也可以調整為強制性的隊列化處理。以下是Data Dispatcher的DataSend方法,用於將資料以Target Port類型分發處理:Function TCarrier.DataSend(sData:String;PortID:Integer):boolean;var Obj:TComponent;begin DataSend:=false;Obj:=FindComponent ('Send'+inttostr(PortID)); //根據Port ID找到物件if (obj=nil) 或 (obj.Tag =-1) then exit;//物件不存在或因初始化失敗已被識別為無效Port case obj.Tag of 1:DataSend:=PutTCP(TClientSocket(obj),sdata); 3:DataSend:=PutSMTP(TIdSMTP (obj),sdata); 5:DataSend:=PutFTP(TIdFTP(obj),sdata); 7:DataSend:=PutHTTP(TIdHTTP(obj),sdata); 9:DataSend:=PutFile(TFilePort(obj),sdata); 11:DataSend:=PutMSMQ(TMSMQPort (obj),sdata); 13:DataSend:DataSend: PutDB(TDBPort(obj),sdata); 15:DataSend:=PutCOM(TCOMPort (obj),sdata); …………… …………… end;end;值得注意的是,如果沒有使用物件數組,而是每種類型的Port只有一個實例的話,處理資料分發處理的更佳辦法應該是使用回呼(Callback)函數,但在現在的情況下,那將導致不知應該由物件數組中哪一個成員處理資料。另外,現在的處理方法使Transceiver Kernel與Transceiver Shell沒有徹底剝離,應該尋求更抽象、獨立性好的處理方法。 viii. 資料傳送以下是TCP的發送Function TCarrier.PutTCP(TCPOBJ:TClientSocket;sdata:string):Boolean;var itime:integer;begin PutTCP:=false; try TCPOBJ.Close; TCPOBJ.Open; itime:=gettickcount; //起始時間repeat application.ProcessMessages; until (TCPOBJ.Active=true) or (gettickcount-itime>5000); //連線成功或5秒超時就跳出循環if TCPOBJ.Active then begin TCPOBJ.Socket.SendText(sdata); PutTCP:=true;//傳送資料成功時,傳回值才為Trueend;TCPOBJ.Close; ExceptTCPOBJ.Close; end; end;以下是COM的發送Function TCarrier.PutCOM(COMOBJ:TCOMPort;sdata:string):Boolean;var Com:OleVariant;begin PutCOM:=false; try Com:=CreateOleObject(COMOBJ.ComFace);//建立預定義的介面PutCOM:=Com.PutData(sdata);//呼叫預先定義的方法Com:= Unassigned; exceptCom:= Unassigned; end; end;其它型別的Port發送大同小異,在此不再贅述。到此為止,Source和Target的基本處理已經完成。一個基本的通訊功能已經建立,經過不同類型的Source和Target的自由匹配,就可以實現完全不同的通訊功能。建立多個Channel,就可以集中實現多個不同功用的通訊處理。 ix. 佇列處理在上文的DataArrive方法中當資料被傳送之後,Data Dispatcher會呼叫資料日誌記錄的writeLog和佇列化處理的PutQueue方法,二者的功能類似,都是根據系統參數對資料資訊進行資料庫的存儲,不是本文的重點。而佇列的Retry處理與Timer事件中按Port類型分發處理的原理類似,是依賴Queue Timer的觸發,將緩衝的資料從資料庫讀出,並依照Target Port ID再次呼叫DataSend進行資料的傳送重試,如發送成功,則本次資料傳輸的事務完成,否則重新進入佇列等待下一次觸發時間進行重試,直到發送成功或達到設定的最大重試數為止。三、開發經驗總結由於本文的重點在於說明Transceiver的核心思想與設計理念,簡化和削弱了Transceiver作為後台服務應當考慮的多線程處理、對像池化以及事務支持、更為複雜強大的Source和Target的Group管理和Cha nnel整合、收發記憶體物件、資料流、二進位資料的能力、系統配置資訊的讀取和其封裝類別的實作、系統及資料的安全性等等,希望讀者朋友們能夠拋磚引玉,理解Transceiver的設計思想,啟發實際開發工作中的靈感火花,做出更出色、更強大的軟體。作者:火鳥[email protected]以Delphi建立通訊與資料交換伺服器—Transceiver技術剖析(上)以Delphi建立通訊與資料交換伺服器—Transceiver技術剖析(下)透過C#實作集合類別縱覽.NET Collections及相關技術老東西:程式捷徑/程式刪除項目/EXE自刪除DIY舊東西:兒時的程式演算法心得筆記