王朝网络
分享
 
 
 

電子商務新紀元-WebService With BizSnap(不比李维的那篇差!)

王朝delphi·作者佚名  2006-01-08
宽屏版  字体: |||超大  

電子商務新紀元-WebService With BizSnap

WebService 是今年最熱門的技術。 所有資訊雜誌都免不了添上幾篇文章,

談談WebService 的遠景,或是她所使用的SOAP 標準,大部份的開發工具也都

投入了WebService 戰局,預期WebService 將會為快速泡沫化的電子商務注入

一股新的動力。 號稱最強的RAD開發工具DELPHI 也不例外,DELPHI 6 提供了BizSnap!

其中包含了3個Wizard,7個元件,讓程式師能夠以最直覺最快速的方式開發及除錯

WebService Server&Client 應用程式,撇開尚未上市的Visual Stdio .NET 不談,

DELPHI 6 可以說是開發WebService 最快速的工具。除了開發工具外,與WebService

相關的SOAP 及WSDL 規格也正以極快的速度成長,預計規格的變動是無法避免的。

雖然目前SOAP 1.1 已經定案,可是參與定制SOAP 的各大廠商實作上都還有差異。

DELPHI 6 所實作的SOAP 及WSDL 比較貼近Sun,IBM 陣營,所以用DELPHI的BizSnap 來呼叫

Java WebService 應該不會有太大的困難,但如果對象是.NET WebService Server 就有點困難度了。

.NET 實作的SOAP 規格預設使用document 方式,而BizSnap 目前只支援rpc,因此與.NET無法溝通,

DELPHI R&D 正在加入document 的支援,相信很快的我們就可以在不更動BizSnap 原始碼的情況下,

正常的呼叫.NET WebService。 跨平台,跨語言是WebService 的主要訴求,希望這些廠商能儘快

解決彼此之間的問題,讓WebService 真正成為我們生活的一部份。

WebService 就如同字面一般,WebService主要是提供服務。 讓其它人可以撰寫程式或經由網頁存取

你所撰寫的服務,或是將你的服務整合至他們的產品中來減少產品開發的時間,讓產品以

更快的速度出現在市場上。 這同時形成了供應鍊關係,你可以利用別人的服務來減少

產品開發時間及成本,而提供服務的廠商則可以藉由販賣服務來獲得利益,使用者也可以

更快速的享受到新產品。 這會使得整個資訊速度大幅提升,也創造出更多的商機。

WebService 也解決了各平台及程式語言間的差異性,使得語言及平台間能夠使用

一致的資料格式來溝通。 舉個例子來說: 像信用卡請款作業,目前各銀行的請款作業

格式都不相同,因此你必須針對個別銀行開發相容與該銀行相容的程式,

這個程式的流程大概是這樣:

上圖中你可以發現到,應用程式必須從資料庫取出資料,轉成銀行要求的請款文件格式

後送給銀行,銀行再將結果轉成特定格式的文件後傳回我們的應用程式,應用程式還得

解譯資料後才能存入資料庫。 這是目前所有信用卡銀行所使用的請款方式,這種方式既

麻煩又沒有公定的資料標準,程式的撰寫也很花時間。 使用者還必須手動操作文件的匯

出及匯入的動作,這既不聰明也容易出錯,假如銀行提供一個WebService 用來處理請款作業,

那麼情況就大大不同了。

上圖中應用程式只需要使用HTTP+SOAP 文件就可以Invoke(喚起)銀行所提供的WebService,

當WebService 處理了應用程式所傳過來的文件後,將結果以SOAP 文件傳回應用程式,

我們的應用程式就可以將資料存入資料庫。 這比起之前的方法聰明,使用者也不需要接觸到

容易出錯的匯入及匯出文件的動作,程式設計師也可以藉由一致的通訊標準來整合各語言

跟平台,這個技術的關鍵點就在於 SOAP。 簡單的說! SOAP 是一組訊息標準,用來傳遞訊息,

使不同語言撰寫的應用程式可以互相溝通。 下一章我會詳細討論SOAP,現在讓我們回到信用卡

請款範例,雖然我們有一致的文件標準,但是文件的內容還是會有差異性,例如甲銀行需要地址,

乙銀行不需要等問題。 那代表程式設計師又得回到老路了嗎? 或許不! 程式還是得寫,

只是這次你有WSDL Tools 幫助你產生骨架程式,比起原先的工作簡單多了。看下圖:

支援WebService 的開發工具應該都會附帶一個WSDL Tools,用來產生SOAP Proxy Code。

上圖中開發工具以送出HTTP Request 的方式向WebService 要求WSDL 文件,之後將

傳回結果交給WSDL Parser, WSDL Parser 根據WSDL文件產生出SOAP Proxy Code,

這個SOAP Proxy Code 在DELPHI 中就是Interface 的定義。 你可以使用THTTPRIO 元件

以as 指令取出這個Interface 後,就可以直接呼叫WebService 中的Method。 在Interface定義

中除了Method 的資訊外,還存在了傳入值與傳回值的型別定義,如果傳回值或傳入值是

複雜型態的話,可能還包含複雜型別的定義及處理的程式碼。 這樣就可以解決上述的格式問題了,

你只需經由各家銀行的WSDL 產生個別的Proxy Code 就能夠呼叫個別的WebService 來完成你的工作,

這樣你要做的事真的減少了許多不是嗎?

SOAP(Simple Object Access Protocol)SOAP 是一個訊息標準。 目前WebService 使用她來當作資料交換的標準,

本文不嘗試完整解釋SOAP! 如果你想完整的了解SOAP 的話,請自行找書

或到W3C 上看看規格書,這裡我只簡單的列出幾樣你應該要知道的部份。

SOAP需要一個通訊協定來傳輸訊息,目前的SOAP 1.0,1.1 都偏向於使用HTTP

做為通訊協定。除了HTTP 之外SOAP 也可以使用SMTP,FTP 等其它通訊協定來傳

輸資料,但是目前除了HTTP 外,其它部份的實作都還沒有定論,由於SOAP 的

Request/Response 作業模式跟HTTP 協定很類似,所以目前的實作都以HTTP為主,

相信以後應該會有更多的協定來滿足不同情況的應用。

一個標準的SOAP Request 樣子大概如下(使用HTTP):

POST /Project2.MyService/soap/IMyService HTTP/1.1

Accept: application/octet-stream, text/xml

SOAPAction: "urn:MyServiceIntf-IMyService#GetComplexType"

…….

<?xml version="1.0" encoding='UTF-8'?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/1999/XMLSchema" xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP-ENV:Body>

<NS1:GetComplexType xmlns:NS1="urn:MyServiceIntf-IMyService" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

<NS1:Param1 xsi:type="xsd:string">

1234

</NS1:Param1>

</NS1:GetComplexType>

</SOAP-ENV:Body>

</SOAP-ENV:Envelope>

我們先看POST /Project2.MyService/soap/IMyService這個部份,這裡指定了

要喚起的是那一個WebService Server 程式,以.NET 來說,這可能是一個ASP .NET 網頁,

以DELPHI 來說,就是一個CGI 或 ISAPI程式。 在下面的SOAPAction 段則是指定我們

想呼叫WebService中的那一個Method, WebService Server 會解讀這一段來喚起對應

的Method 程式碼, 不過SOAP 1.1 並未強制一定要有SOAPAction 值,這必須視實作者而定。

在喚起Method 之前,Server 還要解讀Body 區段中的資訊來取得Client 所傳遞的參數及

型別, 以目前的SOAP規格書來看,你只能夠在Body 區段中包含一個Method 的資訊,

這也代表了一次HTTP Request 只能夠喚起一個Method,雖然有人提出了可以在一次的HTTP

傳輸中喚起多個Method 的資料格式,但目前還沒有正式的規格。 在上例中,<NS1:GetComplexType>

就是我們要呼叫的Method資訊封包,這個區段中包含了呼叫GetComplexType Method的參數型別與值,

NS1:Param1 就是這個Method 所需要的參數,"xsi:type="xsd:string" 就是該參數的型別,

當我們將這個Request 送給WebService 後, WebService 就會根據這些資訊執行對應的Service

及 呼叫Method,完成後她會回覆像這樣的訊息給我們:

HTTP/1.1 200 OK

Content-Type: text/xml

Content-Length: 867

Content:

<?xml version="1.0" encoding='UTF-8'?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/1999/XMLSchema" xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP-ENV:Body>

<NS1:GetComplexTypeResponse xmlns:NS1="urn:MyServiceIntf-IMyService" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:NS2="http://www.w3.org/2001/XMLSchema">

<NS1:return href="#1"/>

<NS2:return id="1" xsi:type="NS2:TXSMyComplexType">

<NS2:ServerMessage xsi:type="xsd:string">

Test My Self

</NS2:ServerMessage>

<NS2:InnerString href="#2"/>

</NS2:return><NS2:InnerString id="2" xsi:type="NS2:TXSInnerType">

<NS2:InnerString xsi:type="xsd:string">This is InnerType</NS2:InnerString>

</NS2:InnerString>

</NS1:GetComplexTypeResponse>

</SOAP-ENV:Body>

</SOAP-ENV:Envelope>

這個WebService 使用了兩個Complex Type, 所以看起來有點亂!

不過你還是可以發現與Request 差別不大,我們先看紅色字的部份

GetComplexTypeResponse 代表呼叫的Method傳回封包, Client 端程式

藉由解讀這部份的資訊來取得傳回值跟型別。例子中藍色及深藍色

部份就是呼叫Method 後的傳回值, 理論上SOAP 可以表現相當複雜的資料型態,

以上面這個例子來說,我使用了一個Complex Type:TXSMyComplexType並且在

裡面使用了一個TXSInnerType,因應情況的不同,你也可以傳遞TXSMyComplexType型態

的陣列或更複雜的資料型態來表示資料庫中的資料列。 SOAP Request/Response Message

通常是由開發工具幫你產生及解讀,所以我們接觸SOAP Message 的機會不多,但這不代表

我們不需要了解SOAP,當你的程式發生問題時,這些資訊將會給你相當大的幫助。

除了Request及Response Message 之外,SOAP 也提供Fault 訊息格式,這個訊息會在

Service Server 發生錯誤時傳回Client 端,DELPHI 的Service Server 會自動將Exception

包裝成這種格式後傳回Client 端,Client 端再根據其中的錯誤訊息產生一個例外。

HTTP/1.1 200 OK

Content-Type: text/xml

Content-Length: 473

Content:

<?xml version="1.0" encoding='UTF-8'?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/1999/XMLSchema"

xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">

<SOAP-ENV:Body>

<SOAP-ENV:Fault>

<SOAP-ENV:faultcode>SOAP-ENV:Server</SOAP-ENV:faultcode>

<SOAP-ENV:faultstring>Test Exception</SOAP-ENV:faultstring>

</SOAP-ENV:Fault>

</SOAP-ENV:Body>

</SOAP-ENV:Envelope>

這應該很易懂,所以我就不再解釋了。

除了這些之外,還有SOAP Header 區段,只是目前的BizSnap 並不能讓我們方便的定義此區段,

在這個區段中可以包含許多自定的資訊,希望以後能夠以BizSnap 來處理這段的資料。

Web Services Description Language (WSDL)WSDL 是用來描述WebService 資訊的語言,應用程式經由解讀WSDL 來取得有關連結

WebService 的資訊,WSDL 大概分成幾部份:

<service name="IDPServiceservice">

<port name="IDPServicePort" binding="IDPServicebinding">

<soap:address location="http://localhost:1024/Project1.MyDPService/soap/IDPService" />

</port>

</service>

藍色部份代表WebService 的名稱,應用程式要連結至WebService 時必須要指定一個Service

跟一個Port, 紅色部份就是Service Port 的描述。 一個Port 必須要有一個SOAP Address Location,

用來描述該Port 的URL 位址,這樣應用程式才能經由這個位址與Service 溝通,

之前提過SOAP 支援多種傳輸方式,而binding 屬性就是描述這些資訊的地方:

<binding name="IDPServicebinding" type="IDPService">

<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />

<operation name="SayHello">

<soap:operation soapAction="urn:DPServiceIntf-IDPService#SayHello" />

<input>

<soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="urn:DPServiceIntf-IDPService" />

</input>

<output>

<soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" namespace="urn:DPServiceIntf-IDPService" />

</output>

</operation>

</binding>

藍色部份就是binding 的資訊,這個Port 使用HTTP 傳輸協定,並且指定使用rpc為傳輸樣式。

SOAP 目前定義兩種傳輸樣式: rpc 跟document。 DELPHI 目前只支援rpc 的傳輸樣式,

.NET 則兩個都支援,預設值是使用document 模式。 深藍色部份描述了Service 中的Method,

你可以在裡面發現SOAPAction的資訊跟SOAP Body 區段所使用的樣式(Style)。

SOAP 的Body 區段可以使用 encoded/literal 兩種樣式,

一般來說, rpc 通常搭配encoded,document 則搭配literal樣式。

在這裡描述的只有binding information,Method 所需要的參數及型別則在下面這段描述中:

<message name="SayHelloRequest" />

<message name="SayHelloResponse">

<part name="return" type="xs:string" />

</message>

籃色部份就是message 的描述,這個描述代表SayHello Method 不需要有傳入值,

其紅色部份則描述了SayHello Method 會有一個傳回值,型別是string。

WSDL 中較重要的資訊大概就是這些了,其它有關於WSDL 的詳細資訊,就請你到www.xml.org 查看。

由於目前各家廠商所實作的標準都略有差異,因此自動產生的Proxy Code 常常需要你介入調整,

希望日後能統一。

(.NET 所產生的WSDL 比其它語言來的複雜,所以我不知道要以誰的為準來解釋.......)

DELPHI 的SOAP使用DELPHI 6 來撰寫WebService 可以說既簡單又快速,只要照著On-line Help 操作

大多可以完成你自己的WebService。 有註冊的使用者還可以從Borland Site 傳回一個

Invokable Wizard,這個Wizard 號稱可以讓你60 秒寫出一個簡單的WebService,有註

冊的使用者可別錯過了。 我自己也寫了一個類似的Wizard,文後會提供一個URL

你只要下載後安裝就可以了。 DELPHI 中提供了三個Wizard 來產生WebService 骨架程式,

現在讓我們運用這幾個 Wizard來實際撰寫一個WebService Server!

撰寫WebService Server 程式首先開啟New Items Dialog 後選擇WebService頁,你會看到這幾個Wizard

(其中有兩個是你安裝了我的Wizard 後才有的),SOAP DataModule 是用來產生

與DataSnap 相容的DataModule,SOAP Application則是用來產生一個

WebService Project,WSDL Import則可以讓你經由WSDL 文件產生SOAP Proxy Code。

請選擇Soap Server Application,開一個新WebService Server專案。

接著選擇Server 型態,在這裡我使用的是Web App Debugger,這可以讓我們

利用IDE 來除錯我們的程式,確定後DELPHI 就會為你產生一個骨架程式,

接著我們必須定義我們要提供的 Service Interface跟Implementation 程式碼,

請再開啟New Items Dialog!

請選擇Service Generate Wizard 來產生一個Service 骨架程式。

在Interface Name 區填入DPService 後按確定後你會看到這個Wizard 已經為你產

生了Service 骨架程式,接著請將整個專案存檔,之後我們再為這個骨架程式提供程式碼:

(紅色部份是加進去的)

(定義部份)

unit DPServiceIntf;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry;

type

IDPService = interface(IInvokable)

['{F5AF5412-0882-4C69-8878-4775C1D77555}']

function SayHello:string; stdcall; //WebService 所提供的Method

end;

implementation

initialization

InvRegistry.RegisterInterface(TypeInfo(IDPService));

end.

(實作部份)

unit DPServiceImpl;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,DPServiceIntf;

type

TDPService = class(TInvokableClass,IDPService)

private

{ Private declarations }

public

function SayHello:string; stdcall;

{ Public declarations }

end;

implementation

//Method 的實作

function TDPService.SayHello:string;

begin

Result:='Hello!';

end;

initialization

InvRegistry.RegisterInvokableClass(TDPService);

end.

編譯完成後執行一次,讓她註冊自己,就這樣! 你已經開發了一個簡單的WebService

並提供一個SayHello Method讓Client 端呼叫,接著我們要寫運用她的Client 端程式,

在動手之前我們必須要將WebAppDebuger 啟動,你可以在Tools 選單中找到她。

撰寫Client 端程式使用DELPHI 中寫Service Client 應用程式就像寫一般程式一樣簡單!

請開啟一個新專案後選擇New Items 的 WebService 頁,

再選擇Web Services Importer Wizard,這個工具可以產生SOAP Proxy Code

也就是Interface 定義。

在這裡填入WSDL 所在的URL:

http://localhost:1024/Project1.MyDPService/wsdl/IDPService

當HTTPRIO 啟動時,HTTPRIO 會根據這設定來取得WSDL 文件,

因此當程式完成後,你可以將WSDL 文件存在本機電腦上減少一些網路流量。

執行成功後你會有一個跟下面程式一樣的骨架程式,在這個程式中定義了

WebService 所提供的Method,有了這些資訊我們就可以使用Code Insight

及Compiler 的型別檢查功能來減少錯誤,這也是以DELPHI 開發Service 的好處

之一。

Unit Unit3;

interface

uses Types, XSBuiltIns;

type

IDPService = interface(IInvokable)

['{06D0E720-0D1D-437F-B2BA-8DAB58AF7E30}']

function SayHello: WideString; stdcall;

end;

implementation

uses InvokeRegistry;

initialization

InvRegistry.RegisterInterface(TypeInfo(IDPService), 'urn:DPServiceIntf-IDPService', '');

end.

將專案存檔後,我們接著加入呼叫WebService 的程式碼,

請到元件盤的WebService 頁拖放一個THTTPRIO 元件到FORM 上

並在她的WSDLLocation 特性中填入WSDL 的URL(之前說的那一個),

之後你就可以在Service 及 Port 特性開窗找到Service Name。

當你使用的是其它語言所撰寫的Service 時請記得在Port 特性值中選用

SOAP 結尾的那一個Port,在Java 及 .NET 中除了SOAP 之外,

還有HTTPGet與HTTPPost 兩種Port,DELPHI 目前只支援SOAP Port。

接著請再放入一個TButton 元件到Form 上,並撰寫她的OnClick Event,

整個Client端程式碼應該像下面這個程式:

unit Unit1;

interface

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, Rio, SoapHTTPClient;

type

TForm1 = class(TForm)

HTTPRIO1: THTTPRIO;

Button1: TButton;

procedure Button1Click(Sender: TObject);

private

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

uses Unit2;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);

begin

ShowMessage((HTTPRIO1 as IDPService).SayHello);

end;

end.

請注義粉紅字的部份,當我們要呼叫Service 時,我們必須將HTTPRIO 轉型為該Interface,

之後才能呼叫Method,這同時也是DELPHI 強大的地方, DELPHI 使用Runtim Creating VTable

的方式完成這個工作,比起SOAP Tookit 的Late Binding 方便多了,到現在為止! 我們簡簡單單

就寫了一個WebService 跟一個Client 端了,以DELPHI 6 來開發WebService真的很方便,

只是如果我需要傳送複雜的資料的話,例如一個TDataSet 那我應該怎麼作呢?

這有很多種方法可以達到,如果你真的要傳DataSet 的話,SOAP DataModule+DataSnap 也許比較方便,

但其它語言可能無法讀取,除了DataSnap 之外,你還可以選擇使用Complex Type,

而這種方式也可以輕易被其它語言所接受,這是我們下一節會談到的。

魔法的秘密如你所見! DELPHI 開發WebService 應用程式真的很快速,且直覺。 但魔法是不會憑空出現的,

在神秘的魔法背後,必定藏著許多的故事。 在BizSnap 背後的技術就有如魔法故事一樣,

精妙且有趣, 當然! 知道這些並不能加快你的開發速度或程式品質,不過她卻賦與你另一種

神奇的力量,使你能夠在程式不能正常運作時,迅速找到解決的方法。

首先我們先看看當Service Server 接收到來自Client AP的要求時的處理方式:

當Client AP 第一次呼叫WebService 時,HTTPRIO 會經由WSDLLocalation 特性先取得WSDL 文件,

因此你在圖中可以看到 Client AP 送出一個HTTP Request 給Service Server,這個訊息會經由

TWebModule 轉送給TWSDLHTMLPublish 接著再由TWSDLHTMLPublish 產生WSDL 文件後傳送給Client AP,

這只有在你指定的WSDLLocation 是連結至Service Server 的URL 時才會發生,假使你預先將WSDL 存

放在本機或是第二次呼叫的話,這個動作就不會發生了。

取得WSDL 文件後,HTTPRIO 就會將Request 轉成SOAP Message傳送至Servcie Server,再由TWebModule

轉送給TSOAPDispatcher,接著經由TSOAPDispatcher解讀SOAP Header中的SOAPAction 資訊後轉送給TSOAPPascalInvoker,

這時TSOAPPascalInvoker 會建立一個THTTPSOAPToPasBind 物件並將SOAP Action傳入後取得Service Class Type,

再運用TOPToSoapDomConvert 來解出SOAP Message 中的參數後呼叫對應的Service。

當Service 執行完之後,TOPToSoapDomConvert 就會將Method 的傳回值打包成SOAP Response Message後傳回Client AP。

除了表面上看到的這些之外,InvokeRegistry也扮演著相當重要的角色,因為TSOAPPascalInvoker 必須得經由她來取得

Service Interface 與Service Object,之後才能使用TInterfaceInvoker來呼叫對應的Method。

可惜Borland 並沒有公開Invoker 的原始碼,但我們可以想像其中必定包含許多Compiler 層級的技術,BizSnap 目前

還有一些缺點, 那就是程式設計師無法介入這些步驟! 因為這些元件既沒有事件,也不能繼承,希望Borland以後

能解除這個限制. 除了Service Server 之外,Client AP端的魔法也很有趣,讓我們繼續探索BizSnap Client 的魔法:

還記得之前提到的要求WSDL 文件嗎? 在這張圖中我省略掉她了。 當你利用HTTPRIO 來呼叫Service Method時,

HTTPRIO 會到InvokeRegistry 取得相關的SoapAction 資訊,再將參數等資訊交由TOPToSoapDomConvert 轉換

為SOAP Request Mesage後經由THTTPReqResp 送至Service Server,回返後再由TOPToSoapDomConvert 轉換

SOAP Response Message 成為Pascal 型態後存入對應的物件,乍看之下 Client 的步驟似乎沒有Server 複雜,

其實不然! 因為還有HTTPRIO 可以轉型成任何型態這個魔法沒解開,Borland 也沒有公開RIO 的原始碼,不過我想其中

必定有許多的magic code,身為程式設計師,我很好奇! 不過我也能了解Borland 不公開的想法,罷了! 這對我們並

沒有太大的影響。

關於魔法的初級篇就到此打住,讓我們繼續往下看吧。

傳送複雜型態資料(Complex Type)在SOAP 一節中我提到了SOAP 可以傳送複雜的資料,在這裡我們就使用DELPHI

來實作一個可以傳送Complex Type 的WebService。

首先請你開啟Service專案,並啟動New Items,

你可以看到WebService內有一個Complex Type Generate Wizard,請執行她。

接著填入Type Name 跟勾選Generate Dynamic Array,

這會產生Array Type 的定義。

之後她會產生Complex Type 的骨架程式碼

unit Unit3;

// Created with Thor-Mjollnir Complex-Type Module Creator

interface

uses InvokeRegistry,Types,XMLSchema;

type

TXSPerson=class;

TXSPersonArray = array of TXSPerson;

TXSPerson = class(TRemotable)

private

//declare your data member,remember! data member need publish in published section

FName:string;

FAge:Integer;

{ Private declarations }

published

property Name:string read Fname write Fname;

property Age:Integer read Fage write Fage;

{ Public declarations }

end;

implementation

initialization

RemClassRegistry.RegisterXSClass(TXSPerson, 'http://www.code6421.com/XMLSchema', 'TXSPerson','');

RemTypeRegistry.RegisterXSInfo(TypeInfo(TXSPersonArray),'http://www.code6421.com/XMLSchema','TXSPersonArray');

finalization

RemClassRegistry.UnRegisterXSClass(TXSPerson);

RemTypeRegistry.UnRegisterXSInfo(TypeInfo(TXSPersonArray));

end.

將她存為uXSPerson.pas,之後我們還要回到Interface,在定義的Unit 中添加新的Method。

unit DPServiceIntf;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,uXSPerson;

type

IDPService = interface(IInvokable)

['{F5AF5412-0882-4C69-8878-4775C1D77555}']

function SayHello:string; stdcall;

function GetPerson:TXSPerson;stdcall;

end;

implementation

initialization

InvRegistry.RegisterInterface(TypeInfo(IDPService));

end.

定義好Interface 後,我們還要為這個Method 提供實作碼。

unit DPServiceImpl;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,DPServiceIntf,uXSPerson;

type

TDPService = class(TInvokableClass,IDPService)

private

{ Private declarations }

public

function SayHello:string; stdcall;

function GetPerson:TXSPerson;stdcall;

{ Public declarations }

end;

implementation

function TDPService.SayHello:string;

begin

Result:='Hello!';

end;

function TDPService.GetPerson:TXSPerson;stdcall;

begin

Result:=TXSPerson.Create;

Result.Name:='code6421';

Result.Age:=18; //ha!

end;

initialization

InvRegistry.RegisterInvokableClass(TDPService);

end.

回到Client 端,請你手動在Service Interface 加入GetPerson function的定義,

這個動作並不困難,你只須要將ComplexType Unit Copy 到你的Client 目錄中並use 她就可以了,

接下來你應該就可以呼叫GetPerson 這個Method 了,基本上你的Complex-Type 中也可以包含

Complex-Type,所以組合後可以表現相當複雜的資料型態,除此之外你也可以產生Complex-Type Array

用以表示更複雜的資料,這些都是很簡單的運用,相信你很快就可以上手了。

以DELPHI 來撰寫Complex Type 可說是相當容易,只是在撰寫Complex Type 時要注意一點,

你必需將所有需要傳送的資料放在published 區,這樣DELPHI 才能由RTTI 取得並轉換成可傳送的格式。

傳送檔案或圖形

如果你要運用SOAP 來傳送檔案的話,你可能得多注意一下了! 因為一旦檔案被轉為XML Array Type 後

大小一定會增加。 再加上編入及取出的動作,速度一定很不理想。 以一個14000 Byte 的檔案來說,

就要花掉1,2 分鐘。這對應用程式來說很難接受,因此這裡我介紹兩種處理方法,一種是以TByteDynArray

直接傳遞,另一種則是以MIME 來傳遞,以TByteDnyArray 型別來處理檔案的傳送是相當簡單的技巧,

你只需要寫幾行程式就可以了,但簡單的事總有代價,而這個代價正是效能低落。

我們用之前的WebService 為基礎,為她添加檔案傳送的功能,下面是傳送檔案的Service Server 片段程式:

unit DPServiceIntf;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,uXSPerson,Types;

type

IDPService = interface(IInvokable)

….

function GetFile:TByteDynArray;stdcall;

end;

unit DPServiceImpl;

uses

InvokeRegistry,DPServiceIntf,uXSPerson,Types,Classes,SysUtils;

…..

function TDPService.GetFile:TByteDynArray;stdcall;

var

fs:TFileStream;

iSize:Int64;

begin

fs:=TFileStream.Create('e:\code\Doc-exam\BizSnap\Exam1\test.jpg',fmOpenRead);

iSize:=fs.Seek(0,soFromEnd);

SetLength(Result,iSize);

fs.Seek(0,soFromBeginning);

fs.ReadBuffer(Result[0],iSize);

fs.Free;

end;

這段程式使用TFileStream物件讀入一個JPG,再將它填入TByteDynArray 中傳回給Client 端。

接著我們再修改一下呼叫Service 的Client 程式。

procedure TForm1.Button1Click(Sender: TObject);

var

vByte:TByteDynArray;

msByte:TMemoryStream;

iSize:Integer;

vImage:TJPEGImage;

begin

vByte:=(HTTPRIO1 as IDPService).GetFile;

msByte:=TMemoryStream.Create;

iSize:=High(vByte);

msByte.WriteBuffer(vByte[0],iSize);

msByte.Position:=0;

vImage:=TJPEGImage.Create;

vImage.LoadFromStream(msByte);

Image1.Picture.Graphic:=vImage;

Image1.Refresh;

msByte.Free;

end;

當上面的程式執行起來時你會發現速度相當的慢,你可別以為當掉了!

這就是使用ByteArray 來傳遞檔案或循序型資料的必然後果。因此我的第二個例子就是使用MIME

來傳遞資料,要完成這個工作的話,你必需先撰寫一個Complex Type Class,用來處理MIME 資料,

在這個例子中你可以學到另一種Complex Type 的應用,就是當你的資料格式無法以PASCAL

的資料型態來表示時,你可以撰寫成TRemotableXS的延伸類別,並且override NativeToXS,XSToNativeXS 兩個函式,

將資料轉成字串。 當傳送至Client 端後,再將字串還原為原來的格式,我們的MIME 就需要這種處理方式,

這個程式的原作者是Noah Kriegel! 原來的程式裡面使用到他自己的MIME Library,因此我稍稍修改了一下,

把處理MIME 的部份改成使用Indy,由於DELPHI 6 所附的Indy 對MIME 支援還不夠,

因此你要到Indy 的網站去下載最新的9.0,記得多看一下安裝說明,

網址是: http://www.nevrona.com/Indy/download90.html

安裝後你就可以開始撰寫處理MIME Type 的Complex Type。

unit B64XSClass;

interface

uses SysUtils, InvokeRegistry,IdBaseComponent, IdComponent, IdCoder,IdCoderMIME,Types;

type

EXSBase64Error = class(Exception);

TXSBase64 = class(TRemotableXS)

private

FBase64String: string;

protected

function GetAsString: string;

procedure SetAsString(Value: string);

public

procedure XSToNative(Value: WideString); override;

function NativeToXS: WideString; override;

property AsString: string read GetAsString write SetAsString;

procedure LoadFromFile(const FileName: string);

procedure SaveToFile(const FileName: string);

end;

implementation

uses SoapConst, Windows, Classes, Dialogs;

procedure TXSBase64.XSToNative(Value: WideString);

begin

SetAsString(Value);

end;

function TXSBase64.NativeToXS: WideString;

begin

Result := GetAsString;

end;

function TXSBase64.GetAsString: string;

begin

Result := FBase64String;

end;

procedure TXSBase64.SetAsString(Value: string);

begin

FBase64String := Value;

end;

procedure TXSBase64.LoadFromFile(const FileName: string);

var

InputFile: TFileStream;

Base64Stream: TStringStream;

IdEncoderMIME1: TIdEncoderMIME;

ResultCode:WideString;

begin

if FileName = '' then

raise EXSBase64Error.Create('No Input File Specified');

if not(FileExists(FileName)) then

raise EXSBase64Error.CreateFmt( 'The Specified File, %s'#10#13'Does Not Exist', [FileName]);

try

InputFile := TFileStream.Create(FileName, fmOpenRead);

Base64Stream := TStringStream.Create('');

InputFile.Position := 0;

Base64Stream.Position := 0;

IdEncoderMIME1:=TIdEncoderMIME.Create(Nil);

ResultCode:=IdEncoderMIME1.Encode(InputFile);

Base64Stream.WriteString(ResultCode);

IdEncoderMIME1.Free;

Base64Stream.Position := 0;

FBase64String := Base64Stream.ReadString(Base64Stream.Size);

finally

InputFile.Free;

Base64Stream.Free;

end; { try..finally }

end;

procedure TXSBase64.SaveToFile(const FileName: string);

var

OutputFile: TFileStream;

Base64Stream: TStringStream;

IdDecoderMIME1: TIdDecoderMIME;

ResultCode:WideString;

iSize:Int64;

begin

if FileName = '' then

raise EXSBase64Error.Create('No Output File Specified');

if FBase64String = '' then raise EXSBase64Error.Create('This Object does not Contain a'#10#13+ 'Base64-Encoded File or String');

try

Base64Stream := TStringStream.Create(FBase64String);

OutputFile := TFileStream.Create(FileName, fmCreate);

OutputFile.Position := 0;

Base64Stream.Position := 0;

iSize:=Base64Stream.Seek(0,soFromEnd);

Base64Stream.Position:=0;

ResultCode:=Base64Stream.ReadString(iSize);

IdDecoderMIME1:=TIdDecoderMIME.Create(Nil);

IdDecoderMIME1.DecodeToStream(ResultCode,OutputFile);

finally

IdDecoderMIME1.Free;

OutputFile.Free;

Base64Stream.Free;

end;{ try..finally }

end;

initialization

RemClassRegistry.RegisterXSClass(TXSBase64, XMLSchemaNameSpace, 'base64', '', True );

finalization

RemClassRegistry.UnRegisterXSClass(TXSBase64);

end.

將這個Unit 加到我們的WebService 專案中,接著我們在Service 中添加一個Methods:GetFile64

unit DPServiceIntf;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,uXSPerson,Types,B64XSClass;

type

IDPService = interface(IInvokable)

['{F5AF5412-0882-4C69-8878-4775C1D77555}']

function SayHello:string; stdcall;

function GetPerson:TXSPerson;stdcall;

function GetFile:TByteDynArray;stdcall;

function GetFile64:TXSBase64;stdcall;

end;

implementation

initialization

InvRegistry.RegisterInterface(TypeInfo(IDPService));

end.

別忘了還要提供實作哦:

unit DPServiceImpl;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,DPServiceIntf,Unit4,SysUtils,Types,Classes,B64XSClass;

type

TDPService = class(TInvokableClass,IDPService)

private

{ Private declarations }

public

function SayHello:string; stdcall;

function GetPerson:TXSPerson;stdcall;

function GetFile:TByteDynArray;stdcall;

function GetFile64:TXSBase64;stdcall;

{ Public declarations }

end;

implementation

function TDPService.SayHello:string;

begin

Result:='Hello!';

end;

function TDPService.GetPerson:TXSPerson;stdcall;

begin

Result:=TXSPerson.Create;

Result.Name:='code6421';

Result.Age:=18; //ha!

end;

function TDPService.GetFile64:TXSBase64;stdcall;

begin

Result:=TXSBase64.Create;

Result.LoadFromFile('e:\code\Doc-exam\BizSnap\Exam1\test.jpg');

end;

function TDPService.GetFile:TByteDynArray;stdcall;

var

fs:TFileStream;

iSize:Int64;

begin

fs:=TFileStream.Create('e:\code\Doc-exam\BizSnap\Exam1\test.jpg',fmOpenRead);

iSize:=fs.Seek(0,soFromEnd);

SetLength(Result,iSize);

fs.Seek(0,soFromBeginning);

fs.ReadBuffer(Result[0],iSize);

fs.Free;

end;

initialization

InvRegistry.RegisterInvokableClass(TDPService);

end.

將Service Server專案編譯後,我們回到Client Project撰寫呼叫端的程式:

procedure TForm1.Button1Click(Sender: TObject);

var

Base64:TXSBase64;

vImage:TJPEGImage;

msByte:TMemoryStream;

begin

Base64:=(HTTPRIO1 as IDPService).GetFile64;

msByte:=TMemoryStream.Create;

IdDecoderMIME1.DecodeToStream(Base64.AsString,msByte);

vImage:=TJPEGImage.Create;

msByte.Position:=0;

vImage.LoadFromStream(msByte);

Image1.Picture.Graphic:=vImage;

Image1.Refresh;

msByte.Free;

end;

當你執行這個程式時,你會發現速度快了數十倍以上,這是因為ByteArray 需要維護一些陣列的資訊,

而MIME 則是整塊資料傳輸,速度當然不能比了。

處理資料庫BizSnap 中有一個SOAP DataModule,她可以讓你以DataSnap 的觀念來處理資料庫,

有關這一部份的技術請參考我之前的文章。 如果你希望你的WebService能被其它語言呼叫,

那你需要的是Complex Type 的技術,我在之前的Complex Type 中所展示的GetPerson function

就屬於這一類的技術。 你只需要將TXSPerson 改為TXSPersonArray 就可以傳輸Record Array了,

接著只須將資料填入這個Array 就可以了。這很簡單不是嗎? 那如果要傳輸BLOB 型態的資料呢?

嘿! 那用上面的MIME 來做就好啦,接下來我使用BCDEMOS 中的魚類資料庫來作示範,

在這個資料庫中有兩個BLOB 欄位,一個是MEMO,另一個是圖形,因此我們得建立一個Complex Type 才能

容納這些資料,原來的XSB64Class 已不敷使用,我提供一個新的,你可以在範例檔中找到最新的,這裡

我就不列出來了。 OK! 那我們就開始吧,首先我們得為我們的Service加上一個DataModule,在裡面放上

TDatabase、TSession、TTable並設定TDatabase.AliasName 為BCDEMOS,Table.TableName為biolife.db,

接著我們得再產生一個Complex Type,用來表示資料列,下面是完整的程式碼:

unit uXSFishData;

// Created with Thor-Mjollnir Complex-Type Module Creator

interface

uses InvokeRegistry,Types,XMLSchema,B64XSClass,SysUtils;

type

TXSFishData=class;

TXSFishDataArray = array of TXSFishData;

TXSFishData = class(TRemotable)

private

FCategory:string;

FCommon_Name:string;

FSpeciesName:string;

FLength_cm:double;

FLength_In:double;

FNotes:TXSBase64;

FGraphic:TXSBase64;

procedure SetNotes(Value:TXSBase64);

function GetNotes:TXSBase64;

procedure SetGraphic(Value:TXSBase64);

function GetGraphic:TXSBase64;

{ Private declarations }

public

constructor Create;override;

destructor Destroy;override;

published

property Category:string read FCategory write FCategory;

property Common_Name:string read FCommon_Name write FCommon_Name;

property SpeciesName:string read FSpeciesName write FSpeciesName;

property Length_cm:double read FLength_cm write FLength_cm;

property Length_In:double read FLength_In write FLength_In;

property Notes:TXSBase64 read GetNotes write SetNotes default Nil;

property Graphic:TXSBase64 read GetGraphic write SetGraphic default Nil;

{ Public declarations }

end;

implementation

procedure TXSFishData.SetNotes(Value:TXSBase64);

begin

if not Assigned(FNotes) then FNotes:=TXSBase64.Create;

FNotes.AsString:=Value.AsString;

end;

function TXSFishData.GetNotes:TXSBase64;

begin

Result:=FNotes;

end;

procedure TXSFishData.SetGraphic(Value:TXSBase64);

begin

if not Assigned(FGraphic) then FGraphic:=TXSBase64.Create;

FGraphic.AsString:=Value.AsString;

end;

function TXSFishData.GetGraphic:TXSBase64;

begin

Result:=FGraphic;

end;

constructor TXSFishData.Create;

begin

inherited;

FNotes:=TXSBase64.Create;

FGraphic:=TXSBase64.Create;

end;

destructor TXSFishData.Destroy;

begin

FreeAndNil(FNotes);

FreeAndNil(FGraphic);

inherited;

end;

initialization

RemClassRegistry.RegisterXSClass(TXSFishData, 'http://www.code6421.com/XMLSchema', 'TXSFishData','');

RemTypeRegistry.RegisterXSInfo(TypeInfo(TXSFishDataArray),'http://www.code6421.com/XMLSchema','TXSFishDataArray');

finalization

RemClassRegistry.UnRegisterXSClass(TXSFishData);

RemTypeRegistry.UnRegisterXSInfo(TypeInfo(TXSFishDataArray));

end.

繼續為Service Interface 添加新的Method:

unit DPServiceIntf;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,uXSPerson,Types,B64XSClass,uXSFishData;

type

IDPService = interface(IInvokable)

['{D88D433C-BD4F-4C4F-BB1A-E859A7E9CE45}']

//interface declare like

//function SayHello:string; stdcall;

function SayHello:string; stdcall;

function GetPerson:TXSPerson;stdcall;

function GetFile:TByteDynArray;stdcall;

function GetFile64:TXSBase64;stdcall;

function GetFishData(SpeciesNo:Integer):TXSFishData;stdcall;

end;

implementation

initialization

InvRegistry.RegisterInterface(TypeInfo(IDPService));

end.

實作部份:

unit DPServiceImpl;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,DPServiceIntf,uXSPerson,Types,Classes,SysUtils,

B64XSClass,uXSFishData,uData,db;

type

TDPService = class(TInvokableClass,IDPService)

private

{ Private declarations }

public

//interface implementation

//function SayHello:string; stdcall;

function SayHello:string; stdcall;

function GetPerson:TXSPerson;stdcall;

function GetFile:TByteDynArray;stdcall;

function GetFile64:TXSBase64;stdcall;

function GetFishData(SpeciesNo:Integer):TXSFishData;stdcall;

{ Public declarations }

end;

implementation

function TDPService.SayHello:string;

begin

Result:='Hello!';

end;

function TDPService.GetPerson:TXSPerson;stdcall;

begin

Result:=TXSPerson.Create;

Result.Name:='code6421';

Result.Age:=18; //ha!

end;

function TDPService.GetFile:TByteDynArray;stdcall;

var

fs:TFileStream;

iSize:Int64;

begin

fs:=TFileStream.Create('e:\code\Doc-exam\BizSnap\Exam1\test.jpg',fmOpenRead);

iSize:=fs.Seek(0,soFromEnd);

SetLength(Result,iSize);

fs.Seek(0,soFromBeginning);

fs.ReadBuffer(Result[0],iSize);

fs.Free;

end;

function TDPService.GetFile64:TXSBase64;stdcall;

begin

Result:=TXSBase64.Create;

Result.LoadFromFile('e:\code\Doc-exam\BizSnap\Exam1\test.jpg');

end;

function TDPService.GetFishData(SpeciesNo:Integer):TXSFishData;

var

MS: TMemoryStream;

begin

Result:=TXSFishData.Create;

DataModule3.Table1.Open;

if not DataModule3.Table1.Locate('Species No',SpeciesNo,[]) then

raise Exception.Create('record not found!')

else

begin

MS := TMemoryStream.Create;

Result.Category:=DataModule3.Table1.FieldByName('Category').AsString;

Result.Common_Name:=

DataModule3.Table1.FieldByName('Common_Name').AsString;

Result.SpeciesName:=DataModule3.Table1.FieldByName('Species Name').AsString;

Result.Length_cm:=DataModule3.Table1.FieldByName('Length (cm)').AsFloat;

Result.Length_In:=DataModule3.Table1.FieldByName('Length_In').AsFloat;

TBlobField(DataModule3.Table1.FieldByName('Notes')).SaveToStream(MS);

MS.Position:=0;

Result.Notes.LoadFromStream(MS);

MS.Clear;

TGraphicField(DataModule3.Table1.FieldByName('Graphic')).SaveToStream(MS);

MS.Position:=8;

Result.Graphic.LoadFromStream(MS,False);

DataModule3.Table1.Close;

end;

end;

initialization

InvRegistry.RegisterInvokableClass(TDPService);

end.

還記得嗎? 如果我們要在Client 端取得Service 所傳回的Complex Type,

最快的方法就是將該Complex Type Unit 複製到Client 的目錄並use 它,

呼叫Service 的Client 程式碼如下:

procedure TForm1.Button1Click(Sender: TObject);

var

FishData:TXSFishData;

msByte:TMemoryStream;

begin

FishData:=(HTTPRIO1 as IDPService).GetFishData(90020);

edtCategory.Text:=FishData.Category;

edtCommonName.Text:=FishData.Common_Name;

edtSpeciesName.Text:=FishData.SpeciesName;

edtLengthCM.Text:=FloatToStr(FishData.Length_cm);

edtLengthIN.Text:=FloatToStr(FishData.Length_In);

msByte:=TMemoryStream.Create;

FishData.Notes.SaveToStream(msByte);

msByte.Position:=0;

Memo1.Lines.LoadFromStream(msByte);

msByte.Clear;

FishData.Graphic.SaveToStream(msByte);

msByte.Position:=0;

Image1.Picture.Bitmap.LoadFromStream(msByte);

Image1.Refresh;

FishData.Free;

msByte.Free;

end;

這是執行結果:

雖然花了一點時間,但執行速度還可以接受,這個例子中我只傳一筆資料,

如果你需要傳送多筆資料,只要將傳回型別改為TXSFishDataArray 後

將資料填入傳回Client端就可以了。

Complex Type 的魔法之前提到TOPToSoapDomConvert 會將SOAP Response 轉換為Pascal 型態的物件,

她同時也處理了Complex Type Object,她會利用RemTypeRegistry 中的資訊來

處理傳入及傳回值,不過這部份的處理有點奇怪,不知道是我使用的方法還是BizSnap

的處理模式的關係,當TOPToSoapDomConvert 處理傳回值並建立物件時,並不會經過物件的

constructor,似乎完全跳過這一段了,目前無解中.....

所以你會看到TXSFishData 中有奇特的防護措施,因為我在裡面使用了另一個ComplexType,

而TOPToSoapDomConvert 並不知道如何建立這個物件,因此如果沒有這些防護措施的話,

那就等著當機吧.....

喔...還有件事,你也許會對下面的程式碼感到疑惑:

RemClassRegistry.RegisterXSClass(TXSFishData, ' 'http://www.code6421.com'', 'TXSFishData','');

RemTypeRegistry.RegisterXSInfo(TypeInfo(TXSFishDataArray),' 'http://www.code6421.com'','TXSFishDataArray');

因為這兩個function 傳回的兩個物件都擁有同樣的能力,在On-line help 中的解釋是這兩個

物件是相同的。

BizSnap+DataSnap這其實不難! 難的是你要有3-Tier 程式設計的概念,這些概念當然由李維老師來說比較好,

所以我就跳過了,你可以使用前面的範例加上一個SOAP DataModule 就可以使Service Server

升級成為Sevice+Application Server,或是在Service Server 中使用ClientDataSet 來取得

另一個AP-Server 的資料。 要注意的是運用SOAP 技術通常會有效率上的隱憂,這來自於

XML Parser 的速度問題,我沒詳細去看過ClientDataSet 與SOAPDataModule 之間傳輸

的格式,因此對此無法提出意見。 就讓李老師來解釋會貼切一點,使用SOAPDataModule 通常

是在跨防火牆的情況下,因為工作的關係我可能很快就會接觸到,屆時有機會的話我再

將她記錄下來,是的!! 我要當白老鼠......

WebSnap+BizSnap像.NET 之類的開發工具會直接產生一個網頁,讓使用者可以直接經由網頁呼叫WebService,

這在許多情況下很方便。 DELPHI 也可以應用WebSnap 來處理這樣的動作,有機會的話我會

在WebSnap 篇介紹這樣的運用。

運用別人寫的WebService在Internet 上已經有許多人撰寫好的WebService 可供我們使用,這裡我選幾個來示範如何撰寫

她們的Client 應用程式,首先是Dr.Bob 的HeadNews! 請你開啟一個新Application Project,

執行 WSDL Import 輸入http://drbob42.tdmweb.com/cgi-bin/DrBobsClinic.exe/wsdl/IHeadline

你可以看到WSDL Import 幫我們產生以下的程式碼:

Unit HeadLineIntf;

interface

uses Types, XSBuiltIns;

type

IHeadLine = interface(IInvokable)

['{57E93B78-62C9-4734-9440-214F3E8B7C0F}']

function DelphiNews(const format: Integer): WideString; stdcall;

function CBuilderNews(const format: Integer): WideString; stdcall;

function JBuilderNews(const format: Integer): WideString; stdcall;

function KylixNews(const format: Integer): WideString; stdcall;

function SOAPNews(const format: Integer): WideString; stdcall;

function BorConNews(const format: Integer): WideString; stdcall;

end;

implementation

uses InvokeRegistry;

initialization

InvRegistry.RegisterInterface(TypeInfo(IHeadLine), 'urn:Headline-IHeadLine', '');

end.

接著撰寫呼叫的程式,在這個程式我使用TWebBrowser 元件來顯示HTML,

完整的Client 程式碼如下:

unit uMain;

interface

uses

Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

Dialogs, StdCtrls, OleCtrls, SHDocVw, Rio, SoapHTTPClient;

type

TNewsType=(D_NEWS,C_NEWS,J_NEWS,K_NEWS,S_NEWS,B_NEWS);

TForm1 = class(TForm)

HTTPRIO1: THTTPRIO;

WebBrowser1: TWebBrowser;

Button1: TButton;

Button2: TButton;

Button3: TButton;

Button4: TButton;

Button5: TButton;

Button6: TButton;

procedure Button1Click(Sender: TObject);

private

function GetNews(NewsType:TNewsType):WideString;

{ Private declarations }

public

{ Public declarations }

end;

var

Form1: TForm1;

implementation

uses HeadLineIntf;

{$R *.dfm}

function TForm1.GetNews(NewsType:TNewsType):WideString;

var

Intf:IHeadLine;

begin

Intf:=(HTTPRIO1 as IHeadLine);

case NewsType of

D_NEWS:

Result:=Intf.DelphiNews(1);

C_NEWS:

Result:=Intf.CBuilderNews(1);

J_NEWS:

Result:=Intf.JBuilderNews(1);

K_NEWS:

Result:=Intf.KylixNews(1);

S_NEWS:

Result:=Intf.SOAPNews(1);

B_NEWS:

Result:=Intf.BorConNews(1);

end;

Intf:=Nil;

end;

procedure TForm1.Button1Click(Sender: TObject);

var

Data:string;

NewsType:TNewsType;

fs:TFileStream;

begin

if (Sender as TButton).Caption = 'DELPHI' then

NewsType:=D_NEWS;

if (Sender as TButton).Caption = 'CBuilder' then

NewsType:=C_NEWS;

if (Sender as TButton).Caption = 'JBuilder' then

NewsType:=J_NEWS;

if (Sender as TButton).Caption = 'Kylix' then

NewsType:=K_NEWS;

if (Sender as TButton).Caption = 'SOAP' then

NewsType:=S_NEWS;

if (Sender as TButton).Caption = 'BorCon' then

NewsType:=B_NEWS;

Data:=GetNews(NewsType);

fs:=TFileStream.Create('c:\temp\BobNews.html',fmCreate);

fs.WriteBuffer(Data[1],Length(Data));

fs.Free;

WebBrowser1.Navigate('C:\temp\BobNews.html');

end;

end.

執行的畫面:

因為Dr.Bob 的WebService 是使用DELPHI 為開發工具所撰寫的,我們使用起來可以說毫不費力,但如果是.NET 呢?

很不幸! 目前的BizSnap 並無法正常使用.NET 或MS SOAP的WebService,原因嘛! 肯定是SOAP 及WSDL 的實作不同所致,

以下我們先用MSSOAP Service Server 來試試看,MSSOAP 中有一個範例叫calc,也就是計算機的意思,你只要將它的WSDL

讀入後DELPHI 就會為你產生SOAP Proxy Code,如果你仔細查看產生的程式碼的話你會發現有點怪異! Calc 應該是傳回

一個double 值,但在SOAP Proxy Code 中我們看到的卻是out,這會使得程式在某些情況下出錯,不管這些! 我們先加入呼叫的程式碼,

執行後你會得到一個Internal Error 500 的錯誤,要找出完整錯誤訊息我們得有工具才行,MSSOAP 提供了一個Trace Utility,

你先將她啟動,並選擇File|New|Formated Trace之後選擇OK 就可以了,接著你還要更改calc.wsdl 的Soap Action 位址將紅色

字的部份以藍色部份取代

<service name='Calc' >

<port name='CalcSoapPort' binding='wsdlns:CalcSoapBinding' >

<soap:address location='http://localhost/MSSoapSamples/Calc/Service/Rpc/AspVbsCpp/Calc.asp' />

<soap:address location='http://localhost:8080/MSSoapSamples/Calc/Service/Rpc/AspVbsCpp/Calc.asp' />

</port>

</service>

接著重新執行DELPHI 程式,你應該可以看到Trace Utility 所欄截到的SOAP Message:

這個錯誤訊息表示我們並未指定SOAP Action或是使用錯誤的namespace,導致發生內部錯誤!

那到底是那一個呢? 兩者都是! 雖然SOAP 規格書上未強制使用SOAPAction,但MSSOAP 需要她,

要知道這些,只須用MSSOAP 呼叫再查訊息就可以了,其實使用MSSOAP 呼叫WebService 很簡單,

你只要照著下面做就可以了:

procedure TForm1.Button2Click(Sender: TObject);

var

Obj:OleVariant;

d:double;

begin

try

Obj:=CreateOleObject('MSSOAP.SoapClient');

Obj.MSSoapInit('http://localhost:8080/MSSOAP/Calc/Service/Rpc/AspVbsVb/Calc.wsdl');

d:=Obj.Add(1,2);

ShowMessage(FloatToStr(d));

finally

Obj:=Unassigned;

end;

end;

因為我們觀察的對象是SOAPAction 參數,這是一個位於Content Header 區的參數,你無法使用Trace Utility 取得,

我們需要的是PocketSoap 公司的TCP Tracer程式! 請到該公司的網站下載後執行(文後有該公司的URL),回到DELPHI

中重新執行程式選擇使用MSSOAP 呼叫,這次應該會傳回結果,切到TCP Tracer 程式中你應該可以看到這樣的畫面:

嗯! 有SOAPAction 定義,那為何HTTPRIO 沒有解出呢?

<service name='Calc' >

<port name='CalcSoapPort' binding='wsdlns:CalcSoapBinding' >

<soap:address location='http://localhost:8080/MSSOAP/Calc/Service/Rpc/AspVbsVb/Calc.asp' />

</port>

</service>

紅色就是問題所在,BizSnap 中有一段程式大概是寫錯了,直接使用了這個值而沒有先做namespace 的處理,

那麼我們只要補上這個值就可以了吧? 嘿! 前面提過BizSnap 目前是很封閉的,所以你找不到地方填這個值,

這必須改原始碼才行,我改完後錯誤訊息變成這個:

None of the matching operations for soapAction http://tempuri.org/action/Calc.Add could

successfully load the incoming request. Potential typemapper problem

.z.z.z.z.z.z.z 唉.....我看不懂! 真的看不懂! 我可是提供了正確的SOAPAction 了耶! 問題在那呢?

你絕對想不到,問題在於參數的namespace 上:

<NS1:A xsi:type="xsd:double">

19

</NS1:A>

<NS1:B xsi:type="xsd:double">

20

</NS1:B>

</NS1:Add>

下面是執行成功的畫面

看來只要把namespace去掉就可以了! 問題是怎麼去掉? 除了改原始碼之外沒別的辦法,那怎麼改? 我不知道!

我想這是MSSOAP 的Bug,因此如果你需要呼叫MSSOAP Server 的話,請你還是使用MSSOAP 較省事。

www.xmethods.com 上有一部份的Service 是使用.NET 撰寫的,正如同先前所提的

DELPHI 呼叫.NET 一樣有問題,MSSOAP 的問題一還是存在,但.NET 並不強制你要填SOAPAction,

所以問題不在這裡,問題在這個地方:

<soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"

.NET預定值是使用document 傳輸樣式,而BizSnap 目前只支援rpc兩者之間的差異頗大! 整個排列方式

都不太一樣了,而WSDL 的差異也是原因之一,不過這都不是大問題,你只要改3 個地方就可以

正常呼叫.NET 了,只是NewsGroup 上DELPHI R&D 已經說明目前他們正在處理,因此你就等等吧!

範例程式中有我呼叫.NET 的原始碼及執行檔,你如果重編譯的話還是會出錯!

因為你沒有修改BizSnap,我附上這個程式的原因只是想讓你知道,DELPHI 呼叫.NET 是絕對可能的,

而且可以傳入或傳回Complex Type。

(以修改後的BizSnap 呼叫.NET WebService)

至於其它開發工具所撰寫的WebService,如Java 等,應該都能正常使用DELPHI 來呼叫,我就不再介紹了。

WebService 應用範圍很廣,你也可以運用WinINet 或是Indy 來將網路的Search Site 包裝起來。

例如Borland 以前展示過的翻譯Service,或是郵政總局的郵遞區號查詢網站。這樣做的好處是我們不需要

接觸這些我們根本完全不熟悉的領域,例如像郵遞區號的查詢程式,5碼郵遞區號的規則我一直都搞不清楚,

正想寫個WebService 來運用。這樣我又省下不少時間了,運氣好的話還可以租給別人用,

呵...當我做夢好了。

使用其它SOAP實作呼叫DELPHI WebService這個嘛! 目前技術上有些困難,大部份的問題都發生在Complex Type 上,目前似乎所有的實作只要遇上Complex Type

就得繳械投降,目前我測試過的有.NET,MSSOAP,MSSOAP 的話只需修改wsdl 資訊就可以呼叫了,但是如果遇上了

Complex Type 的話,可能你得撰寫ISoapTypeMapper 之類的COM 物件來處理,由On-line HELP 上看來,應該是可行。

.NET 就比較麻煩,之前曾提過.NET 實作的SOAP 及 WSDL 規格與BizSnap 有差異,因此我們需要大幅度的更改WSDL及SOAP

才能呼叫。

(PS:MSSOAP 要修改DELPHI 產生的WSDL)

xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap"

xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"

(亦或是一勞永逸的非官方修改)

WSDLIntf.pas

Soapns=Wsdlns +'soap';

Soapns=Wsdlns +'soap/';

Session 管理及例外處理到目前為止! 我們開發了幾個WebService,可是這些應用程式都少了樣功能: 使用者權限管理及Session!

在.NET,Session這個功能是系統支援型的,而使用者也可以經由SOAP Header 支援來解決,

也就是說你不需費太多力氣就可以讓Service 擁有Session及使用者管理功能,DELPHI 呢?

目前BizSnap 並未明白支援,不過這不難,只需要使用一個資料庫來管理使用者及Session 就可以了。

以下我們試著開發這樣的WebService,你會在過程中學習到例外的處理,這對一個WebService 來說是很重要的。

在BizSnap 中,她允許我們訂制自己的例外類別,只需要由ERemotableExecption 類別繼承下來就可以了,

以下就是我們的ELoginError 例外類別:

unit uLoginError;

interface

uses

SysUtils, Classes,InvokeRegistry;

type

ELoginError = class(ERemotableException)

private

FErrorCode:Integer;

protected

public

constructor Create(ErrMsg:WideString;Code:Integer);

published

property ErrorCode:Integer read FErrorCode write FErrorCode;

end;

const

E_LOGGED=1;

E_INVALID_SESSION=2;

E_INVALID_USER=3;

implementation

constructor ELoginError.Create(ErrMsg:WideString;Code:Integer);

begin

Message:=ErrMsg;

FErrorCode:=Code;

end;

initialization

RemTypeRegistry.RegisterXSClass(ELoginError,''http://www.code6421.com/XMLSchema', 'ELoginError','');

finalization

RemTypeRegistry.UnRegisterXSClass(ELoginError);

end.

BizSnap 要求例外類別必需向RemTypeRegistry 註冊,因為這樣才能將資料封裝傳回Client 端。

接著我們要撰寫有關資料庫處理及Session 管理的DataModule:

unit uData;

interface

uses

Windows,SysUtils, Classes, DB, DBClient,SyncObjs,InvokeRegistry,Variants;

type

TDataModule3 = class(TDataModule)

cdsSession: TClientDataSet;

cdsSessionUser: TStringField;

cdsSessionPassword: TStringField;

cdsSessionSessionHandle: TStringField;

cdsSessionExpire_Date: TDateTimeField;

procedure DataModuleCreate(Sender: TObject);

private

AppPath:string;

{ Private declarations }

public

function Login(UserName,Password:WideString):WideString;

function Logout(SessionHandle:WideString):Boolean;

function ValidateSession(SessionHandle:WideString):Boolean;

{ Public declarations }

end;

var

DataModule3: TDataModule3;

CS : TCriticalSection;

implementation

{$R *.dfm}

function TDataModule3.Login(UserName,Password:WideString):WideString;

var

FGUID:TGUID;

begin

try

CS.Enter;

cdsSession.LoadFromFile(AppPath+'\Session.xml');

if cdsSession.Locate('USER;PASSWORD',VarArrayOf([UserName,Password]),[]) then

begin

if cdsSession.FieldByName('SessionHandle').AsString <> '' then

begin

Result:='';

exit;

end;

CreateGUID(FGUID);

cdsSession.Edit;

cdsSession['User']:=UserName;

cdsSession['Password']:=Password;

cdsSession['SessionHandle']:=GUIDToString(FGUID);

cdsSession.Post;

Result:=cdsSession['SessionHandle'];

end

else Result:='ERROR';

finally

cdsSession.SaveToFile(AppPath+'\Session.xml');

cdsSession.Close;

CS.Leave;

end;

end;

function TDataModule3.Logout(SessionHandle:WideString):Boolean;

begin

try

Result:=False;

CS.Enter;

cdsSession.LoadFromFile(AppPath+'\Session.xml');

if cdsSession.Locate('SessionHandle',SessionHandle,[]) then

begin

cdsSession.Edit;

cdsSession['SessionHandle']:='';

cdsSession.Post;

Result:=True;

end;

finally

cdsSession.SaveToFile(AppPath+'\Session.xml');

cdsSession.Close;

CS.Leave;

end;

end;

function TDataModule3.ValidateSession(SessionHandle:WideString):Boolean;

begin

try

Result:=False;

CS.Enter;

cdsSession.LoadFromFile(AppPath+'\Session.xml');

if cdsSession.Locate('SessionHandle',SessionHandle,[]) then

Result:=True;

finally

cdsSession.Close;

CS.Leave;

end;

end;

procedure TDataModule3.DataModuleCreate(Sender: TObject);

var

FN: array[0..255] of char;

begin

GetModuleFileName(hInstance,FN,SizeOf(FN));

AppPath:=ExtractFileDir(FN);

end;

initialization

CS := TCriticalSection.Create;

finalization

CS.Free;

end.

除了同步處理的控制外,這個程式並沒有其它特別之處,你應該能夠輕易理解。

接著我們要建立WebService,這裡我只列出實作部份,完整程式請參考Examples 3。

unit MyDPAuthServiceImpl;

// Created with Thor-Mjollnir Service Module Creator

interface

uses

InvokeRegistry,MyDPAuthServiceIntf,uLoginError;

type

TMyDPAuthService = class(TInvokableClass,IMyDPAuthService)

private

{ Private declarations }

public

//interface implementation

//function SayHello:string; stdcall;

function Login(UserName,Password:WideString):WideString;stdcall;

procedure Logout(SessionHandle:WideString);stdcall;

function SayHello(SessionHandle:WideString):WideString;stdcall;

{ Public declarations }

end;

implementation

uses uData;

function TMyDPAuthService.Login(UserName,Password:WideString):WideString;

var

SessionHandle:WideString;

begin

SessionHandle:=DataModule3.Login(UserName,Password);

if SessionHandle = '' then

raise ELoginError.Create('User already logged in',E_LOGGED)

else if SessionHandle = 'ERROR' then

raise ELoginError.Create('invalid user',E_INVALID_USER);

Result:=SessionHandle;

end;

procedure TMyDPAuthService.Logout(SessionHandle:WideString);

begin

if not DataModule3.Logout(SessionHandle) then

raise ELoginError.Create('invalid session handle!',E_INVALID_SESSION);

end;

function TMyDPAuthService.SayHello(SessionHandle:WideString):WideString;

begin

if not DataModule3.ValidateSession(SessionHandle) then

raise ELoginError.Create('invalid session handle!',E_INVALID_SESSION);

Result:='Hello!';

end;

initialization

InvRegistry.RegisterInvokableClass(TMyDPAuthService);

end.

在WebService 中所產生的例外都會被BizSnap 自動包裝後傳回Client 端,這裡我們產生的

是ELoginError 的自定例外,因此BizSnap 會經由RemTypeRegistry 找到資訊並包裝後傳

回Client 端。

接著建立Client 端應用程式,如之前所提的,你必須將uLoginError 複製到Client 端的目

錄中,下面是Client 程式:

procedure TForm1.Button1Click(Sender: TObject);

var

SessionHandle:WideString;

begin

try

SessionHandle:=(HTTPRIO1 as IMyDPAuthService).Login('code','1234');

except

on E:ELoginError do

begin

if ELoginError(E).ErrorCode = E_LOGGED then

ShowMessage('user already logged in');

if ELoginError(E).ErrorCode = E_INVALID_USER then

ShowMessage('invalid user!');

exit;

end;

end;

try

ShowMessage((HTTPRIO1 as IMyDPAuthService).SayHello(SessionHandle));

finally

(HTTPRIO1 as IMyDPAuthService).Logout(SessionHandle);

end;

end;

執行後你應該得到一個Hello 的訊息,你可以試著用另外的使用者登入,你將會得到錯誤訊息!

這個程式還有很大的改善空間,如Session 過期處理,重複登入等問題! 這些就交給你了。

將Service 分發為ISAPI DLL這並不困難! 你只要開一個新專案,選擇ISAPI 的Server Type 後再將你使用到的Unit 加入

這個Project 中編譯就可以了,有關這一部份你可以在DataSnap 篇找到有關的步驟,

這裡就讓我偷懶一下了 :)

PS:Demos/WebSnap 內許多範例就是這樣做的,你可以參考一下。

PS2:記得打開虛擬目錄的執行權限。

關於程式碼部份:下載位址

http://home.pchome.com.tw/guide/code6421/Wizard.zip

http://home.pchome.com.tw/guide/code6421/Examples.zip

寫完這篇文章後我又修改了小部份的程式碼,所以請你以Examples 內的程式為主!

還有! 別按Get Jpeg(use DynaArray) 按紐,因為我後來換了張圖,大小有61K!

如果你按了的話...嘿! 可以先出去吃飯了。

這些範例程式中多半沒有做完整的錯誤處理,當你實際應用於專案中時,請記得

做好錯誤管理。

(當你發現範例程式有問題時,請先查看你的設定是否正確,再看看範例程式被標為註解的程式碼,

或許你會有意想不到的發現)

關於一些問題及解決方法如果你直接拿範例程式來執行的話,記得先執行Service Server 程式一次!

讓她註冊自己之後你才能正常執行Client 端程式,我的程式需要Indy 9.1 版本,

所以你必需先到Indy 站上Download 最新版本,再將你的Indy 8.0 移除後再安裝新的Indy,

有許多使用者(包括我自己) 在第一次使用WebAppDebuger 時沒有按下Start按紐,

使得WebAppDebuger 沒有啟動HTTP 服務! 所以記得要做! 嘿,我像老太婆般嘮叨了一堆,

就是希望你能體會一下WebService 的好處 :)

PS:當你安裝了MSSOAP 跟她的Samples 後,你要自行到IIS 中建立相關的目錄。

一些除錯工具在我們撰寫WebService Client 端時,常常需要查看網路傳輸的資料,PocketSoap 公司

提供了幾個工具可以讓我們監控網路間傳輸的資料,她提供了兩個工具程式

一個是TCPTracer,另一個是ProxyTracer,你可以到下列的網站下載。

http://www.pocketsoap.com/

關於Wizard 的原始碼我並沒有將原始碼放在壓縮檔中! 主要原因是這個Wizard 還有些問題,

當你儲存她所產生的出來的Unit 時,請使用Menu 內的Save來儲存,不要使用Ctrl+S HotKey 來存。

後記 DELPHI 6 是個相當強的開發工具! 這次的WebSnap 更將Interface 的運用推向另一個高蜂,

在研究WebSnap 時,我時常掉入Jim 所建構的迷宮中,但一旦清楚後,你會發現WebSnap 竟設

計的如此美麗! 比起WebSnap,BizSnap 呈現出另一種美,她所展示的並不是錯蹤複雜的架構,而

是不拖泥帶水的精簡技術與神奇的魔法,兩者我都喜歡!也為Borland 的技術讚嘆,希望你能

在這篇文章中得到你所想要得到的東西,我所寫的不一定是對的,身為工程師! 請保持你的懷疑及

驗證的精神,這會使你得到意想不到的收穫,如果你發現了錯誤也請你通知我,我會儘快修正,

也請你Post 到論壇上,讓其它人得利! 就這樣,希望很快能完成另一篇WebSnap,能再與你們

分享,So...ByeBye!

相关帖子:

李维:軟體服務時代的來臨

看看什么是软件服务时代!大家快来看看!

李维:.net vs delphi 6

delphi6 爆发还是灭亡?

李维:我的回忆和一些有趣的事

看IT风云变幻,宝兰与微软背后的故事,

李维:2001 年軟體界的巨星 - Kylix

看宝兰, 一年之间连续推出kylix1.0 ,interbase6.0, delphi6,jbuilder5 ,c++builder6也不日即出,敬请关注宝兰2001年与微软对绝的杀手锏kylix

李维:Windows 原生開發工具的瑰寶 – Delphi 6

李维问答集之语言选择篇

李维:樂趣無窮,可能無限的新技術-Web Service

我推荐的帖子

陈宽达: 遊戲程式設計初學者常遇之疑問

明修栈道,暗渡陈仓,陈宽达点指开发工具

软件开发中的弊病

这篇文章精彩,引来的评论更精彩!

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
>>返回首页<<
推荐阅读
 
 
频道精选
 
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
© 2005- 王朝网络 版权所有