VCL架构下Web Service实现原理分析(重贴)(0分)

  • 主题发起人 主题发起人 flier
  • 开始时间 开始时间
F

flier

Unregistered / Unconfirmed
GUEST, unregistred user!
昨天贴过一遍,发现看不了,斑竹帮忙删一下吧 :(

转贴一篇我去年在水木首发的系列帖子,当时贴出没什么人回应,最后也懒得收尾了
看看这儿有没有人感兴趣,如果有的话,我抽时间把它补全好了……

发信人: flier (小海 (:好日子不多了:)), 信区: Delphi
标 题: VCL架构下Web Service程序实现 - 0.前言
发信站: BBS 水木清华站 (Sun May 12 01:38:05 2002)

VCL架构下Web Service程序实现

Flier Lu <flier_lu@sina.com.cn>

0.前言

本文将以连载显示,通过对Delphi 6/C++ Builder 6环境下,
Web Service程序实现的分析,加深读者对Web Service程序以及
VCL架构本身的了解。从原理上明白其内部运作机制,而非仅仅停留在
RAD的表层认识。
本文的读者应对Delphi的VCL架构有一定水平了解,以及大概了解
Web Service及其相关协议如SOAP,WSDL等等。
对VCL架构下Web服务程序实现原理的分析讨论,请参考拙作
《VCL架构下Web服务程序实现》(精华区/编程技巧/其它讨论/)

btw:
好久没有正正经经写一点东西了,一方面是因为自己越来越懒,
另一方面也是因为学习的知识越多,发现自己不懂的地方越多,
也就失去了以前那种初生牛犊不怕虎的写作劲头。
下午在bbs闲逛时偶然发现被收到精华区中的我大概一年前的
一个连载文章,回忆起当时的劲头,发现自己现在实在很颓废,
于是萌发了写这个连载的念头,希望不会象上次那样虎头蛇尾吧 :)
只所以选择这个题目,一方面是因为Web Service以及SOAP,
WSDL等等技术因为M$的.Net以及IBM/SUN等公司的抄作目前比较
红火,另一方面也因为前段时间看了李维先生的《Delphi 6/Kylix 2

SOAP/Web Service程序设计篇》一书,觉得有种听了半截话的感觉,
加上刚好几个月前大概分析过相关代码,因而想某种程度上进行一些补充。
推荐大家可以购买上面所说的那本书入门,然后结合本文进一步了解
VCL架构下Web Service程序实现的原理。
因为文中讨论的都是较新的技术,笔者难免有遗漏或错误的地方
希望大家不吝指正,如有不明白的地方,请re此文,我会尽量修正。

发信人: flier (小海 (:好日子不多了:)), 信区: Delphi
标 题: VCL架构下Web Service程序实现 - 1.缘起
发信站: BBS 水木清华站 (Sun May 12 01:38:32 2002)

VCL架构下Web Service程序实现

Flier Lu <flier_lu@sina.com.cn>

1.缘起

文章一开始,难免要说些废话,诸如架构、模型之类的场面话,
觉得烦的话,就直接pass好了 :)

1.1 什么是 Web Service

从宏观上理解,Web Service是互联网上的Component,
是面向组件程序设计发展的必然趋势,是面向服务的商业模型的
基础构件,是松散耦合的基于网络的计算模型的构建节点。
从技术层面上理解,Web Service是符合一定标准的特殊的
Web服务程序,和普通的留言簿程序没有本质区别。但因其遵照的
SOAP标准的灵活,使其一跃成为众多大牌软件厂商吹捧的“银弹”。
但到底 Web Service 是什么呢,回答这个问题,我们得
先看看为什么需要Web Service。

1.2 为什么需要 Web Service

随着软件业的发展,软件的设计方法和计算重心不断在变换。
设计方法方面:从最开始的原始的程序编码,如几千行夹杂着
无数goto和n层if的fortran程序;到面向过程程序设计方法(,
如古老的但仍在使用的《数据结构》教材中的算法程序;到基于对象
(object-base)或者说抽象数据类型(ADT Abstract Data Type)
程序设计方法,如C++中经典的STL库的实现;到面向对象
(object-oriented)程序设计方法,如VCL库的实现;到面向接口
(interface-oriented)或面向组件(component-oriented)
程序设计方法。抽象的层面越来越高,抽象的角度也不断变换。
Web Service可以说是面向组件程序设计思想在Internet环境下
的一次必然的演变。(当然也有其他的分支发展,如gp泛型编程、
ao agent-oriented等等)
计算重心方面:从最开始的mainframe+纸带,到mainframe
拖一堆终端,计算重心都偏向于主机;到PC的崛起,计算重心转移到
PC上;到C/S模型的推广,计算重心开始回归服务器;到n-tiered
(n>=3)模型的应用(如MIDAS的瘦客户机模型),计算重心又离开客户端;
到现在的基于Web Service的分布式应用,计算重心真正分布到网络上。
Web Service是支持“网络就是计算机”这个sun喊了多年的口号的
最好解决方案。
加上一些技术层面的需求,如国外的跨平台跨语言见信息通讯的
强大需求;以及商业模型上,由软件销售转向软件服务的需求等等。
Web Service这个概念可以说是应时而生,来得早不如来得巧。

1.3 Web Service 有什么优势

为什么说 Web Service是应时而生呢?
因为仔细研究其概念以及实现技术,就会发现他不同于以往一些概念
的纯粹靠抄作起家,其设计思想完全应需求出发,符合业界的大的发展趋势。
首先,Web Service是面向组件程序设计思想在Internet上的延续。
面向组件程序设计思想在经过若干年发展,(D)COM/CORBA等技术
逐渐成熟的情况下,面临着基于Internet应用的挑战。现有技术暴露出
种种弊端,如私有的通讯协议造成互互通讯见的复杂度,同一标准不同厂家
实现上不同造成对使用者的锁定,如不同平台不同语言环境下交互的复杂,
如Internet下防火墙的限制等等。
其次,Web Service是计算中心分布在网络上,使用者并不关心
软件服务所在,而专心于实现商业价值。对服务提供商来说,也是其从
出售软件转向软件服务商业模型的契机。前者使使用者不必再承担软件
一次性购买、以及长期维护费用,也可以容易地变更服务提供商来符合
自己需求,避免被某些厂商锁定。后者则使软件开发商与软件服务提供商
分工更细,赢利模型转变,使软件开发从制造业向服务业发展。
诸如此类优势还有很多,如实现技术开发性,功能强大等等,不再罗嗦。

1.4 Web Service 相关概念介绍

这里简要介绍一下将要接触到的几个概念,详细的论述请参考李维先生
的《Delphi 6/Kylix 2 SOAP/Web Service程序设计篇》一书

SOAP (Simple Object Access Protocol)

对 Web Service 进行控制的通讯协议

WSDL (Web Service Description Language)

对 Web Service 提供服务的功能与使用方法的描述规范

UDDI (Universal Description Discovery and Integration)

对 Web Service 服务提供商进行定位的黄页

1.5 废话说完,言归正传 :)
发信人: flier (小海 (:好日子不多了:)), 信区: Delphi
标 题: VCL架构下Web Service程序实现 - 2.架构
发信站: BBS 水木清华站 (Sun May 12 01:38:55 2002)

VCL架构下Web Service程序实现

Flier Lu <flier_lu@sina.com.cn>

2.架构

2.1 框架

VCL架构下Web Service程序的实现,完全是构建在Web Broker框架之上。
也就是说其本质上就是一个Web应用程序。因此在创建一个Web Service程序后,
你会发现.dpr工程文件中的代码和普通Web程序完全一样,如

library Demo;

uses
ActiveX,
ComObj,
WebBroker,
ISAPIApp,
ISAPIThreadPool,
WebModule in 'WebModule.pas' {WM: TWebModule},
HelloImpl in 'HelloImpl.pas',
HelloIntf in 'HelloIntf.pas';

{$R *.RES}

exports
GetExtensionVersion,
HttpExtensionProc,
TerminateExtension;

begin
CoInitFlags := COINIT_MULTITHREADED;
Application.Initialize;
Application.CreateForm(TWM, WM);
Application.Run;
end.

具体分析请参见《VCL架构下Web服务程序实现》一文,这里不再重复。

而Web Service项目创建时缺省建立的三个组件,就是其不同之所在。
THTTPSoapDispatcher是其核心调度算法之所在,THTTPSoapPascalInvoker
是SOAP调用重定向到Pascal方法的调用机制所在,而TWSDLHTMLPublish
则是Web Service描述之WSDL自动实现所在,我们慢慢分析。

2.2 THTTPSoapDispatcher

http调用在到达服务器后,WebModule父类TCustomWebDispatcher
会对其进行分析,抽取参数等信息。然后在TCustomWebDispatcher.HandleRequest
方法中调用TCustomWebDispatcher.DispatchAction方法,将调用
根据其path info重定向到相应的处理方法去。而DispatchAction方法将
Action重定向到FDispatchList字段中所有的实现了IWebDispatch接口的组件

@HTTPAPP.pas
....
function TCustomWebDispatcher.HandleRequest(
Request: TWebRequest
Response: TWebResponse): Boolean;
begin
FRequest := Request;
FResponse := Response;
Result := DispatchAction(Request, Response);
end;
....
function TCustomWebDispatcher.DispatchAction(Request: TWebRequest;
Response: TWebResponse): Boolean;
....
while not Result and (I < FDispatchList.Count) do
begin
if Supports(IInterface(FDispatchList.Items), IWebDispatch, Dispatch) then
begin
Result := DispatchHandler(Self, Dispatch,
Request, Response, False);
end;
Inc(I);
end;
....
function DispatchHandler(Sender: TObject
Dispatch: IWebDispatch
Request: TWebRequest
Response: TWebResponse;
DoDefault: Boolean): Boolean;
begin
Result := False;
if (Dispatch.Enabled and ((Dispatch.MethodType = mtAny) or
(Dispatch.MethodType = Dispatch.MethodType)) and
Dispatch.Mask.Matches(Request.InternalPathInfo)) then
begin
Result := Dispatch.DispatchRequest(Sender, Request, Response);
end;
end;
....
IWebDispatch = interface
...
function DispatchRequest(Sender: TObject
Request: TWebRequest
Response: TWebResponse): Boolean;
....
而THTTPSoapDispatcher正是实现了IWebDispatch,其将在
TCustomWebDispatcher.InitModule方法中被自动检测到并加入
FDispatchList字段,代码如下
@HTTPAPP.pas
....
procedure TCustomWebDispatcher.InitModule(AModule: TComponent);
var
I: Integer;
Component: TComponent;
DispatchIntf: IWebDispatch;
begin
if AModule <> nil then
for I := 0 to AModule.ComponentCount - 1 do
begin
Component := AModule.Components;
if Supports(IInterface(Component), IWebDispatch, DispatchIntf) then
FDispatchList.Add(Component);
end;
end;
....
@WebBrokerSOAP.pas
....
THTTPSoapDispatcher = class(THTTPSoapDispatchNode, IWebDispatch)
...
因而Web Service程序的http请求处理实际上是由THTTPSoapDispatcher进行的。
我们接着看看THTTPSoapDispatcher.DispatchRequest方法中对SOAP
协议的处理,关键代码如下
....
function THTTPSoapDispatcher.DispatchRequest(Sender: TObject;
Request: TWebRequest
Response: TWebResponse): Boolean;
....
{ Get SOAPAction header sent over }
SoapAction := Request.GetFieldByName(SHTTPSoapAction);
{ CGI/Apache remap headers with 'HTTP_' prefix }
if SoapAction = '' then
SoapAction := Request.GetFieldByName('HTTP_' + UpperCase(SHTTPSoapAction));
{ Get the path to which client posted - SOAPAction may not tell us
which porttype to dispatch to
we'll then use the path }
....
SHTTPSoapAction = 'SOAPAction';
....
SOAPAction是SOAP协议对HTTP实现的可选扩展,通过在HTTP Header里面指定
SOAPAction关键字来定位对Web Service功能的调用,可以使Web Service程序
在处理这个SOAP包之前了解是否支持此次SOAP Action,以提高效率。
Delphi中对SOAPAction的定义是以urn:[单元]-[接口]#[方法]调用形式
构建的,如SOAPAction: "urn:HelloIntf-IHello#SayHello"表示调用
HelloIntf单元中的IHello接口实现的SayHello方法。
然后是读取Request的数据,SOAP封包中可能保护大量数据
Stream := TMemoryStream.Create;
try
BytesRead := Length(Request.Content);
if BytesRead < Request.ContentLength then
begin
SetLength(Buffer, Request.ContentLength);
Stream.Write(Request.Content[1], BytesRead);
repeat
ChunkSize := Request.ReadClient(Buffer[0], Request.ContentLength - BytesRead);
if ChunkSize > 0 then
begin
Stream.Write(Buffer[0], ChunkSize);
Inc(BytesRead, ChunkSize);
end;
until ChunkSize = -1;
end else
Stream.Write(Request.Content[1], BytesRead);
接着是调用实际的Action处理方法,最后返回响应数据
RStream := TMemoryStream.Create;
try
FSoapDispatcher.DispatchSOAP(Path, SoapAction, Stream, RStream);
RStream.Position := 0;
Size := RStream.Size;
SetLength(WResp, (Size div 2));
RStream.ReadBuffer(WResp[1], Size);
Response.Content := UTF8Encode(WResp);

if (soUTF8InHeader in ConvertOptions) then
Response.ContentType := ContentTypeUTF8
else
Response.ContentType := ContentTypeNoUTF8;
Result := True;
finally
RStream.Free;
end;
其中FSoapDispatcher: IHTTPSoapDispatch;是基于SoapAction
重定向调用的Dispatcher,等会详细讨论。这里要注意到对UTF8的特殊处理
ContentTypeUTF8 = 'text/xml
charset="utf-8"'
{ Do not localize }
ContentTypeNoUTF8 = 'text/xml'
{ Do not localize }
通过HTTP协议头的ContentType字段定义返回数据类型的字符集。因为
很多情况下,系统是无法对中文等非ascii码进行处理的,可以通过使用utf8
编码解决这个问题,详细使用方法略过。
最后是异常处理机制。整个DispatchRequest方法被保护在一个try except
里面,发生异常后会自动将异常信息以SOAP异常封包形式返回给用户。
except
on E: Exception do
begin
{ Default to 500, as required by spec. }
Response.StatusCode := 500;
{ We may, however, return 200 instead }
if (soReturnSuccessForFault in ConvertOptions) then
Response.StatusCode := 200;
Response.Content := UTF8Encode(Format(DefException, [E.Message]));
if (soUTF8InHeader in ConvertOptions) then
Response.ContentType := ContentTypeUTF8
else
Response.ContentType := ContentTypeNoUTF8;
Result := True;
end;
end;
....
SSoapXMLHeader = '<?xml version="1.0" encoding=''UTF-8''?>';
....
const DefException =
SSoapXMLHeader +
' <SOAP-ENV:Envelope' + ' ' +
'xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" >' +
'<SOAP-ENV:Body> ' +
'<SOAP-ENV:Fault> ' +
'<faultcode>SOAP-ENV:Server</faultcode>' +
'<faultstring>%s</faultstring>' +
'</SOAP-ENV:Fault>' +
'</SOAP-ENV:Body>' +
'</SOAP-ENV:Envelope>';
因此我们可以放心的在Web Service程序中使用异常,VCL会自动帮我们善后 :)
至此WebBrokerSOAP单元基本上分析完毕,下面将转入SOAPHTTPDisp单元

2.3 THTTPSoapDispatchNode

前面我们看到THTTPSoapDispatcher实际上是从THTTPSoapDispatchNode
类继承出来的,而类THTTPSoapDispatchNode定义了SOAP的Dispatcher
....
THTTPSoapDispatcher = class(THTTPSoapDispatchNode, IWebDispatch)
....
@SOAPHTTPDisp
THTTPSoapDispatchNode = class(TComponent)
...
FSoapDispatcher: IHTTPSoapDispatch;
...
procedure DispatchSOAP(const Path, SoapAction: WideString
const Request: TStream;
Response: TStream)
virtual;
end;
....
IHTTPSoapDispatch = interface
['{9E733EDC-7639-4DAF-96FF-BCF141F7D8F2}']
procedure DispatchSOAP(const Path, SoapAction: WideString
const Request: TStream;
Response: TStream);
end;
单元SOAPHTTPDisp的代码非常简单,只是对FSoapDispatcher包装进行一把抽象而已,
THTTPSoapDispatchNode类可以看作是IHTTPSoapDispatch接口的一个封装。
这样的话,以后VCL可以根据实现协议不同定义出TSMTPSoapDispatcher甚至
TQQ2002SoapDispatcher来 :)
因为SOAP协议本身,实际上是不依赖于底层通讯协议的,使用http协议只是目前的第一选择
而已,SMTP协议的实现已经有例可寻,甚至你还可以用平信发送SOAP封包,接收者可以扫描
然后OCR进行处理,然后用平信给你寄回回应SOAP封包,hoho

btw: 这里说几句题外话 :)

虽然SOAPHTTPDisp单元很简单,不过其中比较有意思的是以下代码
procedure THTTPSoapDispatchNode.SetSoapDispatcher(const Value: IHTTPSoapDispatch);
begin
ReferenceInterface(FSoapDispatcher, opRemove);
FSoapDispatcher := Value;
ReferenceInterface(FSoapDispatcher, opInsert);
end;
....
procedure THTTPSoapDispatchNode.Notification(AComponent: TComponent
Operation: TOperation);
begin
inherited;
if (Operation = opRemove) and AComponent.IsImplementorOf(FSoapDispatcher) then
FSoapDispatcher := nil;
end;
在SetSoapDispatcher中使用ReferenceInterface把FSoapDispatcher := Value;
赋值语句保护起来是为了使用THTTPSoapDispatchNode.Notification时不会因为重入
造成FSoapDispatcher赋值错误。
....
function TComponent.ReferenceInterface(const I: IInterface
Operation: TOperation): Boolean;
var
ICR: IInterfaceComponentReference;
begin
Result := (I <> nil) and Supports(I, IInterfaceComponentReference, ICR);
if Result then
if Operation = opInsert then
ICR.GetComponent.FreeNotification(Self)
else
ICR.GetComponent.RemoveFreeNotification(Self);
end;
....
IInterfaceComponentReference = interface
['{E28B1858-EC86-4559-8FCD-6B4F824151ED}']
function GetComponent: TComponent;
end;
....
这里的IInterfaceComponentReference接口是从接口到实现组件的逆映射支持。
在面向接口开发中,在实现某些功能时非常有用。Borland这种方法则规范一些,
但灵活性较差,而JCL(www.delphi-jedi.org)提出了另外一种接口无关的
很cool的解决方法,不过跟编译器有关 :)
@JclSysUtils.pas
....
function GetImplementorOfInterface(const I: IInterface): TObject;
{ TODO -cDOC : Original code by Hallvard Vassbotn }
{ TODO -cTesting : Check the implemetation for any further version of compiler }
const
AddByte = $04244483
// opcode for ADD DWORD PTR [ESP+4], Shortint
AddLong = $04244481
// opcode for ADD DWORD PTR [ESP+4], Longint
type
PAdjustSelfThunk = ^TAdjustSelfThunk;
TAdjustSelfThunk = packed record
case AddInstruction: LongInt of
AddByte: (AdjustmentByte: ShortInt);
AddLong: (AdjustmentLong: LongInt);
end;
PInterfaceMT = ^TInterfaceMT;
TInterfaceMT = packed record
QueryInterfaceThunk: PAdjustSelfThunk;

end;
TInterfaceRef = ^PInterfaceMT;
var
QueryInterfaceThunk: PAdjustSelfThunk;
begin
try
Result := Pointer(I);
if Assigned(Result) then
begin
QueryInterfaceThunk := TInterfaceRef(I)^.QueryInterfaceThunk;
case QueryInterfaceThunk.AddInstruction of
AddByte:
Inc(PChar(Result), QueryInterfaceThunk.AdjustmentByte);
AddLong:
Inc(PChar(Result), QueryInterfaceThunk.AdjustmentLong);
else
Result := nil;
end;
end;
except
Result := nil;
end;
end;
至此,Web Service程序的底层架构基本上分析完毕,下一步将看看实现了
IHTTPSoapDispatchf接口的THTTPSoapPascalInvoker.DispatchSOAP
方法到底如何对SoapAction进行处理,以及TWSDLHTMLPublish如何自动生成
发布描述Web Service的WSDL信息。

发信人: flier (小海 (:好日子不多了:)), 信区: Delphi
标 题: VCL架构下Web Service程序实现 - 3.映射
发信站: BBS 水木清华站 (Mon May 13 01:27:59 2002)

VCL架构下Web Service程序实现

Flier Lu <flier_lu@sina.com.cn>

3.映射

3.1 DispatchSOAP

上节我们提到SOAP调用最后回根据其调用的Action被
THTTPSoapPascalInvoker.DispatchSOAP方法进行调度操作。
而在此方法中,实际上是通过THTTPSOAPToPasBind将SOAP调用
以SOAPAction或PathInfo为判断,映射到实现此服务的类的相关
方法上,最后以TSoapPascalInvoker.Invoker调用此方法,
完成操作,并返回结果。关键代码如下
....
@SOAPHTTPPasInv
....
THTTPSoapPascalInvoker = class(TSoapPascalInvoker, IHTTPSoapDispatch)
....

procedure THTTPSoapPascalInvoker.DispatchSOAP(const Path, SoapAction: WideString;
const Request: TStream
Response: TStream);
var
PascalBind: IHTTPSOAPToPasBind;
...
begin
PascalBind := THTTPSOAPToPasBind.Create as IHTTPSOAPToPasBind;
if (SoapAction <> '') and (SoapAction <> '""') then
begin
if not PascalBind.BindToPascalByAction(SoapAction, InvClassType, IntfInfo, ActionMeth) or (InvClassType = nil) then
begin
// 处理异常
end;
end else
begin
if not PascalBind.BindToPascalByPath(Path, InvClassType, IntfInfo, ActionMeth) or (InvClassType = nil) then
begin
// 处理异常
end;
end;
{ Here we've found the interface/method to invoke }
Invoke(InvClassType, IntfInfo, ActionMeth, Request, Response);
end;
TSoapPascalInvoker是THTTPSoapPascalInvoker的基类,提供实现协议无关的
Soap调用到Pascal调用的映射机制,我们等会详细讨论。
THTTPSOAPToPasBind是一个帮助类,负责将Soap调用映射到调用接口的实现类的相应方法
....
IHTTPSOAPToPasBind = interface
['{FDA9957E-F8A1-4E46-9107-9DB7E90E0DBE}']
function BindToPascalByAction(const Action: WideString
var AClass: TClass
var IntfInfo: PTypeInfo
var AMeth: string): Boolean;
function BindToPascalByPath(Path: string
var AClass: TClass
var IntfInfo: PTypeInfo
var AMeth: string): Boolean;
end;
....
THTTPSOAPToPasBind = class(TInterfacedObject, IHTTPSOAPToPasBind)
public
function BindToPascalByAction(const Action: WideString
var AClass: TClass
var IntfInfo: PTypeInfo
var AMeth: string): Boolean
virtual;
function BindToPascalByPath(Path: String
var AClass: TClass
var IntfInfo: PTypeInfo
var AMeth: String): Boolean
virtual;
end;

....
function THTTPSOAPToPasBind.BindToPascalByAction(const Action: WideString
var AClass: TClass
var IntfInfo: PTypeInfo
var AMeth: string): Boolean;
begin
Result := InvRegistry.GetInfoForURI('', Action, ACLass, IntfInfo, AMeth);
end;
....
function THTTPSOAPToPasBind.BindToPascalByPath(Path: String;
var AClass: TClass
var IntfInfo: PTypeInfo
var AMeth: String): Boolean;
begin
Result := InvRegistry.GetInfoForURI(Path, '', AClass, IntfInfo, AMeth);
end;
....
客户端可以使用SOAPAction和传统Web程序PathInfo两种方式进行Soap调用。
InvRegistry是Soap调用到Pascal实现的核心映射库,等会再详细介绍。
以上就是SOAPHTTPPasInv单元的主要功能,以SoapAction和PathInfo两种方式
将客户端的Soap调用,通过InvRegistry.GetInfoForURI映射到相应类/接口/方法
然后以TSoapPascalInvoker.Invoke实现实际调用。

3.2 TInvokableClassRegistry

VCL对Web Service的核心支持之一,就是实现Soap调用到实现类/接口/方法的映射。
而此支持的实现就是在InvokeRegistry单元中。因而此单元可以算是VCL的SOAP支持的核心
单元之一,我们将分几个部分进行讨论。首先是TInvokableClassRegistry类
TInvokableClassRegistry类维护了两个核心数据结构数组。
....
TInvokableClassRegistry = class(TInterfacedObject)
...
FRegClasses: array of InvRegClassEntry;
FRegIntfs: array of InvRegIntfEntry;
....
FRegClasses保存着所有使用RegisterInvokableClass
方法注册到数据库的接口实现类,而FRegIntfs则保存着所有使用
RegisterInterface注册的Soap接口。
....
TCreateInstanceProc = procedure(out obj: TObject);

InvRegClassEntry = record
ClassType: TClass;
Proc: TCreateInstanceProc;
URI: string;
end;
....
InvRegClassEntry保存着实现类的相关信息,ClassType是类的元数据TClass
Proc是Factory函数,负责实际构造实现类,而URI指向此类的Schema。
....
InvRegIntfEntry = record
Name: string
{ Native name of interface }
ExtName: Widestring
{ PortTypeName }
UnitName: string
{ Filename of interface }
GUID: TGUID
{ GUID of interface }
Info: PTypeInfo
{ Typeinfo of interface }
DefImpl: TClass
{ Metaclass of implementation }
Namespace: Widestring
{ XML Namespace of type }
WSDLEncoding: WideString
{ Encoding }
Documentation: string
{ Description of interface }
DefaultSOAPAction: string
{ SOAPAction of interface }
ReturnParamNames: string
{ Return Parameter names }
InvokeOptions: TIntfInvokeOptions
{ Invoke Options }
MethNameMap: array of ExtNameMapItem
{ Renamed methods }
MethParamNameMap: array of MethParamNameMapItem
{ Renamed parameters }
end;
....
InvRegIntfEntry结构则比较复杂,大家可以先看看其英文解释,
等用到时我们再一一详细介绍。
作为核心数据维护者,TInvokableClassRegistry一般不直接
由用户创建,而是通过InvRegistry包装函数访问全局数据库。
var
InvRegistryV: TInvokableClassRegistry;
....
function InvRegistry: TInvokableClassRegistry;
begin
Result := InvRegistryV;
end;
....
initialization
InvRegistryV := TInvokableClassRegistry.Create;
....
finalization
InvRegistryV.Free;
....
用户开发Web Service时,定义的SOAP接口,必须注册到数据库,如
....
IHello = interface(IInvokable)
['{A3D05D28-8C7E-4F47-89A0-5AC983D7085D}']
function SayHello: string
stdcall;
end;
....
initialization
InvRegistry.RegisterInterface(TypeInfo(IHello));
....
而用户实现接口的类,也必须自动注册才能使用,如
....
THello = class(TInvokableClass, IHello)
protected
function SayHello: string
stdcall;
end;
....
initialization
InvRegistry.RegisterInvokableClass(THello);
....
注意,Soap接口最好从IInvokable接口继承出来,但并不是必须。
起码就Delphi6的VCL实现而言,并非必须。这是因为缺省情况下,
为提高效率和减少空间占用,Delphi的接口和类并不包括RTTI信息,
而Soap接口必须能够提供RTTI便于动态映射、调用。
IInvokable接口实际上就是起到提供RTTI信息的作用
{$M+}
IInvokable = interface(IInterface)
end;
{$M-}
可以看到IInvokable接口实际上就是IInterface的翻版,
但被{$M[+|-]}指令包含,此编译指令强制编译器为范围内声明
加上RTTI信息,类似的还有TPersistent类也是如此处理,
因为TPersistent作为可序列化类的基类,必须提供RTTI供
序列化时使用。
好,题外话打住,继续看我们的TInvokableClassRegistry

大家可能注意到,RegisterInvokableClass方法有两种
重载版本,带和不带类工厂函数都可以,如果不显示指定类工厂函数
则实现soap接口的类必须从TInvokableClass继承出来。首先,
因为TInvokableClass类是从TInterfacedObject继承出来
提供了必须的引用计数支持;其次,此类有一个无参数虚构造函数
在构造此类时可直接使用之,而不必另外指定类工厂函数。
TInvokableClass = class(TInterfacedObject)
public
constructor Create
virtual;
end;
TInvokableClassClass = class of TInvokableClass;
接下来看看实际的RegisterInvokableClass函数实现代码
因为此数据库一般作为全局数据库使用,因而在所有涉及此数据库操作
的函数中,都以try...finally包装起来,避免函数重入
....
Lock;
try
...
finally
Unlock
end;
....
procedure TInvokableClassRegistry.Lock;
begin
EnterCriticalSection(FLock);
end;
....

procedure TInvokableClassRegistry.UnLock;
begin
LeaveCriticalSection(FLock);
end;
....
然后,获取接口实现类实现的所有接口。如没有实现任何接口,
则追寻其父类实现的接口。因此,实现soap接口的类,必须在两级继承
以内实现此接口才能正常使用。
不过这里我不是很理解borland为何限制实现soap接口在两级以内
他大可一直追述到TObject为止,递归枚举所有被实现的接口,然后检测
此接口是否从IInvokable接口继承出来,这样也许更合理一些。
否则这种限制是没有什么道理的,IInvokable接口也没有充分利用。
Table := AClass.GetInterfaceTable;
{ If a class does not implement interfaces, we'll try it's parent }
if Table = nil then
begin
if (AClass.ClassParent <> nil) then
begin
Table := AClass.ClassParent.GetInterfaceTable;
{
if Table <> nil then
AClass := AClass.ClassParent;
}
end;
end;
if Table = nil then
raise ETypeRegistryException.CreateFmt(SNoInterfacesInClass, [AClass.ClassName]);
在获取接口表后,将实现类加入到数据库相应数组中
Index := Length(FRegClasses);
SetLength(FRegClasses, Index + 1);
FRegClasses[Index].ClassType := AClass;
FRegClasses[Index].Proc := CreateProc;
最后查看数据库中现有已经注册的接口,有哪些没有关联到实现类,如没有关联的接口
被当前注册的实现类实现的话,就进行关联。InvRegIntfEntry的
GUID字段表示接口的IID,DefImpl表示接口的实现类。
{ Find out what Registered invokable interface this class implements }
for I := 0 to Table.EntryCount - 1 do
begin
for J := 0 to Length(FRegIntfs) - 1 do
if IsEqualGUID(FRegIntfs[J].GUID, Table.Entries.IID) then
{ NOTE: Don't replace an existing implementation }
{ This approach allows for better control on which }
{ class implements a particular interface }
if FRegIntfs[J].DefImpl = nil then
FRegIntfs[J].DefImpl := AClass;
end;
个人感觉Borland这里的实现并不是非常漂亮,大概是开发期限较紧,
还遗留下不少值得改进的地方,期待下一个版本的改进。
接着看看RegisterInterface方法的实现。

首先是看看此接口是否已经被注册
for I := 0 to Length(FRegIntfs) - 1 do
if FRegIntfs.Info = Info then
Exit;
这里是通过InvRegIntfEntry.Info进行比较,此字段执行接口的
TypeInfo信息。所谓TypeInfo信息,就是此接口的RTTI信息所在,
每种有RTTI信息的数据类型,都有一个唯一的TypeInfo定义入口。
然后获取接口信息,填表并加入到相应信息数组中
....
Index := Length(FRegIntfs);
SetLength(FRegIntfs, Index + 1);

GetIntfMetaData(Info, IntfMD, True);
FRegIntfs[Index].GUID := IntfMD.IID;
FRegIntfs[Index].Info := Info;
FRegIntfs[Index].Name := IntfMD.Name;
FRegIntfs[Index].UnitName := IntfMD.UnitName;
FRegIntfs[Index].Documentation := Doc;
FRegIntfs[Index].ExtName := ExtName;
FRegIntfs[Index].WSDLEncoding := WSDLEncoding;

if AppNameSpacePrefix <> '' then
URIApp := AppNameSpacePrefix + '-';

{ Auto-generate a namespace from the filename in which the interface was declared and
the AppNameSpacePrefix }
if Namespace = '' then
FRegIntfs[Index].Namespace := 'urn:' + URIApp + IntfMD.UnitName + '-' + IntfMD.Name
else
begin
FRegIntfs[Index].Namespace := Namespace;
FRegIntfs[Index].InvokeOptions := FRegIntfs[Index].InvokeOptions + [ioHasNamespace];
end;
....
GetIntfMetaData是一个非常底层的函数,通过接口的RTTI信息获取
相应的数据,我们有机会再详细介绍。
GUID是接口的IID;Info是接口的RTTI信息即TypeInfo结构入口;
Name是接口名称,用于输出WSDL或错误信息;UnitName是接口定义所在
单元名称;Documentation是接口描述信息;ExtName是Soap Port名称;

WSDLEncoding是后面要提到的Web Service发布的WSDL信息的编码;
Namespace是SOAP封包中的命名空间名称,由urn:[单元]-[接口]组成
和前面提过的SOAPAction的定义格式相同,都是用于定位的。
可以看到通过RTTI能够获得的信息是相当丰富的。
最后会检测此接口是否被已注册的实现类实现,如找到实现类,则进行关联。
if FRegIntfs[Index].DefImpl = nil then
begin
{ NOTE: First class that implements this interface wins!! }
for I := 0 to Length(FRegClasses) - 1 do
begin
{ NOTE: We'll allow for a class whose parent implements interfaces }
Table := FRegClasses.ClassType.GetInterfaceTable;
if (Table = nil) then
begin
Table := FRegClasses.ClassType.ClassParent.GetInterfaceTable;
end;
for J := 0 to Table.EntryCount - 1 do
begin
if IsEqualGUID(IntfMD.IID, Table.Entries[J].IID) then
begin
FRegIntfs[Index].DefImpl := FRegClasses.ClassType;
Exit;
end;
end;
end;
end;
这里的if FRegIntfs[Index].DefImpl = nil then实际上是废话,
大概Borland开发时原有其他指定DefImpl方式代码,删去时忘记处理这里了吧。
这里又只是处理了两层继承而已,而且是先发现的实现接口的实现类优先。
只能说这样的代码是正确但并非完美的,期待下个版本的改进。
此外还有两个注册相关方法,如下
procedure RegisterExternalMethName(Info: PTypeInfo
InternalName: string
const ExternalName: InvString);
procedure RegisterExternalParamName(Info: PTypeInfo
MethodName, InternalName: string
const ExternalName: InvString);
这两个注册方法可以更改已注册接口的方法或参数的实现信息,这主要是为了
处理当SOAP接口方法或参数名与Delphi关键字相同时的情况。代码较简单,
分析略过。
与注册信息相对应的还有删除信息的方法
....
procedure TInvokableClassRegistry.UnRegisterInvokableClass(AClass: TClass);
begin
DeleteFromReg(AClass, Nil);
end;
....
procedure TInvokableClassRegistry.UnRegisterInterface(Info: PTypeInfo);
begin
DeleteFromReg(nil, Info);
end;
....
删除操作实现都是在TInvokableClassRegistry.DeleteFromReg方法里面完成的。
基本上就是遍历数组,查找信息等等,代码较简单,分析略过。
在分析了TInvokableClassRegistry的数据结构与管理后,我们再回头来看看前面
跳过的TInvokableClassRegistry.GetInfoForURI方法。此方法是从SoapAction
或PathInfo映射到指定接口的实现类的相应方法。
对通过PathInfo进行映射时分析较简单,直接将PathInfo最后一个分隔符'/'后的字符串
作为接口名进行查找,而且没有处理不同单元同名接口的情况,可以说是并不完整的代码。
而通过SoapAction进行映射的代码相对较好,因为有namspace可以进行比较,定位精确。
在找到接口后,返回接口的TypeInfo信息和实现接口的实现类元数据。代码略去。

至此,Soap调用映射到soap接口实现类的方法,这一核心功能基本分析完毕,下次将详细
分析实际执行Soap调用的TSoapPascalInvoker.Invoke方法以及相关机制。

发信人: flier (小海 (:好日子不多了:)), 信区: Delphi
标 题: VCL架构下Web Service程序实现 - 4.调用(1)
发信站: BBS 水木清华站 (Tue May 14 01:57:48 2002)

问一下,到底有没有人在看啊?
要是没人看我就懒得写了,反正分析早就做完了 :(

VCL架构下Web Service程序实现

Flier Lu <flier_lu@sina.com.cn>

4.调用

4.1 TSoapPascalInvoker

上一章中,我们追踪到TSoapPascalInvoker.Invoke方法,
它在获取了有关Soap调用映射到的指定接口的实现类的方法信息后,
实际完成方法的调用,并返回结果信息。
整个方法首先被一个try...finally包含,进行COM初始化
CoInitialize(nil);
try
...
finally
CoUnInitialize;
end;
这是因为Web Service目前使用的http协议是无状态的,
每次处理一个Soap调用时,都必须使用一个新的线程进行处理,
虽然VCL内部使用了诸如线程池之类的优化手段,还是必须在
每次分析SOAP包前后进行COM环境的管理。如果以后Borland
不使用MS的COM形式的XML解析器的话,也许不再需要。
实际上Delphi6里面对XML已经进行了不同Provider的抽象
可以很方便地引入新的提供商的解析器,如xml.apache的。

然后,以一个try...except包装了异常处理代码,在发生
异常时会调用IOPConvert.MakeFault将异常信息映射到
SOAP错误封包。IOPConvert接口是负责在SOAP封包格式和
VCL内部结构间进行转换的,等会还会用到。
try
...
except
on E: Exception do
begin
FConverter := TOPToSoapDomConvert.Create(nil) as IOPConvert;
ExMsg := FConverter.MakeFault(E);
Response.Write(ExMsg[1], Length(ExMsg) * 2);
end;
end;
在实际调用代码开始,构造并设置了RemotableDataContext
这个Context里面包含着此次SOAP调用的方法/参数/返回值等信息
InvContext := TInvContext.Create;
SetRemotableDataContext(InvContext);
try
...
finally
InvContext.Free;
SetRemotableDataContext(nil);
end;
....
TInvContext = class(TDataContext)
private
ResultP: Pointer;
public
procedure SetMethodInfo(const MD: TIntfMethEntry);
procedure SetParamPointer(Param: Integer
P: Pointer);
function GetParamPointer(Param: Integer): Pointer;
function GetResultPointer: Pointer;
procedure SetResultPointer(P: Pointer);
procedure AllocServerData(const MD: TIntfMethEntry);
end;
关于VCL内部使用的接口信息TIntfMethEntry是通过GetIntfMetaData
函数根据接口的RTTI TypeInfo结构获取的。
GetIntfMetaData(IntfInfo, IntfMD, True);
然后通过调用的方法名称获取方法在接口vtable中的索引号
MethNum := -1;
...
if MethName <> '' then
MethNum := GetMethNum(IntfMD, MethName);
接着使用IOPConvert.MsgToInvContext将SOAP调用封包转换为
所需调用方法的信息,在调用完毕后会有一次相应的逆向转换,生产最终
回应给客户端的SOAP response封包
FConverter.MsgToInvContext(Request, IntfMD, MethNum, InvContext);
...
FConverter.MakeResponse(IntfMD, MethNum, InvContext, Response);
在分析完SOAP调用请求封包后,根据请求信息,建立相应接口实现类的实例
Obj := InvRegistry.GetInvokableObjectFromClass(AClass);
if Obj = nil then
raise Exception.CreateFmt(SNoClassRegistered, [IntfMD.Name]);
最后使用TInterfaceInvoker.Invoke方法进行实际的调用操作
{ Dispatch }
Inv := TInterfaceInvoker.Create;
try
Inv.Invoke(Obj, IntfMD, MethNum, InvContext);
finally
Inv.Free;
end;
值得注意的是这里掺杂着几个方法的激活:在实际调用前后,
有TBeforeDispatchEvent和TAfterDispatchEvent事件被激活,
在调用出现异常时,有TOnExceptionEvent事件被激活。
TBeforeDispatchEvent = procedure(const MethodName: string;
const Request: TStream) of object;
TAfterDispatchEvent = procedure(const MethodName: string;
SOAPResponse: TStream) of object;
TOnExceptionEvent = procedure(const MethodName: string;
const Request: TStream
const Response: TStream) of object;

接着来看看Soap接口实现类的构建过程,GetInvokableObjectFromClass
函数根据传入的实现类的元数据TClass,在已注册实现类数据库中寻找相应的项目
如果没有找到,或者相应的类没有提供构造工厂函数的话,就将此类作为
TInvokableClassClass类的子类,直接强制构造,如
if not Found and AClass.InheritsFrom(TInvokableClass) then
Result := TInvokableClassClass(AClass).Create;
这就是为什么TInvokableClassClass类作为父类的实现类在注册时无需提供
构造工厂函数的原因。

4.2 TInterfaceInvoker

在分析完TSoapPascalInvoker类之后,我们发现实际的方法调用被
交给TInterfaceInvoker.Invoke函数完成。此类的实现代码在Invoker
单元里面,不知为何其源代码没有跟随Delphi6发布,也许是因为涉及到太多
Delphi实现相关的底层知识的缘故。好在我们可以在C++ Builder 6的
发布中找到此文件,继续我们的分析。
接下来的分析工作将设计大量的Delphi底层实现原理,阅读前必须对Delphi
底层架构以及RTTI有一定了解,最好熟悉x86汇编,并有足够耐心,否则建议直接
跳过下面分析,阅读下一掌对SOAP封包正/逆向分析/组合的说明。

4.3 GetIntfMetaData

在分析实际调用代码之前,我们先来看看GetIntfMetaData函数的实现。
这个函数我们已经遇到过多次,其大概作用就是获取接口的MetaData元数据信息,
输入接口的TypeInfo,输出TIntfMetaData结构描述的接口信息。

(待续……)
 
good thanks
 
好东西
谢谢!
 
非常感谢!希望继续!
 
不错!继续!
 
期待下文!不要说没有下文了!
 
期待!!
 
谢谢,还没有仔细看
 
搜藏~~
 
怎么没有下文了?
 
极其关注!翘首以待!望穿秋水!思念如飞!
说那么多没用的干哈,拷贝下来,然后安排时间学习先!
 

Similar threads

S
回复
0
查看
906
SUNSTONE的Delphi笔记
S
S
回复
0
查看
884
SUNSTONE的Delphi笔记
S
D
回复
0
查看
1K
DelphiTeacher的专栏
D
后退
顶部