Delphi SOAP WebService 服务器端多个 SoapDataModule 的做法

以下描述的实验结果,在 Delphi 10.3.3 社区版上测试通过。

前情提要:Delphi 写 WebService 架构的三层程序如何维护 Session

 

简述:

用 Delphi 实现 WebService 服务器端,客户端也使用 Delphi 来开发。

服务器端,可以用 TSoapDataModule 在设计期,可视化地拖放数据库控件,连接数据库,从数据库获取数据,并通过 TDataSetProvider 输出给客户端。在客户端,则可以在服务器运行的情况下,使用 TSoapConnection 去连接我们开发的 WebService 服务器,使用 TClientDataSet 去直接从服务器端的 TDataSetProvider 获取数据,或者提交数据。

在上述架构下,基本上不写程序,设计期拖拉控件,就能建立 WebService 的服务器端和客户端,并且客户端可以获得数据库的数据,编辑后提交到服务器端,最终通过服务器端提交到数据库服务器。

在上述架构下,如果有一些业务逻辑需要控制,可以在服务器端的 TSoapDataModule 里面的 TDataSetProvider 的事件里面,自己写代码,控制客户端可以获得的数据和可以提交的数据。

问题:

如果程序复杂了,代码非常多,全部写在服务器端的一个 TSoapDataModule 里面,代码的组织和管理就成了问题。这时候需要模块化,比如,用多个 TSoapDataModule 来模块化服务器端的业务逻辑代码。

问题是,当服务器端有多个 TSoapDataModule 的时候,客户端采用什么方式,可以定位要访问的 SoapDataModule ?

 

解决方法:

以下是我写的测试程序的描述:

首先,服务器端有一个 SoapDataModule,定义如下:

  ItestSoap = interface(IAppServerSOAP)
    ['{A992AB93-6455-4B0E-A35F-5C7FFBFD4EA3}']
    procedure SaveDataSet(ADeltas: Variant); stdcall;
  end;

  TtestSoap = class(TSoapDataModule, ItestSoap, IAppServerSOAP, IAppServer)

这个是服务器里面的第一个。在只有一个的情况下,客户端的 SoapConnection1 在设计期默认的 SoapServerIID 为:
IAppServerSOAP - {C99F4735-D6D2-495C-8CA2-E53E5A439E61}

URL 为:SoapConnection1.URL := 'http://localhost:8080/soap';

到这里,一切正常,设计期客户端拖过来的 TClientDataSet 是可以看到服务器端的 DataSetProvider 的,设置好以后,设计期就可以在客户端打开 ClientDataSet。这也是 Delphi 开发 Soap WebService 的基本方式。

接下来,我们在服务器端再增加一个 SoapDataModule:

  IsoapDataModule2 = interface(IAppServerSOAP)
    ['{52B5735F-378C-427E-8590-19C4704FAC94}']
    function Hello: string; stdcall;
  end;

  TsoapDataModule2 = class(TSoapDataModule, IsoapDataModule2, IAppServerSOAP, IAppServer)

Delphi 帮我们自动创建的代码框架,这里的 IsoapDataModule2 有一个 ISoapDataModule2 接口,它有一个接口 IID:['{52B5735F-378C-427E-8590-19C4704FAC94}']

服务器运行起来后,客户端做一个 Import WSDL 的操作,在客户端创建 IsoapDataModule2 对应的客户端声明文件。这个操作的 URL 是:http://127.0.0.1:8080/wsdl/IsoapDataModule2

客户端创建了接口声明文件,保存为 IsoapDataModule21.pas ,代码如下:

// ************************************************************************ //
// The types declared in this file were generated from data read from the
// WSDL File described below:
// WSDL     : http://127.0.0.1:8080/wsdl/IsoapDataModule2
//  >Import : http://127.0.0.1:8080/wsdl/IsoapDataModule2>0
// Codegen  : [wfMapArraysToClasses+, wfAllowOutParameters+, wfUseXSTypeForSimpleNillable+, wfCreateArrayElemTypeAlias+]
// Version  : 1.0
// (2020/11/25 23:25:25 - - $Rev: 96726 $)
// ************************************************************************ //

unit IsoapDataModule21;

interface

uses Soap.InvokeRegistry, Soap.SOAPHTTPClient, System.Types, Soap.XSBuiltIns, SOAPMidas;

type

  // ************************************************************************ //
  // The following types, referred to in the WSDL document are not being represented
  // in this file. They are either aliases[@] of other types represented or were referred
  // to but never[!] declared in the document. The types from the latter category
  // typically map to predefined/known XML or Embarcadero types; however, they could also 
  // indicate incorrect WSDL documents that failed to declare or import a schema type.
  // ************************************************************************ //
  // !:string          - "http://www.w3.org/2001/XMLSchema"[Gbl]


  // ************************************************************************ //
  // Namespace : urn:UsoapDataModule2-IsoapDataModule2
  // soapAction: urn:UsoapDataModule2-IsoapDataModule2#Hello
  // transport : http://schemas.xmlsoap.org/soap/http
  // style     : rpc
  // use       : encoded
  // binding   : IsoapDataModule2binding
  // service   : IsoapDataModule2service
  // port      : IsoapDataModule2Port
  // URL       : http://127.0.0.1:8080/soap/IsoapDataModule2
  // ************************************************************************ //
  IsoapDataModule2 = interface(IAppServerSOAP)
  ['{F28A698B-98B0-DF8E-4977-6AE075489E81}']
    function  Hello: string; stdcall;
  end;



implementation
  uses System.SysUtils;

initialization
  { IsoapDataModule2 }
  InvRegistry.RegisterInterface(TypeInfo(IsoapDataModule2), 'urn:UsoapDataModule2-IsoapDataModule2', '');
  InvRegistry.RegisterDefaultSOAPAction(TypeInfo(IsoapDataModule2), 'urn:UsoapDataModule2-IsoapDataModule2#Hello');

end.

客户端的 SoapConnection1 的 URL 或者 SoapServerIID 在设计期无论如何设置,都无法看到第二个 SoapDataModule -- ClientDataSet 的 ProviderName 属性无法下拉看到对应的 DataSetProvider 也无法在设计期打开。但是因为 ProviderName 属性是字符串,设计期是可以手动填写进去的。但填写了,设计期也无法打开。

但是,在运行期,是可以打开的。运行期代码如下:

  //测试服务器端有多个 SoapDataModule 的情况是否可以在客户端调用
  SoapConnection1.Connected := False;
  SoapConnection1.SOAPServerIID := '{F28A698B-98B0-DF8E-4977-6AE075489E81}';
  SoapConnection1.URL := 'http://localhost:8080/soap';
  SoapConnection1.Connected := True;
  ClientDataSet3.Open;

上述代码,其实就是设置了 SOAPServerIID 为第二个 SoapDataModule 里面声明的接口的 GUID;

当然,设置了 SOAPServerIID 后,要打开对应第一个模块的 ClientDataSet 就不行了。因此,打开之前要设置回默认的那个 ID:

  SoapConnection1.Connected := False;
  SoapConnection1.SOAPServerIID := '{C99F4735-D6D2-495C-8CA2-E53E5A439E61}';
  SoapConnection1.URL := 'http://localhost:8080/soap';
  SoapConnection1.Connected := True;

  ClientDataSet1.Close;
  ClientDataSet2.Close;

  ClientDataSet1.Open;
  ClientDataSet2.Open;

上述代码中的 SOAPServerIID 是设计期 SoapConnection1 的默认值。也就是 IAppServerSOAP - {C99F4735-D6D2-495C-8CA2-E53E5A439E61}  --  这个字符串在设计期,拖到界面上的 SoapConnection1 的属性面板里面能看到。

到这里,一切正常。

其实,上述代码中,对 SOAPServerIID 的设置,也可以不采用默认值,而是采用该服务器端数据模块的接口 ITestSoap 的 IID。

 

需要注意的问题:

第二个 SoapDataModule 的接口,一定要有一个方法,而不能是空的。如果是空的,客户端在执行 Import 的 WSDL 接口声明文件 IsoapDataModule21.pas 里面,正常的是这样的:

InvRegistry.RegisterInterface(TypeInfo(IsoapDataModule2), 'urn:UsoapDataModule2-IsoapDataModule2', '');

但是,如果是空的,会是这样的:

InvRegistry.RegisterInterface(TypeInfo(IsoapDataModule2), 'http://tempuri.org/', '');

如果是上述的不正常的声明,则前面说的调用会失败。

但是,我修改服务器端代码,把第二个 SoapDataModule 里面的接口方法 function Hello: string; stdcall; 去掉,再次编译运行服务器端,但这时候客户端代码不改,维持之前正确的接口代码(通过 WSDL Import 的方式获得),执行结果没问题。

因此,客户端通过 Import WSDL 获得的对应服务器端第二个 SoapDataModule 的接口的声明的代码,如果服务器端的接口是空的,没有函数,自动化生成的代码里面是:

InvRegistry.RegisterInterface(TypeInfo(IsoapDataModule2), 'http://tempuri.org/', '');

把上述代码手动修改为

InvRegistry.RegisterInterface(TypeInfo(IsoapDataModule2), 'urn:UsoapDataModule2-IsoapDataModule2', '');

应该是可以正常工作的。

值得注意的是:

服务器端和客户端都是 Delphi 开发的情况下,服务器端的接口声明如果是放在一个单独的文件里面,客户端可以直接引用那个文件来使用服务器端接口;也可以通过 Import WSDL 的方式,通过网络访问服务器端,自动创建出对应服务器端的接口文件。但是,通过 Import WSDL 自动创建的接口文件,其接口里面声明的 GUID 和服务器端并不一致,却能正常使用。

经过测试,在客户端,如果是采用 Import WSDL 的方式创建的对应服务器端接口的文件,其接口的 IID 和服务器端的不一样,这时候设置 SoapConnection1.SOAPServerIID 的时候采用客户端创建的接口文件里面的 GUID 是可以调用到服务器端对应的接口方法,也可以通过 ClientDataSet 直接打开获取到对应的服务器端的 SoapDataModule 里面的 DataSetProvider 的数据的。

如果客户端直接引用服务器端的接口声明文件而不是采用 WSDL 创建的文件,接口 ID 和上述不同。但将这个接口 ID 设置给 SoapConnection1.SOAPServerIID,也能正确执行所有操作。

这里,居然两种方法的接口的 IID 不同,但都能正常使用,背后是什么机理,还不清楚。

 

服务器端有多个 SoapDataModule 的时候,哪个是第一个呢?

经过测试,第一个就是服务器端的工程文件里面声明在前面的那个。工程文件代码如下:

program WebServiceServerTest;
{$APPTYPE GUI}

uses
  Vcl.Forms,
  Web.WebReq,
  IdHTTPWebBrokerBridge,
  FormUnit1 in 'FormUnit1.pas' {Form1},
  WebModuleUnit1 in 'WebModuleUnit1.pas' {WebModule1: TWebModule},
  MyTestImpl in 'MyTestImpl.pas',
  MyTestIntf in 'MyTestIntf.pas',
  //Unit2 in 'Unit2.pas' {testSoap: TSoapDataModule},
  UsoapDataModule2 in 'UsoapDataModule2.pas' {soapDataModule2: TSoapDataModule},
  Unit2 in 'Unit2.pas' {testSoap: TSoapDataModule};

{$R *.res}

begin
  if WebRequestHandler <> nil then
    WebRequestHandler.WebModuleClass := WebModuleClass;
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

上述工程文件我修改过,把 UsoapDataModule2 放到 Unit2 的前面。运行服务器端以后,客户端的 ClientDataSet 下拉 ProviderName 时看到的就是 UsoapDataModule2 里面的 DataSetProvider;

结论:服务器端有多个 SoapDataModule 的时候,工程文件的 uses 里面,声明在前面那个,就是默认的第一个。

默认的第一个 SoapDataModule,在客户端的 SoapConnection.SoapServerIID 设置为默认值,就可以调用到。设计期也能看到。

 

总结:

1. 服务器端可以有多个 SoapDataModule;

2. 客户端的 SoapConnection 要访问哪个 SoapDataModule,则设置其对应的 IID;SoapConnection.URL := 'http://127.0.0.1:8080/soap';  这个 URL 不变。变的仅仅是 SoapConnection1.SOAPServerIID

3. 上述 IID 是客户端对应的服务器端 SoapDataModule 里面的接口的 IID;在客户端,这个 IID 有两种来源方式,取一种就可以了;

3.1. 服务器端运行起来,客户端通过 Delphi IDE 提供的菜单里面的 Import WSDL 菜单提供的功能,从服务器获得接口定义文档,Delphi 自动创建对应的 pas 源代码文件。这是一种方式,尤其是当服务器端是其它语言比如 JAVA 开发的时候,用 Delphi 开发客户端,需要通过这种方式获得服务器端的接口声明。上面说的 IID 就在这个接口声明文件里面。

3.2. 服务器端是 Delphi 开发的,客户端可以直接引用服务器端的接口声明文件,使用服务器端接口声明文件里面给该接口的 IID;

3.3. 这里有点奇怪的是,3.1 生成的接口声明里面的 IID 和 3.2 里面提到的服务器端的接口声明源代码的 IID 并不相同。但是,经过测试,同样有效。

 

进一步的问题:

切换访问不同的 SoapDataModule,需要客户端 SoapConnection 切换 SOAPServerIID,而切换这个 ID,必须先关闭连接,代码如下:

procedure TForm1.Button6Click(Sender: TObject);
begin
  //测试服务器端有多个 SoapDataModule 的情况是否可以在客户端调用
  SoapConnection1.Connected := False;
  SoapConnection1.SOAPServerIID := '{52B5735F-378C-427E-8590-19C4704FAC94}'; // '{C99F4735-D6D2-495C-8CA2-E53E5A439E61}'; // '{F28A698B-98B0-DF8E-4977-6AE075489E81}';
  SoapConnection1.URL := 'http://localhost:8080/soap';
  SoapConnection1.Connected := True;
  ClientDataSet3.Open;
end;

上述代码中,必须先执行 SoapConnection1.Connected := False; 否则设置 IID 无效;追踪进 SoapConnection 的源代码,设置其 Connected 属性,实际上是释放了其内部的 RIO;

如果服务器端采用 HTTP Cookie 验证客户端的登录信息,在客户端已经从服务器端获得 Cookie 的情况下,经过测试发现,执行了 SoapConnection1.Connected := False; 以后,访问服务器端,服务器端无法获得客户端的 Cookie;也就是说,客户端的 SoapConnection 没有缓存住之前拿到的 Cookie;

这个问题和 SoapConnection 无法和 HTTPRIO 共享从服务器端获得的 COOKIE 的原因可能是相同的:Cookie 没有缓存在一个统一的地方,实例之间不能共享。按照浏览器的做法,浏览器是把 Cookie 缓存到了硬盘上,浏览器下次打开,还能获得上次访问服务器获得的 Cookie;

这个问题,需要进一步研究。

当然,如果 Cookie 搞不定,客户端登录信息可以采用 URL 参数的方式,因为 SoapConnection 访问服务器端的不同模块,其 URL 是相同的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值