項目(Delphi開發)需要調用另一個系統的WebService。走了不少彎路,現記錄總結一下經驗。以下是WebService要求:
1、WebService概述
營銷Webservice接口采用Apache Axis(version 1.4)技術實現。客戶端和服務器用SOAP(Simple Object Access Protocol)協議通過HTTP來交互,客戶端根據WSDL描述文檔生成SOAP請求消息發送到服務端,服務端解析收到的SOAP請求,調用Web service,然后再生成相應的SOAP應答送回到客戶端。
2 、認證機制
營銷的所有Webservice服務均需要認證通過(部分需要授權)才能夠被調用。營銷Webservice服務接收到請求后從Soap頭中獲取用戶名和密碼,進行認證,認證通過后再調用具體服務。
作為客戶端,應用程序代碼(使用Axis的客戶端編程模型來編寫的)需要將用戶名和密碼設置到SOAPHeader中。SOAPHeaderElement的namespace約定為Authorization,localPart約定為username 和 password。
根據客戶端程序語言及調用方式不同,設置的方法也不同,下面示例說明客戶端程序語言為java調用方式為動態調用的設置方法:用org.apache.axis.client.Call 的addHeader方法:
call.addHeader(new SOAPHeaderElement("Authorization","username",username));
call.addHeader(new SOAPHeaderElement("Authorization","password",password));
其他的調用方式及其他語言設置方式請查閱Axis相關文檔。
最終傳輸的SOAP報文格式如下:
最終傳輸的SOAP頭信息如下:
<soapenv:Header> <ns1:username soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" soapenv:mustUnderstand="0" xsi:type="soapenc:string" xmlns:ns1="Authorization" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"> username </ns1:username> <ns2:password soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" soapenv:mustUnderstand="0" xsi:type="soapenc:string" xmlns:ns2="Authorization" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"> password </ns2:password> </soapenv:Header>
開始的時候,按照一般調用WebService方法進行:導入wsdl,自動生成WebService調用函數,手工添加一個類繼承TSOAPHeader類,使用HTTPRIO發送SOAP報文。但是使用SOAPUI測試發出的報文,發現SoapHeader信息和WebService要求的格式不一樣。
於是想到,在soap報文發出前,手動將soap報文改成WebService要求的格式,即在HTTPRIO的BeforeExecute事件中修改soap報文:
procedure TForm1.HTTPRIO1BeforeExecute(const MethodName: String; var SOAPRequest: WideString); var head_begin,head_end,head_len: Integer; SOAPData: WideString; old_head: WideString; begin SOAPData := SOAPRequest; //替換SOAP頭 head_begin := Pos('<SOAP-ENV:Header',SOAPData); head_end := Pos('</SOAP-ENV:Header>',SOAPData); head_len := head_end + Length('</SOAP-ENV:Header>') - head_begin; old_head := Copy(SOAPData,head_begin,head_len); SOAPData := StringReplace(SOAPData,old_head,NewSoapHeader,[rfReplaceAll, rfIgnoreCase]); //轉義字符處理 < 改 < SOAPData := StringReplace(SOAPData,'<','<',[rfReplaceAll, rfIgnoreCase]); //轉義字符處理 > 改 > SOAPData := StringReplace(SOAPData,'>','>',[rfReplaceAll, rfIgnoreCase]); SOAPRequest := SOAPData; Memo2.Clear; Memo2.Lines.Add(Utf8ToAnsi(SOAPRequest)); end;
但是,用SoapUI測試,發現這樣修改后發出的報文Header沒有了,只有Body部分。
仔細研究了一下Delphi的Soap相關控件,最終找到以下解決方法使用THTTPReqResp控件直接發送完整的soap報文,相關代碼如下:
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs,IniFiles, DB, ADODB, StdCtrls, InvokeRegistry, Rio, SOAPHTTPClient,GenericServer1, ExtCtrls,ActiveX, SOAPHTTPTrans; const SOAP_DATA = '<?xml version="1.0"?>' + '<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' + ' xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">' + '<SOAP-ENV:Header>' + '<ns1:username SOAP-ENV:actor="http://schemas.xmlsoap.org/soap/actor/next" SOAP-ENV:mustUnderstand="0" xsi:type="soapenc:string" xmlns:ns1="Authorization" ' + 'xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">' + ':WS_USER_NAME' + '</ns1:username>' + '<ns2:password SOAP-ENV:actor="http://schemas.xmlsoap.org/soap/actor/next" SOAP-ENV:mustUnderstand="0" xsi:type="soapenc:string" xmlns:ns2="Authorization" ' + 'xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">' + ':WS_PASSWORD' + '</ns2:password>' + '</SOAP-ENV:Header>' + '<SOAP-ENV:Body SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">' + '<NS2:invoke xmlns:NS2="http://server.webservice.core.epm">' + '<path xsi:type="xsd:string">:WS_PATH</path>' + '<methodName xsi:type="xsd:string">:WS_METHOD_NAME</methodName>' + '<dataXmlStr xsi:type="xsd:string">' + '<![CDATA[' + ':WS_XML_DATA' + ']]>' + '</dataXmlStr>' + '</NS2:invoke>' + '</SOAP-ENV:Body>' + '</SOAP-ENV:Envelope>'; type TForm1 = class(TForm) ADOConnection1: TADOConnection; GroupBox8: TGroupBox; Label21: TLabel; Label22: TLabel; Label23: TLabel; Label24: TLabel; Label25: TLabel; edt_wsdl_url: TEdit; edt_path: TEdit; edt_method: TEdit; edt_user: TEdit; edt_password: TEdit; Button1: TButton; Memo1: TMemo; Label1: TLabel; Label2: TLabel; Memo2: TMemo; Timer_Ping: TTimer; HTTPReqResp1: THTTPReqResp; procedure FormCreate(Sender: TObject);procedure Button1Click(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure Timer_PingTimer(Sender: TObject); private { Private declarations } public { Public declarations } NETWORK_ID, NETWORK_NAME, WSDL_URL, USER_NAME, PASSWORD, METHOD_NAME, PATH: WideString; dataXmlStr: WideString; NewSoapData: WideString; procedure sendData(); end; { 使用線程發送WebService } TPingThread = class(TThread) protected procedure execute; override; end; procedure write_log(str: string);//寫入記錄文件 var Form1: TForm1; { 初始化臨界區CS變量 } PingCS:TRTLCriticalSection; implementation uses util_utf8; {$R *.dfm} procedure write_log(str: string); var F: TextFile; mfile: string; begin try //判斷保存日志文件的目錄是否存在 if not DirectoryExists(ExtractFilePath(ParamStr(0)) + 'log') then MkDir(ExtractFilePath(ParamStr(0)) + 'log'); //按日期及時間設定保存日志的文件名 mfile := ExtractFilePath(ParamStr(0)) + 'log\' + formatdatetime('yyyy-mm-dd', now) + '.txt'; AssignFile(F,mfile); if not FileExists(mfile) then Rewrite(F);//如果文件不存在,則創建一個新的文件,並寫入 Append(F); //追加寫入 Writeln(F,str);//寫入並換行 CloseFile(F); except end; end; //讀txt Procedure ReadTxt(FileName:String); Var F:Textfile; str: String; Begin AssignFile(F, FileName); {將文件名與變量 F 關聯} Reset(F); {打開並讀取文件 F } while not Eof(F) do begin Readln(F, str); Form1.Memo2.Lines.Add(str); end; ShowMessage(str); Closefile(F); {關閉文件 F} End; procedure TForm1.FormCreate(Sender: TObject); begin InitializeCriticalSection(PingCS); end; //發送 procedure TForm1.Button1Click(Sender: TObject); begin Timer_Ping.Enabled := True; end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin { 清除線程CS變量 } DeleteCriticalSection(PingCS); end; procedure TForm1.Timer_PingTimer(Sender: TObject); begin { 創建線程, 向LED屏發送數據 } TPingThread.Create(False); end; procedure TForm1.sendData; var svc: GenericServer; tmpstr: string; strSend: TStringStream; begin WSDL_URL := Trim(edt_wsdl_url.Text); USER_NAME := Trim(edt_user.Text); PASSWORD := Trim(edt_password.Text); METHOD_NAME := Trim(edt_method.Text); PATH := Trim(edt_path.Text); dataXmlStr := Trim(Memo1.Text); //獲取自定義soap報文 NewSoapData := SOAP_DATA; NewSoapData := StringReplace(NewSoapData,':WS_USER_NAME',USER_NAME,[rfReplaceAll, rfIgnoreCase]); NewSoapData := StringReplace(NewSoapData,':WS_PASSWORD',PASSWORD,[rfReplaceAll, rfIgnoreCase]); NewSoapData := StringReplace(NewSoapData,':WS_PATH',PATH,[rfReplaceAll, rfIgnoreCase]); NewSoapData := StringReplace(NewSoapData,':WS_METHOD_NAME',METHOD_NAME,[rfReplaceAll, rfIgnoreCase]); NewSoapData := StringReplace(NewSoapData,':WS_XML_DATA',dataXmlStr,[rfReplaceAll, rfIgnoreCase]); Memo2.Text := NewSoapData; //使用HTTPReqResp1控件進行發送soap報文,不適用HTTPRIO控件(發出的報文xml會被轉義,也不需要導入wsdl了) CoInitialize(nil); //線程中使用必須加上CoInitialize(nil)和CoUninitilize(), 單元中要uses activex。 //將string轉換成stream strSend := TStringStream.Create(NewSoapData); try try //加上try。。except,不要彈出爆粗提示 HTTPReqResp1.URL := WSDL_URL; HTTPReqResp1.Send(strSend); except on e:Exception do begin write_log(FormatDateTime('yyyy-mm-dd hh:nn:ss',Now) + ' 調用WebService時發生異常,錯誤原因:'+E.Message); end; end; finally strSend.Free; couninitialize; end end; { TPingThread } procedure TPingThread.execute; begin Form1.Timer_Ping.Enabled :=false; FreeOnTerminate := True; {線程臨界區代碼塊開始} EnterCriticalSection(PingCS); try form1.sendData; {線程臨界區代碼塊結束} except on e:Exception do begin write_log(FormatDateTime('yyyy-mm-dd hh:nn:ss',Now) + ' TPingThread.execute:'+E.Message); end; end; LeaveCriticalSection(PingCS); end; end.
測試效果,可以發現,發出的報文和接收的報文是一致的:
源碼下載:http://files.cnblogs.com/files/tc310/WebServiceDemo.rar