|
Using Microsoft's SOAP Toolkit for remote object access广告 Using Microsoft's SOAP Toolkit for remote
object access by Rick Strahl West Wind Technologies Microsoft's SoapToolKit: Examples for this paper: http://www.west-wind.com/presentations/soap/soapsamples.zip Last update: 02/11/2001 Web Services and XML Messaging are gaining ever more attention and at the core of this movement is the Simple Object Access Protocol (SOAP), which promises to bring a standard interface to calling server side code. In this article Rick discusses what SOAP is how it works and how to use Microsoft's SOAP Development Kit to call COM components and script code over the Web. SOAP's mission in life is to provide a standard, XML based interface to making remote procedure calls. Distributed application development has really taken off over the last year or so, and XML has been at the center of making it possible to efficiently share data and content between client and server applications. I've written a number of papers (see resources at the end of the article) on the basic concepts of XML messaging in custom application environments. In these scenarios the XML is based on custom XML structures that map application specific functionality. Typically the data maps to table based data or to business objects with the XML providing the message format to share the data over the wire. While this works really well for custom applications, it doesn't use a standard message format and it leaves the XML management up to the application. SOAP promises to help in both respects by using a standard XML wrapper (called an envelope) that transports the content of the message (typically parameters to a function call and result values). Special system tools (such as Microsoft's ROPE.DLL client) can then be used abstract the process of making the remote call. When it's all said and done SOAP's goal is to make it as easy to call a remote function as it is making a function call in your local application. As we'll see shortly it's not quite this easy, but this is definitely a step in the right direction. What is SOAP? SOAP doesn't change this model in any way. Instead it standardizes it for the purpose of making remote calls on object methods or functions (such as script page calls) more natural. In custom XML applications the application – both client and server – have to know what the message format is beforehand which results in some amount of coupling between the client and the server. By providing a standard mechanism for representing the procedure call interface and a mechanism for querying what functionality is available and what the signature of each call is, SOAP can abstract away the explicit XML conversions that occur in custom XML implementations. To make this process truly seamless some services or tools must be in place that can provide the SOAP XML packaging and unpackaging and perform the wire transfer operations. The current flock of tools is not there yet, although as we'll see in a minute it only takes a few lines of code to make a remote procedure call in this fashion. The implementation of this 'middle-ware' tool is up to the vendor and in fact major implementations from Microsoft, IBM and DevelopMentor (one of the original partners in the SOAP spec group) differ significantly in their implementations. However, the actual content that goes over the wire – an XML document – is interchangeable between these implementations. The idea is that the customized aspects of each implementation facilitates the actual calling process and results in fully spec compliant SOAP packages being sent over the wire. The rest is simply services that are used internally by the implementation. SOAP is not a replacement for DCOM or
CORBA There is a significant difference between what COM provides and what SOAP does. SOAP is only a protocol that mandates how a method call transfers over the wire. More importantly, SOAP caters only to single function or method invocations, which means you can't maintain state to an object between calls. With SOAP, the client makes single method or function calls, one per HTTP request. The SOAP model follows standard Web and distributed application fare: Stateless transactions with disconnected data used to provide the client with the data needed to do its work. Get the data from the Web server in a method call, use the data offline locally, the send the updated data back to the server in another stateless remote call. This is very different from DCOM or CORBA where a client creates a persistent connection to the object to perform multiple property accesses and method calls. DCOM and CORBA both support stateful remote connections – SOAP typically does not (although it's conceivable this could be implemented with server side state management of an application, something that's usually to be avoided in distributed applications). The SOAP concept is simple: If a single wire format is used to call a function or object method on the server, the server doesn't care what type of client the call is being made from. So you could be calling with the Microsoft SOAP Toolkit from a Windows 2000 machine, or you could be calling from an IBM Java SOAP implementation running on a Linux box. The server can run ASP script code, a COM object, or run a Java Servlet on Linux – as long as the data traveling over the wire matches the SOAP spec they all can talk to each other. The server doesn't care and services the request as needed and simply returns a SOAP compliant result package. Implementations vary however, with Microsoft's implementation using an intermediary SDL file that acts as a sort of remote type library for the Web service. This concept is not supported by other implementations such as the one from IBM. But because the Microsoft packager still outputs a plain SOAP package in the end it can call an IBM implementation on the server or talk to an IBM client. What all this means is that SOAP is vendor independent and the spec has been submitted for standard status. Several large players besides Microsoft have added support including IBM, but some big players like Sun, Oracle and Netscape (surprise, surprise, eh?) are not on board as of yet. Third party and free tools are available for those platforms however. Microsoft's SOAP Toolkit When Microsoft released the SOAP toolkit I was pleasantly surprised at what I found: The toolkit comes with heaps of documentation, full source code (yes, full source for the ROPE client DLL, the ISAPI listener and the ASP listener – each of these has a ton of useful pieces of code for developers), a Wizard that generates Service Descriptions (SDL) from COM objects and several useful and easy to follow examples. Getting started involves downloading and installing the SOAP toolkit. I highly suggest you read through the first section of the help file that explains how the toolkit is structured – I'll review the main points here, but the documentation is very useful and easy to follow and has a number of useful tips and insights. The trouble shooting section is also good – it helped me figure out a couple of assumptions I made when I started. The Microsoft toolkit is a custom SOAP implementation. What this means is that the SOAP messages passing over the wire are SOAP protocol compliant, but that there are additional tools and operations that occur to verify method call signatures. In particular the toolkit uses a Service Description file in XML Schema format that describes what methods the Web Services exposes and how those methods should be called. You can think of the SDL (Service Description Language) file as a server side type library that both the client and the server use to validate the method call parameters and result values. Figure 1 – The Microsoft Toolkit relies on a Service Description to provide type information both to the client and the server applications accessing the 'Web Service'. Based on the type information SOAP messages are created and passed over the wire. The SDL is implementation specific, but is not required as long as the server receives a valid SOAP request from the client and the client receives a valid SOAP response from the server. Here's how it works: The process is started by creating a Web Service on the server. To help with this process an SDL Wizard is provided which can create a Web Service template from an existing COM object. Let's say we have a COM DLL SoapDemo with a SoapServer class and a HelloWorld method to match Figure 1. The Wizard generates the SDL file as well as a wrapper ASP page that calls the COM component. The ASP page references a library ASP page that knows how to process the SOAP request and pass the call on to the method in the generated ASP file. The actual method does nothing more than a CreateObject followed by a call to the method with the parameters and returning that value back to the listener. The client application initiates a Web Service request by downloading the SDL file. The SDL file is then passed to the Proxy object, which uses it to create the SOAP package that is put onto the wire and transferred to the server. The ROPE.DLL provides both the SOAP encoding/decoding functionality (SOAPPackager methods) as well as the ability to make HTTP calls to the server (WireTransfer component). The SOAP request is then sent to the server for processing. Note that the actual SOAP request is the key piece here – the server won't care how the SOAP package was created as long as it is in the right format. So, you can manually create the appropriate XML via code, or you could have a non-Windows client create that message. The server doesn't care and happily processes the message. The code on the server fires the ASP file the Wizard generated which has the name of the class – SoapServer.asp. The ASP page includes Listener.asp which is a library file that contains the code that uses ROPE.DLL to process the incoming SOAP message, route the request to the function in SoapServer.asp, and then package up the result value into a response SOAP package. The server also uses the SDL file to validate the parameters the client passed as well as the result value that the listener function returns. The call to the function is made, The actual function merely instantiates the COM object, calls the method and returns the value to the listener code. The listener, checks the return value and if it doesn't match the type in the SDL file, creates an error response package. If it does match a Result SOAP package is sent back to the client. The client now picks up the SOAP message as part of the Proxy's operation to make the SOAP call. Behind the scenes the Proxy object is making an HTTP POST request against the server using the WireTransfer component posting the SOAP request and retrieving the SOAP response in a single HTTP operation. Once the result comes back the result is unbundled in a value that's properly typed and returned to the client application. The SOAP toolkit supports both high level and low-level methods for calling server side code, with the high-level methods taking about 10 lines, while the low level takes about 25. It's relatively easy to get up and running as we'll see in a minute. The MS SOAP toolkit doesn't support all of the supported SOAP datatypes. In fact you're limited to the following types as parameters and return values:
Notably you can't return objects, which makes sense in this context. (Note: .Net's Web Services can return persisted objects). Note that date values are also not explicitly supported – you can pass and return dates only as strings. There are few additional restrictions on the these types: They must be XML compliant to the point that they can live in a regular element tag. If strings contain other XML (especially XML containing CDATA content) or extended ANSI characters the SOAP call will fail. You'll get 'Bad SOAP return' errors. I'll talk about some workarounds for this later. Installation of the Toolkit Register ROPE.DLL Once you've configured the server pieces you can run the sample VB program in the samples/client directory. The sample calls server side code in a variety of different ways which is useful to see the different ways you can handle the SOAP messaging. If you don't like looking at VB code – or even if you do – read on for VFP examples. Creating a Web Service from a COM
object #DEFINE CRLF CHR(13)+CHR(10) ************************************************************* DEFINE CLASS SoapServer AS Session OLEPUBLIC ************************************************************* ************************************************************************ * SoapServer :: HelloWorld **************************************** FUNCTION helloworld(lcName as String) AS String RETURN "Hello World from " + GETENV("COMPUTERNAME") + ", " + lcName + "! Time is: " + TIME() ************************************************************************ * SoapServer :: Evaluate **************************************** PROCEDURE evaluate( lcCommand as String) AS Variant RETURN EVALUATE(lcCommand) ENDPROC ************************************************************************ * SoapServer :: GetXML **************************************** *** Function: Demonstrates returning XML results *** Pass: lcName - Name to wrap in XML *** Return: ************************************************************************ PROCEDURE getxml(lcName as String) as String lcXML = [<?xml version="1.0"?>] + CRLF +; [<docroot>] + CRLF+; [ <name>] + lcName + [</name>] + CRLF + ; [</docroot>] + CRLF RETURN lcXML ************************************************************************ * SoapServer :: GetXML ************************************************************************ PROCEDURE getxmlcdata(lcName as String) as String lcXML = [<?xml version="1.0"?>] + CRLF +; [<docroot>] + CRLF+; [ <name><![CDATA[] + lcName + CHR(13) + CHR(10) + lcName + "]]</name>" + CRLF + ; [</docroot>] + CRLF RETURN lcXML PROCEDURE getobject(lnValue as Long, lcString as String, ldDate as Datetime) ; as soapdemo.retObject loCustom = CREATEOBJECT("soapdemo.retObject") loCustom.nValue = lnValue loCustom.cString = lcString loCustom.dDate = ldDate RETURN loCustom ENDDEFINE DEFINE CLASS retObject as Relation OLEPUBLIC nValue = 0 cString = "" dDate = { : } ENDDEFINE Figure 2 – The SDL Wizard asks for a COM component to import The first step is to pick a COM componet that the Wizard will work with. The Wizard will generate one SDL file for each class you select from the next dialog: Figure 2 – The SDL Wizard lets you pick which methods to expose via the Web Service. Each COM class/interface you select is generated into a separate SDL and ASP file. Notice the red methods in Figure 2. These methods mean that the parameters and return types were not set up with specific types but use variants instead. SOAP will allow you to use variant parameters but they will always be returned as Text rather than by their variant types. For example, the above evaluate method takes a string input parameter but a variant output parameter so we can return any result that our COM object can EVAL to (a string, a date or numeric value etc.). If a numeric value is returned SOAP will return the number but it will be returned as a string. The same goes for dates and logical values (1 or 0 for True and False respectively). In the final two steps you need to tell the Wizard where your Web service will be located: Figure 3 – You have to specify the HTTP location where the Web service will be accessed from. This Web virtual directory must already exist – the Wizard will not create it. Figure 4 – This dialog requests the physical location for the file that the Wizard will generate. Again this directory must exist and should be the physical directory of the Web location chosen in the previous step. It's very important that the directory that you're copying the generated files to exists already. I would suggest you create the directory and make sure it's also set up as a virtual Web directory before you actually run the Wizard. The Web name is not checked by the Wizard and is used only as a template value in the SDL file's Address element that points at the location for the processing ASP page. Note that you can create both an ASP and ISAPI listener. The ISAPI listener is actually less flexible than the ASP listener, although the ISAPI version can be more efficient. The ASP version has the advantage that it can be edited and add to the functionality of calling the COM object. In fact, the ASP methods don't even need to call the COM object at all, but could perform the processing in script code. Once the Wizard completes you end up with two files in the specified directory: SoapServer.asp Your custom SOAP Listener SoapServer.xml SDL file You also need to copy: listener.asp Library file for SOAP message cracking from the samples/server directory of your toolkit installation. Alternately you can put this file into a central location and change the reference to it in the SoapServer.asp file, which uses server side includes to include it. And this takes care of the server side. The Client
Side To review we want to access our SoapServer Web service and call the HelloWorld method. The method signature looks like this: Helloworld(lcName as String) as String Note I'm using VFP 7 syntax which allows you to cast parameters and return values into specific types. VFP 7 doesn't really do anything different here than VPF 6 did, except that these names get written into the type library. When the SDL Wizard picked up the COM object it was then able to add the type information into the SDL file it created. Let's take a look at the SDL file which contains the Web Service type information for all of the methods included in the SoapServer class: <?xml version='1.0' ?> <!-- Generated 8/9/2000 2:55:09 AM by Microsoft SOAP Toolkit Wizard, Version 205.0.3 --> <serviceDescription name='soapdemo' xmlns='urn:schemas-xmlsoap-org:sdl.2000-01-25' xmlns:dt='http://www.w3.org/1999/XMLSchema' xmlns:SoapServer='SoapServer' > <import namespace='SoapServer' location='#SoapServer'/> <soap xmlns='urn:schemas-xmlsoap-org:soap-sdl-2000-01-25'> <interface name='SoapServer'> <requestResponse name='helloworld'> <request ref='SoapServer:helloworld'/> <response ref='SoapServer:helloworldResponse'/> <parameterorder>lcName</parameterorder> </requestResponse> <requestResponse name='evaluate'> <request ref='SoapServer:evaluate'/> <response ref='SoapServer:evaluateResponse'/> <parameterorder>lcCommand</parameterorder> </requestResponse> <requestResponse name='getxml'> <request ref='SoapServer:getxml'/> <response ref='SoapServer:getxmlResponse'/> <parameterorder>lcName</parameterorder> </requestResponse> <requestResponse name='getxmlcdata'> <request ref='SoapServer:getxmlcdata'/> <response ref='SoapServer:getxmlcdataResponse'/> <parameterorder>lcName</parameterorder> </requestResponse> <requestResponse name='getobject'> <request ref='SoapServer:getobject'/> <response ref='SoapServer:getobjectResponse'/> <parameterorder>lnValue lcString ldDate</parameterorder> </requestResponse> </interface> <service> <addresses> <address uri='http://localhost/soap/SoapServer.asp'/> </addresses> <implements name='SoapServer'/> </service> </soap> <SoapServer:schema id='SoapServer' targetNamespace='SoapServer' xmlns='http://www.w3.org/1999/XMLSchema'> <element name='helloworld'> <type> <element name='lcName' type='dt:string'/> </type> </element> <element name='helloworldResponse'> <type> <element name='return' type='dt:string'/> </type> </element> <element name='evaluate'> <type> <element name='lcCommand' type='dt:string'/> </type> </element> <element name='evaluateResponse'> <type> <element name='return' type='dt:string'/> <element name='lcCommand' type='dt:string'/> </type> </element> <element name='getxml'> <type> <element name='lcName' type='dt:string'/> </type> </element> <element name='getxmlResponse'> <type> <element name='return' type='dt:string'/> </type> </element> <element name='getxmlcdata'> <type> <element name='lcName' type='dt:string'/> </type> </element> <element name='getxmlcdataResponse'> <type> <element name='return' type='dt:string'/> </type> </element> <element name='getobject'> <type> <element name='lnValue' type='dt:integer'/> <element name='lcString' type='dt:string'/> <element name='ldDate' type='dt:string'/> </type> </element> <element name='getobjectResponse'> <type> <element name='return' type='dt:string'/> </type> </element> </SoapServer:schema> </serviceDescription> This file is an XML schema that defines the class, its member methods and each of the parameters and return values as well as the Uri link that services this Web Service (the <address> tag in the soap:service fragment). In typical Schema fashion, you have a declaration section (<interface>) which points to the <soapserver> section for the implementation details such as parameters and return values. The schema is a bit verbose, but quite readable if you look at it closely. The client needs to get this SDL file downloaded first and then can assign it to the proxy object to make the SOAP call over the wire. So the simple syntax looks as follows: LOCAL oProxy as Rope.Proxy, oWire as Rope.WireTransfer *** Download the SDL file oWire=CREATEOBJECT("Rope.WireTransfer") lcXML = oWire.GetPageByURI("http://localhost/soap/soapserver.xml") *** Assign it to the Proxy so it can get type info oProxy = CREATEOBJECT("Rope.Proxy") ? oProxy.LoadServicesDescription(2, lcXML) && .T./1 *** Call the 'dynamic' method lvResult = oProxy.helloworld("Rick") ? VARTYPE(lvResult) ? lvResult If all goes well with this code you'll get the result back as: Hello World
from WESTWINDSERVER, Rick! Time is: 11:30:12 Visual FoxPro cannot call methods that are not lowercase in the SDL file when using dynamic Proxy methods. Dynamic Proxy Methods create custom methods on the fly and if these methods are not lowercase VFP calls against them will fail. ROPE also includes lower level methods that make indirect method calls, which allows to side-step this issue. Both examples are shown in the article's text. When you build classes that are to be
used as Web classes make sure you build the method names in lowercase in the PRG
file (VCX methods automatically are lowercased but be aware that you can't
attach type information to VCX methods even in VFP 7) to facilitate calling the
methods from Visual FoxPro. XML is case sensitive so matching the SDL case is
vital to call success. The call to helloworld actually performs the entire SOAP transfer of creating the SOAP message that goes on the wire, and then unpackaging the returned SOAP package into the return value. If something goes wrong (couldn't connect, or the method call failed) the call raises a COM error (typically 'Bad Soap Return') which you can trap in your code with a typical COM error handler (or you can use an Eval() and capture the result type). This scheme of accessing server side methods is nice and easy but it has several problems. First you're stuck with the case sensitivity issues mentioned earlier – there's no control over how the methods are accessed. Additionally, if there's a problem in the call you can't get any information of what goes wrong. Although the SOAP packages contain error information, the above scheme doesn't return it to you, so you're left in the dark about any failures. To work around this you can take a lower level approach which requires a bit more code to deal with effectively. With this approach you can see what's happening behind the scenes as the messages are created and what sent onto the wire: #INCLUDE Rope.h && provided with these samples * create objects LOCAL oSoap AS Rope.SoapPackager LOCAL oWire AS Rope.WireTransfer *** Must set URL for listener and SDL lcListener = "http://localhost/soap/soapserver.asp" lcSDLUri = "http://localhost/soap/soapserver.xml" lcMethod = "helloworld" oSoap = CREATEOBJECT("ROPE.SOAPPackager") oWire = CREATEOBJECT("ROPE.WireTransfer") lcResponse = oWire.GetPageByURI(lcSDLUri) *** load ServicesDescription file IF oSoap.LoadServicesDescription(icString, lcResponse) # 1 MESSAGEBOX("Error loading SDL file") ENDIF *** retreive the SOAP request structure for method (XML fragment) sRequestStruct = oSoap.GetMethodStruct(lcMethod, icINPUT) *** Set up the method to call oSoap.SetPayloadData(icREQUEST,"", lcMethod, sRequestStruct) *** Add parameters oSoap.SetParameter(icRequest,"lcName","Rick") *** Return the SOAP XML packet that goes on the wire sRequestPayload = oSoap.GetPayload(icREQUEST) MESSAGEBOX( sRequestPayload, 64, "SOAP XML Payload - Client to Server") *** Now put the the packet on the wire and POST to Server oWire.AddStdSOAPHeaders(lcListener, lcMethod, LEN(sRequestPayload)) *** Get the SOAP response (same wire call) sResponsePayload = oWire.PostDataToURI(lcListener, sRequestPayload) MESSAGEBOX( sResponsePayload,64, "SOAP XML Payload - Server To Client" ) *** Assign the result XML oSoap.SetPayload(icRESPONSE, sResponsePayload) *** Parse the SOAP response back into a result value sTemp = oSoap.GetParameter(icRESPONSE, "return") MESSAGEBOX( sTemp, "Result" ) In this code you can see each of the steps along the way from creating the SOAP packages and then reading the response. If we actually look at the SOAP requests captured here is what you'd see: SOAP Request <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP:Body> <helloworld> <lcName>Rick</lcName> </helloworld> </SOAP:Body> </SOAP:Envelope> SOAP Response <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP:Body> <helloworldResponse> <return>Hello World from RASNOTE, Rick! Time is: 11:49:21</return> </helloworldResponse> </SOAP:Body> </SOAP:Envelope> Very straightforward. Remember that as far as the server's expectations of the client are concerned it only needs to receive this SOAP request. As far as the client is concerned all it expects is this SOAP response. In fact if you wanted to be clever and create a simple demo using only a few lines of VFP you could do something like this (VFP 7 code): *** Create the SOAP string TEXT TO lcSOAPRequest NOSHOW <?xml version="1.0"?> <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP:Body> <helloworld> <lcName>Rick</lcName> </helloworld> </SOAP:Body> </SOAP:Envelope> ENDTEXT *** West Wind HTTP components loIP = CREATEOBJECT("wwIPStuff") *** Use HTTP to POST the request and get SOAP Result loIP.nHTTPPostMode = 4 && XML loIP.AddPostKey("",SUBSTR(lcSoapRequest,3)) && Post the data lcSOAPResponse = loIP.HTTPGet("http://localhost/soap/soapserver.asp") *** Display the XML ShowXML(lcSoapResponse) *** Extract the result value (string) lcResult = EXTRACT(lcSoapResponse,"<return>","</return>") Using a custom HTTP Client Why would you use a custom HTTP client especially since ROPE
already provides one? The problem is that the ROPE.WireTransfer component is
limited. It doesn't support secure connections (SSL) or authentication (Basic
Auth) which means that it's very difficult to protect your application. Proxy
support is also fairly limited which can cause problems with firewalls and
inhouse proxy servers. By using a WinInet based client (such as wwIPStuff) you
can implement these features easily (proxy support is mostly automatic, SSL and
Authentication are supported directly) and you can actually control more tightly
how data is passed between the client and server including improved connection
error handling. On the Server The SDL wizard created the following ASP file for our SoapServer COM class: <%@ Language=VBScript %> <% Option Explicit Response.Expires = 0 '-------------------------------------------- ' SOAP ASP Interface file SoapServer.asp ' Generated 8/9/2000 2:55:13 AM ' By Microsoft SOAP Toolkit Wizard, Version 205.0.3 '-------------------------------------------- Const SOAP_SDLURI = "http://localhost/soap/SoapServer.xml" 'URI of service description file %> <!--#include file="listener.asp"--> <% '_________________________________________________________________________________ Public Function helloworld (ByVal lcName) Dim objhelloworld Set objhelloworld = Server.CreateObject("soapdemo.SoapServer") helloworld = objhelloworld.helloworld(lcName) 'Insert additional code here Set objhelloworld = NOTHING End Function '_________________________________________________________________________________ Public Function evaluate (ByRef lcCommand) Dim objevaluate Set objevaluate = Server.CreateObject("soapdemo.SoapServer") evaluate = objevaluate.evaluate(lcCommand) 'Insert additional code here Set objevaluate = NOTHING End Function … additional methods left out here '_________________________________________________________________________________ %> This page is straightforward: Every method in the class gets a function here with the parameters passed in and a result value returned. Notice that listener.asp is imported into this document and that the mainline code starts in listener.asp. This code basically performs task very similar to the tasks in the manual SOAP message packaging described above (code example 2), but specific to the server side. The way it works is that it decodes the SOAP message getting the method name and parameters to pass. The Function in the above document is then called and the return value retrieved. The result is packaged up back into a SOAP package. Adding custom functions to the SDL You can also add custom functions to this ASP document, but if you do you need to add the functions you create to the SDL document manually. For example, to add a HelloWorldAspOnly method to the SDL file you'd add the following: <requestResponse name='helloworldasponly'> <request ref='SoapServer:helloworldasponly'/> <response ref='SoapServer:helloworldasponlyResponse'/> <parameterorder>lcName</parameterorder> </requestResponse> into the Interface section and <element name='helloworldasponly'> <type> <element name='lcName' type='dt:string'/> </type> </element> <element name='helloworldasponlyResponse'> <type> <element name='return' type='dt:string'/> </type> </element> into the SoapServer section. You can now add a new function to the ASP page: Public Function helloworldasponly (ByVal lcName) helloworldasponly = "Hello from ASP, " & lcName & ". Time is: " & now End Function Voila – you've added a new method to your Web service and without even recompiling any code. Think of this as an easy way to do remote scripting. SOAP and Variants LPARAMETER lcEvalCommand LOCAL oProxy as Rope.Proxy, oWire as Rope.WireTransfer oWire=CREATEOBJECT("Rope.WireTransfer") lcXML = oWire.GetPageByURI("http://localhost/soap/soapserver.xml") oProxy = CREATEOBJECT("Rope.Proxy") ? oProxy.LoadServicesDescription(2, lcXML) && .T./1 lvResult = oProxy.evaluate (lcEvalCommand) ? VARTYPE(lvResult) ? lvResult Then run the following in the command window: soapproxy("sys(0)") soapproxy("DateTime()") soapproxy("Month(DateTime())") What you'll see is that SOAP will return the correct values, but it returns them all as strings rather than their proper types. You'd have to do the type conversions yourself. Note that this is a very powerful (but potentially dangerous) concept – you can create generic methods that execute code on the server. You could for example add a method called Execute to the COM server that runs a block of VFP code and returns a result by using the COMPILE command to actually execute that block of code (or you can use VFP7's new ExecScript() function for this). However, without access restrictions this becomes a very dangerous proposition – if you can run code generically you can generically delete your hard disks file too (ExecScript("erase \winnt\system32\*.*") anyone?)… Soap and XML results The current release of the SOAP toolkit does not directly support XML parameters and results. The sample server includes an XML result method getXML, which passes a single lcName parameter and returns that parameter wrapped into an XML block: <?xml version="1.0"?> <docroot> <name>Rick</name> </docroot> To call this method against the Web Serivce use: lvResult = oProxy.getxml("lcName") You'll get a 'Bad SOAP return' error. This 'generic' error message is about the only error you get from the dynamic method interface, and it's not terribly useful. If you run this request through the SoapManual.prg (change the method name and parameter in the PRG) file you'll see that the SOAP Response actually returns the XML string to the client. However, because the XML is not delimited in a special way the returned XML is actually an invalid XML document, resulting in the SOAP call to fail: <?xml version="1.0"?> <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP:Body> <getxmlResponse> <return><?xml version="1.0"?> <docroot> <name>Rick</name> </docroot></return> </getxmlResponse> </SOAP:Body> </SOAP:Envelope> The above is invalid XML, so the parser that ROPE uses fails to do anything with the XML document. There's a crude workaround you can use for this: Add a <![CDATA[ ]]> tag around the output generated in
your code I like the latter approach the least of the two evils: Public Function getxml (ByVal lcName) Dim objgetxml Set objgetxml = Server.CreateObject("soapdemo.SoapServer") getxml = "<![CDATA[" + objgetxml.getxml(lcName) + "]]>" 'Insert additional code here Set objgetxml = NOTHING End Function Now the XML response is valid: <?xml version="1.0"?> <SOAP:Envelope xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <SOAP:Body> <getxmlResponse> <return><![CDATA[<?xml version="1.0"?> <docroot> <name>Rick</name> </docroot> ]]></return> </getxmlResponse> </SOAP:Body> </SOAP:Envelope> Unfortunately, this only solves part of the problem. If your XML result includes a CDATA section of its own, the above won't work still resulting in an invalid XML document. Due to XML's limitations on what can be contained inside of a CDATA section you can't embed complex XML inside of the data. The problem is more farreaching than this as well – if you return a result string that contains extended characters you may also run into trouble because of the way that ROPE packages the XML manually (not using the parser). For example, try this (in SoapProxy.prg): lvResult = oProxy.helloworld("R?|ick") You'll get an error that nothing was posted. If you try this with the SoapManual.prg you'll find that the data is not getting posted to the server and the result times out after 10 seconds (ROPE's default timeout). SOAP Problems Parameter/Return type support limitations Whether these issues are show stoppers for you depends on your application and implementation. As showed you before, you can actually sidestep many of these issues by build a custom SOAP client and server using Wininet to address the Wire Protocol issues and using custom code that uses the XMLDOM to create messages to ensure proper message types. The only insurmountable issue then remains to be passing <![DATA[]] as part of XML messages around. Note, that because all source code is provided you can even fix up the ROPE client directly instead of rebuilding everything from scratch. However, building a ROPE client in Visual FoxPro code is surprisingly easy to do and may well be worth the effort in the control it gives you. You can also build basic ROPE-like functionality using scripting and the XMLDOM in Internet Explorer. Check our Web site for updates that will include VFP and Jscript SOAP clients that can interact with the Microsoft SOAP toolkit and custom VFP SOAP servers created with any VFP capable tool (ASP with COM, Web Connection, FoxISAPI etc.). Summary The concept of Web Services is a powerful one, and if you look at the model you can see that it has lots of potential. The SDL file seems like a perfect start of a cataloging tool that allows you find out what services are available on a given Web site. Robots in the future can go out and look for service descriptions and build search engines based on exposed Web Services that are available for example. So next time you need an XML based stock quote you can look at a service directory to get find this service and plug it into your application. The possibilities are endless. But we're not there yet. The tools are still immature as evidenced by the return type problems and lack of industrial strength HTTP support, which is crucial for mission critical operations. Many applications don't need these features and for those that do custom implementations allow extension to fix the problems without deviating from the SOAP standard. It's a distributed world and it's getting more connected all the time – SOAP is a step in the right direction to make it easier to get at the information available. Take a shower, and rub it in… Resources Microsoft's Soap ToolKit: West Wind SOAP Manager classes: Soap Spec: http://msdn.microsoft.com/xml/general/soaptemplate.asp MS SOAP Newsgroup: news:microsoft.public.msdn.soaptoolkit XML Messaging Article: http://www.west-wind.com/presentations/xmlmessaging/xmlmessaging.htm West Wind Web
Connection: http://www.west-wind.com/webconnection.asp West Wind Internet Protocols (wwIPStuff): HTTP services (as well as SMTP, FTP and more) for Visual
FoxPro West Wind XML Converter (convert
VFP/SQL data and object to and from XML): http://www.west-wind.com/wwxml.asp Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows 2000 and Visual Studio. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro, West Wind HTML Help Builder and co-author of Visual WebBuilder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to FoxPro magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/. 如果您希望与本文章的作者或其所在机构,进一步交流,请联系:姜小姐 jill.jiang@amt.com.cn | 021-51096826-112 | 在线联系 |
CIO职场,强者生存?在2008年,我们将继续看到CIO向商业运营方向发展。与此同时,我们也会看到商业管理人员将与技术管理人员一起竞争CIO岗位。 IT领导者的就职机会虽有不少,但其难度将会大幅提高。2…… 防震减灾,IT当关今天,任何的防震救灾体系,都离不开IT技术。地震观测台是数字化的,震害防御需要对以往的地震信息进行数据分析,应急救援要需要现代多样化的通讯技术。如果说,在许多行业,信息技术还只是一…… |
|
|