程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 用Delphi建立通訊與數據交換服務器—Transceiver技術剖析(下)

用Delphi建立通訊與數據交換服務器—Transceiver技術剖析(下)

編輯:Delphi

二、 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;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, SvcMgr, Dialogs;
type
TService1 = class(TService)
private
{ Private declarations }
public
function GetServiceController: TServiceController; override;
{ Public declarations }
end;
var
Service1: TService1;
implementation
{$R *.DFM}
procedure ServiceController(CtrlCode: DWord); stdcall;
begin
Service1.Controller(CtrlCode);
end;
function TService1.GetServiceController: TServiceController;
begin
Result := 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; //用於Mail 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
begin
comSource [itmp].ComFace:=NullTest; //將有效值賦與ComFace
NullTest:=rece.Fields['interval'].value;
//得到系統配置中COM對象獲取數據的觸發時間間隔
SetTimer(application.handle,isource,NullTest*60000,nil);
//為當前Port建立用於定時收取數據的觸發時鐘, isource為Port ID
end
else
comSource [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 then
MailTarget[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;
//分配OnClientError事件的處理過程為TCPServerClientError
TCPSource [itmp].Name:= 'Receive'+inttostr(isource);
//設置Name屬性為'Receive'+Port ID
TCPSource [itmp].Tag:=itarget; //設置為其所在Channel的target ID
TCPSource [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);
begin
DataArrive(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 ID
if (TSCfg.LogOnlyError=false) or (bSendSeccess=false) then
iLogID:=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) or (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:=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;//發送數據成功時,返回值才為True
end;
TCPOBJ.Close;
Except
TCPOBJ.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;
except
Com:= 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管理和Channel集成、收發內存對象、數據流、二進制數據的能力、系統配置信息的讀取和其封裝類的實現、系統及數據的安全性等等,希望讀者朋友們能夠拋磚引玉,理解Transceiver的設計思想,啟發實際開發工作中的靈感火花,做出更加出色強大的軟件。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved