| Home | White Papers |  Message Board |  Search |  Products |  Purchase |  News |  |
By Rick Strahl
Last Update: 10/24/2000
See Also:
XML Messaging in Distributed Applications (previous parts of this series)
wwXML Conversion classes (required for samples)
wwIPStuff classes (contains wwHTTPData class)
Imagine that you need some specific information in your application such as a shipping rate calculator. You now go to a service search engine and look up availability for the type of service you need in a standard manner over the Web. Now imagine that you can get at this information easily and simply plug the info directly into your application. Sound too good to be true? Believe it or not the technological pieces to make this type of functionality possible are available today. Web Services promise to make this type of functionality a reality by bringing the same interlinked mechanisms that have made the Web so popular for Web browsing to application development by sharing data over the Web using standard formats. Web Services have become the new industry buzzword. Microsoft is talking about Web Services as the second life of the Internet that will tie together applications the same way that the Web Browser and URL based links have tied together HTML based Web pages. The Web at your Service is a new mantra rising. In this article Rick discusses what SOAP and Web Services are and then delves into creating a sample Web Service and calling and integrating it into an application.
Web Services promise to bring the same kind of interlinked functionality that hyperlinks brought to the browser experience to application development.
The move to distributed application development is a natural evolution for the Web. It gets back to the roots of how data is used in applications in general. With HTML the focus has always been on presentation with data bound directly into the presentation. On the other hand the focus in distributed applications is on totally separating the display and the data delivery. XML has become the preferred way to provide the data to client applications. To date XML has rapidly gained ground as a messaging format that serves as an intermediary between the data and the consuming client application – XML is typically converted from some native data format like a database table or an object and then used as the transfer mechanism. The client then has the choice of consuming that data directly through the XMLDOM or by converting it back into a native format such as a table or object that maps the XML. In the latter scenario XML is primarily used as a persistence format to transfer a state from the server to client or vice versa. In order to do this, all you need is an XML parser and a mechanism for pushing the XML over the wire via HTTP.
SOAP and Web Services don't change this model in any way. Instead SOAP 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.
SOAP is a contender in the field for remoting technologies like DCOM and CORBA. Unlike those technologies SOAP has the advantage of easily running over HTTP and avoiding the complex configuration and security administration issues that surround DCOM for example. Because SOAP uses HTTP it can take advantage of the HTTP features like encryption and authentication as well as having the benefit of going through the Enterprise firewall.
It's important to understand though that SOAP is not meant to replace DCOM or CORBA in high performance environments. SOAP has a lot of overhead associated with it compared to these lower level binary formats. HTTP is slower than native TCP/IP for example and the XML encoding required by the SOAP messages cause SOAP to be up to a 1000 times slower than a similar implemtation using DCOM (it depends on what type of SOAP server you use of course – if you build an ISAPI all C++ listener you probably get better numbers.
It's important to remember that you should not think of SOAP as a Remote COM implementation. Although the MS SOAP implementation focuses on exposing COM objects as Web Services, that is just one way you can create a Web Service. SOAP is open and does not specify how a Web Service must be implemented, so you can implement a SOAP Web Service with a function in script code in ASP, a visual FoxPro class, JSP class or a COM object run through the SDL Wizard.
What makes SOAP so nice is that it's relatively simple protocol that's easy to implement and work with and building a custom SOAP server that can handle requests is trivial.
What's a Web Service?
Over the last few issues I've discussed using XML in a distributed messaging architecture. The basic concept in these scenarios is using the HTTP protocol to communicate between client and server and passing data in XML format over the wire. This mechanism works really well for custom applications that follow a standard format and where both sides know and agree on the message format (the XML structure, the URL to call etc.)
Web Services are based on the same concepts and technologies, but extend this mechanism by providing a standard interface as to how the server side Web Service is called. This interface comes in the form of the Simple Object Access Protocol or SOAP. SOAP's mission in life is to provide a standard, XML based interface to making remote procedure calls. The purpose of SOAP is to standardize the way that data is requested and remote code is executed by providing standard parameter information (input) and return value (output) that returned in an XML document that follow the SOAP protocol specification. A server SOAP implementation is required to handle incoming SOAP requests. This implementation can be implemented with any Web Backend. MS SOAP uses ASP, Web Connection uses a special process class to handle SOAP requests to a specific Web scriptmap. Keep in mind that the server implementation is independent of the SOAP spec. As long as it can read the incoming SOAP Request packet and return a valid SOAP Response packet it's fulfilling the SOAP contract.
In simplistic terms the idea behind a Web Service and SOAP is nothing more than making a remote function call over the Internet. The client passes a parameter, the server returns a result value. It's not quite that easy yet, but we can look forward to full development tool integration of SOAP and Web Services that literally will blur the lines between where code is executed, whether it's in you own compiled application or from a service sitting on the other side of the globe with a 12 hour time difference.
Why bother since XML is already here?
If you're already building XML based applications you may be asking yourself just about now what is so different about using SOAP compared to using standard XML messages? There are many reasons why SOAP has advantages over raw XML as a protocol, but here are the two most important ones:
- Simplicity
The idea behind SOAP is that tools exist to facilitate the process of creating SOAP messages easily. I'll show examples later in this article, but the basic idea is that client code should resemble the simplicity of making a simple function call – on another server. On the server side it means you can implement a Web Service by simply implementing a function or class method. The SOAP Web Service server manager and the SOAP client handle everything that happens in between – the XML packaging, unpackaging and transferring of the request over the wire via HTTP. In many cases this removes the requirement for needing to create XML manually at all. On the client it means writing only a few lines of code to access that remote Web Service code and not having to know how to package up XML and make an HTTP request.
- Standardization
If you publish functionality via SOAP you open up your architecture to other clients that can call your code easily using any of the above mentioned SOAP clients. SOAP clients exist today for just about all major operating system platforms and development languages including Java, Perl and for Windows developers COM objects and custom implementations in various languages. Furthermore, you can publish an interface definition that describes the service and what functionality it provides which makes it possible to provide the same type of IntelliSense type functionality that is common for Windows development tools.
SOAP is still evolving as we speak, but tools that let you take advantage of it today are available. The most visible tool is Microsoft's SOAP Toolkit. For more info on the MS implemententation see my article Using SOAP for remote object access with Microsoft's SOAP Toolkit. In this article I'll discuss building a Web Service with West Wind Web Connection's Web Service implementation, which is much easier and more flexible in a number of ways especially when demonstrating functionality.
West Wind Web Connection and Web Services
West Wind Web Connection includes direct support for SOAP based Web Services with both a wwSOAP client implementation and a wwWebService process class that can handle incoming SOAP requests mapped to a specific .wwSOAP scriptmap extension in IIS. To demonstrate how all of this works I'll implement a stock lookup service as a SOAP application. We then also look at another example in a VFP fat client application that is a bit more sophisticated in a Time and Billing sample application. All of the samples are available as part of the free wwSOAP classes which you can download from http://www.west-wind.com/wwsoap.asp.
These examples use two sources for quotes from MSNBC and NASDAQ. The MSNBC site retrieves less data and is faster but retrieves data in an INI like format, while the NASDAQ site retrieves more data and provides this data in XML format. The links for the sites are (with Microsoft (MSFT) as an example stock):
MSNBC
http://www.msnbc.com/tools/newsalert/nagetstk.asp?s=MSFT
NASDAQ
http://quotes.nasdaq.com/quote.dll?page=xml&mode=stock&symbol=MSFT
Retrieving Stock Quotes from the Web
Let's talk a little about the application I'll build as an example. Now I want you to understand up front that there are other ways to do this example application, especially because some of this data that I'll be using for quotes is directly available over the Web. However, this is meant as an example to show how to present data and as such shows a variety of ways that you can consume data from Web Services.
This application is an HTML based Web Server application that allows you to add stocks to a personal portfolio. The user enters a symbol name and a qty and the app then recalculates the portfolio based on the current stock prices. The portfolio form also contains a simple stock quote retriever that lets you pull a single quote and display the stock price and other info. The stock data is retrieved from a SOAP Web Service that I'll describe in detail. The Web Service is responsible for retrieving the stock quotes in a variety of ways. The Web Service retrieves the actual stock information from the NASDAQ and MSNBC Web sites (I used both for a little variety <s>). So, we're dealing with three Web sites here: The Web site that runs the portfolio application, the Web site that hosts the SOAP Web Service and the stock server at NASDAQ or MSNBC. The portfolio application can be considered an aggregation engine that consolidates data from the local data store (the portfolio) and the Web Service.
Getting a Stock Quote from MSNBC
Let's start with retrieving only a single stock price based on a symbol to demonstrate the basics of how Web Services work. Here's the code that retrieves a stock quote from the MSNBC Web site using the wwHTTP class (included as part of wwSOAP):
************************************************************************
* SOAPService :: GetStockQuoteSimple
****************************************
*** Function: Returns a stock quote by symbol
*** Assume: Must be connected to Web and msn.moneycentral.com
*** Pass: lcSymbol -
*** Return: Last stock price in string format
************************************************************************
FUNCTION GetStockQuoteSimple(lcSymbol as String) as String
lcSymbol = UPPER(lcSymbol)
oHTTP=CREATEOBJECT("wwHTTP")
lcHTML=oHTTP.HTTPGet("http://www.msnbc.com/tools/newsalert/nagetstk.asp?s=" + lcSymbol)
RETURN EXTRACT(lcHTML,"N=",CHR(13),CHR(10))
ENDFUNC
To get the latest stock price for Microsoft for example you'd simply do:
lcQuote = GetStockQuoteSimple("MSFT")
What you'll see is a string result that returns something like: 65.888. A pretty depressing number when considered that it's off from Microsoft's 120 high earlier this year, huh?
Easy enough. So, now lets set this up as a Web Service that can be generically called from other applications. To do this with Web Connection you can use the Create Web Service option of the Web Connection Management Console. To start the console type: DO CONSOLE and you'll get the wizard shown in Figure 1.
Figure 1 – Creating a new Web Service involves specifying of a new file to create the Web Service class into. The template contains the class plus a small loader function.
In the dialog you need to specify a file location for the Web Service. This file should be placed into a Web virtual directory, because the Web Server will actually access this file and route it to the Web Connection Web Service handler via the .wwSOAP script map extension. The actual template generated looks like this:
*** DO NOT REMOVE - CALL WRAPPER
PARAMETERS lcMethod, lcParmString, lvResult
PRIVATE _loServer
_loServer = CREATEOBJECT("StockService")
lvResult = Eval("_loServer."+ lcMethod + "(" + lcParmString+ ")" )
RETURN lvResult
ENDFUNC
*** DO NOT REMOVE - CALL WRAPPER
*************************************************************
DEFINE CLASS StockService AS Session OLEPUBLIC
*************************************************************
*** Remove after testing
FUNCTION Test(lcEcho)
IF EMPTY(lcEcho)
RETURN "Test Result"
ENDIF
RETURN lcEcho
ENDDEFINE
*EOC test
The Web Service consists of a small loader that's called by the Web Connection Web Service engine, which in turn loads the class and calls the method in question. Note that the class is created with the OLEPUBLIC keyword. The Web Connection Web Service classes don't load this class as a COM object however – the OLEPUBLIC is only used to create an SDL file for consumption by MS SOAP as well as providing the ability to compile your object into COM object that can be called from an MS SOAP hosted Web Service. More on that later.
The Wizard also generates a test method into the class as well so you can check out the Web Service easily. Let's remove that test class and instead add our GetStockQuoteSimple() function into the class as a method like this:
*************************************************************
DEFINE CLASS StockService AS Session OLEPUBLIC
*************************************************************
************************************************************************
* SOAPService :: GetStockQuoteSimple
****************************************
*** Function: Returns a stock quote by symbol
*** Assume: Must be connected to Web and msn.moneycentral.com
*** Pass: lcSymbol -
*** Return: Last stock price in string format
************************************************************************
FUNCTION GetStockQuoteSimple(lcSymbol)
lcSymbol = UPPER(lcSymbol)
oHTTP=CREATEOBJECT("wwHTTP")
lcHTML=oHTTP.HTTPGet("http://www.msnbc.com/tools/newsalert/nagetstk.asp?s=" + lcSymbol)
RETURN EXTRACT(lcHTML,"N=",CHR(13),CHR(10))
ENDFUNC
* SOAPService :: GetStockQuoteSimple
ENDDEFINE
*EOC test
That's all there's to it! Voila, you've created your first Web Service!
Before you go on make sure the Web Connection Server is running. Note that Web Services in Web Connection are dynamically compiled both under VFP 6 and 7. This means that you can make changes to the Web Service while the Web Connection server is running and without stopping the Web service.
Calling the Web Service
Let's make sure that the service actually works. Since this service exists on the Internet on the West Wind Web Site I'll use that as an example. wwSOAP includes a SOAP Method Tester form that you can use to quickly check out a Web Service. Figure 2 shows how to set up the form to call our newly created Web Service:
Figure 2 – The SOAP method tester allows you to quickly test Web Services without writing any code.You can also view the SOAP Request and Response to see what the SOAP messages look like.
Fill in the URL to the Web Service in this case the I used the service running on the West Wind Web Site. If you're testing on your local machine use localhost and the virtual directory that you copied the Web Service to instead. Enter the method name and each of the parameters required, in this case on the symbol and set the type. Note what happens if you play with the types. Try passing an integer instead of a string. You get an error, which is the error message thrown by the FoxPro Web Service code: Variable 'LCHTML' is not found. wwSOAP embeds type information into the SOAP messages and the Web Service on the other end interprets those types. So when you passed an integer from the client it gets passed to the server application which fails in the GetStockQuoteSimple() method call because we didn't check for a non-string input parameter.
If you click on the SOAP Request button you can look at the request packet traveling over the wire:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<GetStockQuoteSimple>
<lcSymbol xsi:type="xsd:string">MSFT</lcSymbol>
</GetStockQuoteSimple>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
This is a basic SOAP request envelope that consists of the following sections:
- The SOAP Envelope
The envelop contains the SOAP header that define the SOAP and related namespaces such as xsi and xsd used for data types. The envelop can also contain an optional <headers> section that lets you provide custom headers to a request. Custom SOAP server implantations can read info from the header and use it internally.
- The SOAP Body
This is the meat of the SOAP packet. It contains the method call interface. In the request this consists of the name of the method to call on the server and all of the parameters passed. The parameters are each described as sub-elements of the method and with wwSOAP are typed with XML types.
The SOAP response is very similar:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<GetStockQuoteSimpleResponse>
<return xsi:type="xsd:string">65.188</return>
</GetStockQuoteSimpleResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The layout of the SOAP Response is identical to the Request with the exception that here the return value is returned in the body section.
The SOAP Method Tester uses the wwSOAP client behind the scenes to perform the SOAP method calls. Let's see what we have to do to call the service with code:
* Function GetStockQuoteSimpleSOAP
LPARAMETERS lcSymbol
IF EMPTY(lcSYMBOL)
lcSymbol = "MSFT"
ENDIF
*** Load wwSOAP dependencies
DO wwSOAP
oSOAP = CREATEOBJECT("wwSOAP")
oSOAP.cServerUrl = "http://www.west-wind.com/wconnect/soap/stockservice.wwsoap"
oSOAP.AddParameter("lcSymbol","MSFT")
lcPrice = oSOAP.CallMethod("GetStockQuoteSimple")
IF oSOAP.lError
MESSAGEBOX(oSOAP.cErrorMsg)
RETURN
ENDIF
? lcSymbol + ":", lcPrice
? "Result Type: " + VARTYPE(lcPrice)
Pretty easy, eh? You basically specify the URL to call, add parameters then call the method. CallMethod() goes out and does all of the hard work of packaging up your parameters into the SOAP XML packet, sending the request over the wire and decoding the result into a simple return value. wwSOAP also includes low level methods that let you do these steps independently and properties like cRequestXML and cResponseXML let you view what goes over the wire. The wwSOAP documentation help file includes examples on how to do this.
Notice when you run this that the Web Service returns a character value. That's not really a big problem – you can just run VAL() on the returned quote, but it would be much nicer if the server actually did this for us. Since wwSOAP and most SOAP applications understand embedded types in the SOAP packet we can make a simple change in our Web Service code to return a numeric value. Just change the last line in the GetStockQuoteSimple method of the Web Service to:
RETURN VAL( EXTRACT(lcHTML,"N=",CHR(13),CHR(10)) )
Save, then simply re-run the SOAP test code above and notice that now the value returned is a numeric value! Notice how simple this process is: You didn't have to recompile or stop the server. You simply change the code and the new value is immediately returned to you.
Expanding the Stock Web Service with Objects
Returning a single value like a stock is nice, but it's not all that useful. If you wanted to retrieve information about a stock you'd probably want to know a few things about the stock. Like the high and low and change for the day, the actual time of the quote and a few other things. You could set up multiple Web Service methods that return each of these values and then make several SOAP calls to these methods, but this would be vastly inefficient since it requires multiple round trips to the server. SOAP calls have a fair amount of overhead in the packaging and unpackaging of parameters and return values, and passing that data over the Web and through the Web Server. There is a base amount of overhead that occurs for every hit in addition to the time it takes to run the actual request, so bundling up data into a single SOAP package is a big advantage.
For this reason SOAP supports embedding of object parameters and return values. Since VFP can return objects from method calls and wwSOAP supports objects you can create a method that returns an object as a result value. There's a catch though as we'll see in a minute: The client side must provide an object instance to receive the SOAP result.
To demonstrate lets add a new method to our Web Service called GetStockQuote which will return an object that contains several properties retrieved from a stock quote retrieved from the NASDAQ Web site. The NASDAQ site provides quotes in XML format and this method retrieves values from this XML packet:
************************************************************************
* StockService :: GetStockQuote
****************************************
*** Function: Returns a Stock Quote
*** Assume: Pulls data from msn.moneycentral.com
*** Pass: lcSymbol - MSFT, IBM etc.
*** Return: Object
************************************************************************
FUNCTION GetStockQuote(lcSymbol as String) as Object
lcSymbol = UPPER(lcSymbol)
oHTTP = CREATEOBJECT("wwHTTP")
lcHTML=oHTTP.HTTPGet("http://quotes.nasdaq.com/quote.dll?page=xml&mode=stock&symbol=" +;
lcSymbol)
loQuote = CREATEOBJECT("Relation")
loQuote.AddProperty("cSymbol",lcSymbol)
loQuote.AddProperty("cCompany",EXTRACT(lcHTML,"<issue-name>","</issue-name>"))
loQuote.AddProperty("nNetChange",;
VAL(Extract(lcHTML,"<net-change-price>","</net-change-price>")))
loQuote.AddProperty("nLast",;
VAL(EXTRACT(lcHTML,"<last-sale-price>","</last-sale-price>")))
loQuote.AddProperty("nOpen",;
VAL(Extract(lcHTML,"<previous-close-price>","</previous-close-price>")))
loQuote.AddProperty("nHigh",;
VAL(Extract(lcHTML,"<todays-high-price>","</todays-high-price>")))
loQuote.AddProperty("nLow",;
VAL(Extract(lcHTML,"<todays-low-price>","</todays-low-price>")))
loQuote.AddProperty("nPERatio",;
VAL(Extract(lcHTML,"<current-pe-ratio>","</current-pe-ratio>")))
lcOldDate = SET("DATE")
SET DATE TO YMD
lcDate=Extract(lcHTML,"<trade-datetime>","</trade-datetime>")
loQUote.AddProperty("tUpdated",;
CTOT( SUBSTR(lcDate,1,4)+"/" + SUBSTR(lcDate,5,2) + "/" +;
SUBSTR(lcDate,7,2) + SUBSTR(lcDate,9) ))
SET DATE TO &lcOldDate
RETURN loQuote
This code retrieves the XML based stock quote from NASDAQ and then parses several of the XML properties into object properties for easier access and returns the newly created object over SOAP.
If you now call this method with the SOAP Method Tester you can use:
Url: http://www.west-wind.com/wconnect/soap/stockservice.wwsoap
Method: GetStockQuote
Parameter: lcSymbol - MSFT - string
The result that is returned is an XML fragment that looks like this:
<return xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xsi:type="record">
<ccompany>Microsoft Corporation</ccompany>
<csymbol>MSFT</csymbol>
<nhigh>66.13</nhigh>
<nlast>65.19</nlast>
<nlow>61.13</nlow>
<nnetchange>3.31</nnetchange>
<nopen>61.88</nopen>
<nperatio>38.12</nperatio>
<tupdated>2000-10-20T00:00:00</tupdated>
</return>
The SOAP Method tester simply displays this XML fragment, but when you use code to call this method you can actually retrieve the object directly.
Here's the client code to do this:
* Function GetStockQuoteSOAP
LPARAMETERS lcSymbol
IF EMPTY(lcSYMBOL)
lcSymbol = "MSFT"
ENDIF
*** Load wwSOAP dependencies
DO wwSOAP
oSOAP = CREATEOBJECT("wwSOAP")
oSOAP.cServerUrl = "http://www.west-wind.com/wconnect/soap/stockservice.wwsoap"
oSOAP.AddParameter("lcSymbol","MSFT")
*** Create object to be filled with result
loQuote = CREATEOBJECT("cStockQuote")
lcPrice = oSOAP.CallMethod("GetStockQuote",,loQuote)
IF oSOAP.lError
MESSAGEBOX(oSOAP.cErrorMsg)
RETURN