XML Q&A with Darshan Singh

December 2003
In this article, perfectxml.com managing editor, Darshan Singh, responds to questions asked by PerfectXML visitors. In this part:
Using WinInet to call a Web service
I am working on a MFC/C++ application, and need to call a Web service. I would like to avoid adding any dependency or use any other API (MSXML or SOAP Toolkit), and continue with my application's minimum requirements (which includes Internet Explorer 5.5 or above), and still write the Web service client code. In other words, can you please show me an example of using WinInet to call a Web service?
Answer:
Click here to download a sample console application that uses MFC WinInet classes to call a Web service.

MFC provides wrapper classes around WinInet API. These classes simplify the task of writing HTTP/FTP client applications. Following are the eight steps required to send a HTTP request using MFC WinInet classes:
  1. Create an instance of CInternetSession class. This begins an HTTP session.

  2. Call CInternetSession::GetHttpConnection to get an instance of CHttpConnection. Pass the server and HTTP port to this method, and it establishes a connection to an HTTP server.

  3. Open an HTTP request using CHttpConnection::OpenRequest. Pass the rest of the URL (except server name), the HTTP method (GET/POST/...) to this method, and it returns a CHttpFile object.

  4. Optionally, call CHttpFile::AddRequestHeaders to supply any request headers.

  5. Call CHttpFile::SendRequest to actually send the request and get the response back.

  6. Use CHttpFile::QueryInfoStatusCode to find out if the HTTP request succeeded.

  7. On success, use pHttpFile->Read to read the response bytes.

  8. Finally, call CHttpFile::Close and CHttpConnection::Close.
Here is a Web service client code that uses MFC WinInet classes to call Weather - Temperature Web service (written using Apache SOAP) on XMethods.com. This Web service, given a zip code (US only), returns the current temperature.
...
...
#include <afxinet.h>
...
...

static const TCHAR* g_lpszSOAPRequest =    
_T("<soap:Envelope "
    "xmlns:n='urn:xmethods-Temperature' "
    "xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' "
    "xmlns:soapenc='http://schemas.xmlsoap.org/soap/encoding/' "
    "xmlns:xs='http://www.w3.org/2001/XMLSchema' "
    "xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'> "
    "<soap:Body soap:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'> "
    "  <n:getTemp> "
    "     <zipcode xsi:type='xs:string'>98007</zipcode> "
    "  </n:getTemp> "
   "</soap:Body> "
"</soap:Envelope>");

#define CHUNK_SIZE      2048
   
void CallWebService()
{
   try
   {
      //   1. Instantiate CInternetSession
      CInternetSession httpSession(_T("Sample Web Service Client"), 
                           1,
                           INTERNET_OPEN_TYPE_PRECONFIG,
                           NULL,
                           NULL,
                           INTERNET_FLAG_DONT_CACHE);
      
      //   2. Get CHttpConnection (Server URL and Port required)
      CHttpConnection* pHttpConnection = 
                              httpSession.GetHttpConnection(_T("services.xmethods.net"), 
                                 INTERNET_FLAG_NO_AUTO_REDIRECT,
                                 80, NULL, NULL);

      //   3. Open HTTP Request (pass method type [get/post/..] and URL path (except server name))
      CHttpFile* pHttpFile             = 
                              pHttpConnection->OpenRequest
                              (_T("POST"), 
                                 _T("soap/servlet/rpcrouter"), 
                                 NULL, 1, NULL, NULL, 
                                 INTERNET_FLAG_KEEP_CONNECTION |
                                 INTERNET_FLAG_EXISTING_CONNECT |
                                 INTERNET_FLAG_DONT_CACHE |
                                 INTERNET_FLAG_RELOAD);

      //   4. Add HTTP Request Headers
      CString strSOAPReq(g_lpszSOAPRequest);
      DWORD dwRewLen = strSOAPReq.GetLength();
      CString strHeaders;
      strHeaders.Format(_T("Content-Type: text/xml; charset=utf-8\nContent-Length:%d"), 
         dwRewLen);
      pHttpFile->AddRequestHeaders(strHeaders);

      
      //   5. Send the request
      pHttpFile->SendRequest(NULL, 0, (LPVOID)(LPCTSTR)strSOAPReq, dwRewLen);

      //   6. Check the return HTTP Status Code
      DWORD dwStatucCode = HTTP_STATUS_OK;

      pHttpFile->QueryInfoStatusCode(dwStatucCode);

      if(dwStatucCode == HTTP_STATUS_OK)
      {
         CString strResponse;
         TCHAR szBuf[CHUNK_SIZE] = {0};
         UINT nBytesRead;

         //   7. Read the response text
         do
         {
            nBytesRead = pHttpFile->Read((void*) szBuf, CHUNK_SIZE);
            strResponse += szBuf;
            if(nBytesRead < CHUNK_SIZE)
               break;
         }while(nBytesRead == CHUNK_SIZE);

         AfxMessageBox(strResponse);
         //TODO: Process the response
      }
      else
      {
         //TODO: Error handling
      }

      //   8. Close the stream/connection
      if(pHttpFile)
      {
         pHttpFile->Close();
         delete pHttpFile;
         pHttpFile = NULL;
      }

      if(pHttpConnection)
      {
         pHttpConnection->Close();
         delete pHttpConnection;
         pHttpConnection = NULL;
      }

   }
   catch(CInternetException* exp)
   {
      TCHAR lpszErrorMsg[MAX_PATH+2];
      exp->GetErrorMessage(lpszErrorMsg, MAX_PATH);
      AfxMessageBox(lpszErrorMsg);
   }
}
...
...
Click here to download the above sample console application.

Related Article: MSXML on a clean machine



DOM and limiting the size of XML document
I am using MSXML DOM to load and process an XML document (posted to my C++ ISAPI DLL code). I want to make sure that the load succeeds only if the XML document is less than or equal to some pre-specified size. Is it possible to achieve this using MSXML?
Answer:
Click here to download a sample console application associates with this answer.

The good news is that the recent MSXML 3.0 SP4 and MSXML 4.0 SP2 includes a new property called MaxXMLSize that you can set to make sure that DOM does not load the document if it exceeds the specified size. Note that this size is not the file size, but it refers to the size of document as loaded in the memory - and that the XML encoding and entities (if any) have effect on the document size loaded in the memory.
#include "stdafx.h"
#include <atlbase.h>

#import <msxml4.dll> named_guids
using namespace MSXML2;

#define      MAX_XML_SIZE_KB      1

int main(int argc, char* argv[])
{

   CoInitialize(NULL);
   HRESULT hr = S_OK;
   {
      CComPtr<IXMLDOMDocument2> spXMLDOMDoc;
      hr = spXMLDOMDoc.CoCreateInstance(CLSID_DOMDocument40);

      spXMLDOMDoc->async = false;
      spXMLDOMDoc->resolveExternals = false;
      spXMLDOMDoc->validateOnParse = false;
      
      CComVariant cvMaxSize(MAX_XML_SIZE_KB);
      spXMLDOMDoc->setProperty(_T("MaxXMLSize"), cvMaxSize);


      if (spXMLDOMDoc->load(_T("c:\\1.xml")))
      {
         printf(_T("Load successful!"));
      }
      else
      {
         wprintf(spXMLDOMDoc->parseError->reason);
      }
   }
   
   CoUninitialize();

   return 0;
}
The above code uses MSXML 4.0 SP2 and sets the MaxXMLSize to 1 (KB). If the specified XML document (c:\1.xml in this case) exceeds the specified size (1KB), the load operation would fail/abort, else if the document is well-formed, the load will succeed.

Click here to download a sample console application associates with this answer.


Binary data from Visual Basic 6.0 to ASP.NET Web service
In my Visual Basic 6.0 client application, I would like to make a call to an ASP.NET Web service method, and pass it some binary data. Can you please show some sample code that illustrates this?
Answer:
Click here to download a ZIP file that contains sample ASP.NET Web service and two Visual Basic 6.0 client applications (one uses MSXML XMLHTTP to send binary data to the Web service, and the other uses Microsoft SOAP Toolkit 3.0).

From the Visual Basic 6.0 client code, you can use MSXML XMLHTTP or the Microsoft SOAP Toolkit to invoke a Web service method. Before we look at the Visual Basic client code, let's first study the ASP.NET Web service code.

This sample ASP.NET Web service has only one method called, SaveImage. This Web method accepts two parameters the filename and the binary data. All it does is saves the input binary data into the disk file with the specified filename.

ASP.NET Web method
...
...
      [WebMethod]
      public string SaveImage(string fileName, byte[] imageData)
      {
         try
         {
            string fileNameWithPath = Path.Combine(@"c:\temp\empImages", fileName);
            
            FileStream fileStream = new FileStream(fileNameWithPath, 
               FileMode.Create, FileAccess.Write);
               
            fileStream.Write(imageData, 0, imageData.Length);
            
            fileStream.Close();
         }
         catch(Exception exp)
         {
            return exp.ToString();
         }
         finally
         {
         }

         return "Success";
      }
...
...
The above Web method code creates a file under the c:\temp\empImages folder and writes the input byte array data into this file and closes the stream. Make sure correct permissions are set for this code to execute (for instance, if anonymous access is enabled on this Web site/virtual folder, make sure the anonymous account has write access to this directory).

Note that even though the Web method is called SaveImage, it's not like you can pass only image files with this sample; this sample should work with any kind of file (text/binary such as PDFs, Doc files, etc.).

Here is how you would call this Web service method from a Visual Basic 6.0 application using Microsoft SOAP toolkit 3.0:
    Dim objSOAPClient As New MSSOAPLib30.SoapClient30
    objSOAPClient.MSSoapInit "http://localhost/EmpImages/EmpImages.asmx?wsdl", "EmpImages"
    
    Dim backSlashPos As Integer
    Dim fileNameNoPath As String
    backSlashPos = InStrRev(txtFileName.Text, "\")
    If backSlashPos > 0 Then
        fileNameNoPath = Mid(txtFileName.Text, backSlashPos + 1)
    Else
        fileNameNoPath = txtFileName.Text
    End If
    
    Dim btArr() As Byte
    Open txtFileName.Text For Binary Access Read As #1
    ReDim btArr(LOF(1))
    Get #1, , btArr()
    Close #1
    
    MsgBox objSOAPClient.SaveImage(fileNameNoPath, btArr)
Remember to add reference (Project | References) to Microsoft SOAP Toolkit type library. The above Visual Basic code creates an instances of SoapClient30 class, accepts the file name (using the text box control on the form), extracts the file name (excludes path), reads the file into a byte array and finally calls the SaveImage Web service method. In this case, the SOAP Toolkit converts byte array (binary data) into text using Base64 encoding, and then sends that as the second parameter. On the Web method side, the ASP.NET automagically converts the Base64 encoded text into binary data or byte [].

SOAP Toolkit converts byte array to Base64 encoded text

The above screenshot shows the MSSoapT (the SOAP trace tool that comes with Microsoft SOAP Toolkit), which captures the Web service method call. Note that the imageData parameter is a Base64 encoded text string.

Let's say you do not want to add dependency on SOAP toolkit (that is, avoid installing it on the client machines), you can use MSXML (version 3.0 ships with Internet Explorer 6.0) to call a Web service and pass binary data. The following Visual Basic 6.0 code uses MSXML 3.0 to first convert the binary data into Base64 and then pass it to a Web service using XMLHTTP.

Using MSXML 3.0 XMLHTTP
Dim soapReq As String
Dim objSOAPXMLDoc As New MSXML2.DOMDocument30
Dim objXMLHTTP As New MSXML2.XMLHTTP30

Dim btArr() As Byte

Dim backSlashPos As Integer
Dim fileNameNoPath As String

soapReq = "<?xml version='1.0' encoding='utf-8'?> " & _
   "<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' " & _
    " xmlns:xsd='http://www.w3.org/2001/XMLSchema' " & _
    " xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'> " & _
   "  <soap:Body> " & _
   " <SaveImage xmlns='http://samples.perfectxml.com/BinaryData'> " & _
   "   <fileName></fileName> " & _
   "   <imageData></imageData> " & _
   " </SaveImage> " & _
   "  </soap:Body> " & _
   "</soap:Envelope> "

backSlashPos = InStrRev(txtFileName.Text, "\")
If backSlashPos > 0 Then
 fileNameNoPath = Mid(txtFileName.Text, backSlashPos + 1)
Else
 fileNameNoPath = txtFileName.Text
End If

objSOAPXMLDoc.loadXML soapReq

objSOAPXMLDoc.setProperty "SelectionNamespaces", _
 "xmlns:pxml='http://samples.perfectxml.com/BinaryData'"

objSOAPXMLDoc.selectSingleNode("//pxml:fileName").nodeTypedValue = _
 fileNameNoPath

objSOAPXMLDoc.selectSingleNode("//pxml:imageData").dataType = _
 "bin.base64"

Open txtFileName.Text For Binary Access Read As #1
ReDim btArr(LOF(1))
Get #1, , btArr()
Close #1

objSOAPXMLDoc.selectSingleNode("//pxml:imageData").nodeTypedValue = btArr
MsgBox objSOAPXMLDoc.xml

objXMLHTTP.open "POST", "http://localhost/EmpImages/EmpImages.asmx", False

objXMLHTTP.setRequestHeader "Content-Type", "text/xml; charset=utf-8"

objXMLHTTP.setRequestHeader "SOAPAction", _
     "http://samples.perfectxml.com/BinaryData/SaveImage"

objXMLHTTP.setRequestHeader "Content-Length", Len(objSOAPXMLDoc.xml)

objXMLHTTP.send objSOAPXMLDoc.xml

MsgBox objXMLHTTP.Status & ": " & objXMLHTTP.statusText
MsgBox objXMLHTTP.responseText

Set objXMLHTTP = Nothing
Set objSOAPXMLDoc = Nothing
Remember to add reference to MSXML 3.0 type library. The above Visual Basic 6.0 code defines a string variable and initializes it to the SOAP request envelope. It then extracts the file name (from the file name with full path as specified for the text box on the form). Next, it reads the file content into a byte array, sets the imageData node dataType to bin.base64. Now, when byte array is assigned to this node, MSXML automagically converts it into base64 encoded string. The code then uses MSXML XMLHTTP to POST the SOAP request envelope XML to the Web service.

Click here to download a ZIP file that contains sample ASP.NET Web service and two Visual Basic 6.0 client applications (one uses MSXML XMLHTTP to send binary data to the Web service, and the other uses Microsoft SOAP Toolkit 3.0).

Related Links: