Skip to content

10. Web Services

10.1. Introduction

In the previous chapter, we presented several TCP/IP client-server applications. Since clients and the server exchange lines of text, they can be written in any language. The client simply needs to know the communication protocol expected by the server. Web services are TCP/IP server applications with the following characteristics:

  • They are hosted by web servers, and the client-server communication protocol is therefore HTTP (HyperText Transfer Protocol), a protocol running over TCP/IP.
  • The Web service has a standard communication protocol regardless of the service provided. A Web service offers various services S1, S2, .., Sn. Each of them expects parameters provided by the client and returns a result to the client. For each service, the client needs to know:
    • the exact name of the service Si
    • the list of parameters to be provided and their types
    • the type of result returned by the service

Once these elements are known, the client-server interaction follows the same format regardless of the web service being queried. Client code is thus standardized.

  • For security reasons related to attacks originating from the Internet, many organizations maintain private networks and open only certain ports on their servers to the Internet: primarily port 80 for the web service. All other ports are locked. Consequently, client-server applications such as those presented in the previous chapter are built within the private network (intranet) and are generally not accessible from the outside. Hosting a service on a web server makes it accessible to the entire Internet community.
  • The web service can be modeled as a remote object. The services offered then become methods of this object. A client can access this remote object as if it were local. This hides the entire network communication layer and allows for the development of a client independent of that layer. If the layer changes, the client does not need to be modified. This is a huge advantage and likely the main benefit of web services.
  • As with the TCP/IP client-server applications presented in the previous chapter, the client and server can be written in any language. They exchange lines of text. These consist of two parts:
    • the headers required by the HTTP protocol
    • the message body. For a server response to the client, the body is in XML (eXtensible Markup Language) format. For a client request to the server, the message body can take several forms, including XML. The client’s XML request may use a specific format called SOAP (Simple Object Access Protocol). In this case, the server’s response also follows the SOAP format.

10.2. Browsers and XML

Web services send XML to their clients. Browsers may react differently when receiving this XML stream. Internet Explorer has a predefined style sheet that allows it to display the XML. Netscape Communicator, however, does not have this style sheet and does not display the received XML code. You must view the source code of the received page to access the XML. Here is an example. For the following XML code:

<?xml version="1.0" encoding="utf-8"?>
<string xmlns="st.istia.univ-angers.fr">hello again!</string>

Internet Explorer will display the following page:

Image

while Netscape Navigator will display:

Image

If we view the source code of the page received by Netscape, we get:

Image

Netscape did indeed receive the same content as Internet Explorer, but it displayed it differently. From here on, we will use Internet Explorer for the screenshots.

10.3. A First Web Service

We will explore web services through a very simple example available in three versions.

10.3.1. Version 1

For this first version, we’ll use VS.NET, which has the advantage of being able to generate a web service skeleton that’s immediately operational. Once we understand this architecture, we’ll be able to start working independently. That will be the focus of the following versions.

Using VS.NET, let’s create a new project via the [File/New/Project] option:

Image

Note the following points:

  • the project type is Visual Basic (left pane)
  • the project template is ASP.NET Web Service (right pane)
  • the location is flexible. Here, the web service will be hosted by a local IIS web server. Its URL will therefore be http://localhost/[path] where [path] is to be defined. Here, we choose the path http://localhost/polyvbnet/demo. VS.NET will then create a folder for this project. Where? The IIS server has a root directory for the web document tree it serves. Let’s call this root <IISroot>. It corresponds to the URL http://localhost. We can deduce that the URL http://localhost/polyvbnet/demo will be associated with the folder <IISroot>/polyvbnet/demo. <IISroot> is normally the \inetpub\wwwroot folder on the drive where IIS was installed. In our example, this is drive E. The folder created by VS.NET is therefore the e:\inetpub\wwwroot\polyvbnet\demo folder:

Image

As always, there is an abundance of folders created. They are not always useful. We will only explain the ones we need at a given time. VS.NET has created a project:

Image

We find some of the files present in the project’s physical folder. The most interesting one for us is the file with the .asmx extension. This is the extension for web services. A web service is managed by VS.NET as a Windows application, i.e., an application that has a graphical user interface and code to manage it. That is why we have a design window:

Image

A web service normally does not have a graphical user interface. It represents an object that can be called remotely. It has methods, and applications call these methods. We will therefore view it as a classic object with the unique feature that it can be instantiated remotely over the network. Therefore, we will not use the design window provided by VS.NET. Instead, let’s focus on the service’s code using the View/Code option:

Image

Several points are worth noting:

  • The file is named Service1.asmx.vb, not Service1.asmx. We’ll return to the contents of the Service1.asmx file a little later.
  • We see a code window similar to the one we had when building Windows applications with VS.NET

The code generated by VS.NET is as follows:


Imports System.Web.Services

<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
    Inherits System.Web.Services.WebService

#Region "Code generated by the Web Services Designer"

    Public Sub New()
        MyBase.New()

        'This call is required by the Web Services Designer.
        InitializeComponent()

        'Add your initialization code after the InitializeComponent() call

    End Sub

    'Required by the Web Services Designer
    Private components As System.ComponentModel.IContainer

    'NOTE: The following procedure is required by the Web Services Designer
    'It can be modified using the Web Services Designer.  
    'Do not modify it using the code editor.
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
    End Sub

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        'CODEGEN: This procedure is required by the Web Services Designer
        'Do not modify it using the code editor.
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

#End Region

    ' WEB SERVICE EXAMPLE
    ' The HelloWorld() service example returns the string "Hello World".
    ' To build, do not comment out the following lines, then save and build the project.
    ' To test this web service, make sure the .asmx file is the start page
    ' and press F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' End Function

End Class

First, note that we have a class here, the Service1 class, which derives from the WebService class:

Public Class Service1
    Inherits System.Web.Services.WebService

This leads us to import the System.Web.Services namespace:


Imports System.Web.Services

The class declaration is preceded by a compilation attribute:


<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
    Inherits System.Web.Services.WebService

The System.Web.Services.WebService() attribute indicates that the following class is a web service. This attribute accepts various parameters, including one called NameSpace. It is used to place the web service in a namespace. Indeed, one can imagine that there are several web services named "weather" in the world. We need a way to differentiate them. The namespace makes this possible. One could be named [namespace1].weather and another [namespace2].weather. This is a concept analogous to class namespaces. VS.NET automatically generated code and placed it in a region of the source:


#Region "Code generated by the Web Services Designer"

If we look at this code, it’s the same code the designer generated when we built Windows applications. This is code we can simply delete if we don’t have a graphical user interface, which will be the case for web services.

The class concludes with an example of what a web service might look like:


#End Region

    ' EXAMPLE OF A WEB SERVICE
    ' The HelloWorld() service example returns the string "Hello World".
    ' To generate, do not comment out the following lines, then save and build the project.
    ' To test this web service, make sure the .asmx file is the start page
    ' and press F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' End Function

Based on what has just been said, we clean up the code so that it becomes the following:


Imports System.Web.Services

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Hello
    Inherits System.Web.Services.WebService

    <WebMethod()> Public Function Hello() As String
        Return "Hello!"
    End Function
End Class

Now we have a clearer picture.

  • A web service is a class derived from the WebService class
  • The class is qualified by the attribute <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. We therefore place our service in the st.istia.univ-angers.fr namespace.
  • The class’s methods are qualified by a <WebMethod()> attribute indicating that we are dealing with a method that can be called remotely over the network

The class providing our web service is therefore called Bonjour and has a single method, also named Bonjour, which returns a string. We are ready for an initial test.

  • Start the IIS web server if you haven't already
  • Use the Debug/Run Without Debugging option. VS.NET

VS.NET will then compile the entire application, launch a browser (often Internet Explorer if it is available), and display the URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

Why the URL http://localhost/polyvbnet/demo/Service1.asmx? Because it was the only .asmx file in the project:

Image

If there had been multiple .asmx files, we would have had to specify which one should be executed first. This is done by right-clicking on the relevant .asmx file and selecting the [Set as Start Page] option.

Image

You might be interested in knowing what the service1.asmx file contains. In fact, with VS.NET, we worked on the service1.asmx.vb file and not on the service1.asmx file. This file is located in the project folder:

Image

Let’s open it with a text editor (Notepad or another). We get the following content:

<%@ WebService Language="vb" Codebehind="Service1.asmx.vb" Class="demo.Bonjour" %>

The file contains a simple directive for the IIS server indicating:

  • that this is a web service (WebService keyword)
  • that the language of this service's class is Visual Basic (Language="vb")
  • that the source code for this class is located in the file Service1.asmx.vb (Codebehind="Service1.asmx.vb")
  • that the class implementing the service is called demo.Bonjour (Class="demo.Bonjour"). Note that VS.NET has placed the Bonjour class in the demo namespace, which is also the name of the project.

Let’s return to the page accessed at the URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

Who wrote the HTML code for the page above? Not us—we know that. It’s IIS, which presents web services in a standard way. This page offers us two links. Let’s follow the first one [Service Description]:

Image

Oops... that’s some pretty obscure XML. Note, however, the URL

http://localhost/polyvbnet/demo/Service1.asmx?WSDL. Open a browser and type this URL directly. You’ll get the same result as before. So, remember that the URL http://serviceweb?WSDL provides access to the XML description of the web service. Let’s go back to the home page and click the [Hello] link. Remember that Hello is a method of the web service. If there had been multiple methods, they would all have been listed here. We get the following new page:

Image

We have intentionally truncated the resulting page to keep our demonstration concise. Note the URL again:

http://localhost/polyvbnet/demo/Service1.asmx?op=Bonjour

If we type this URL directly into a browser, we’ll get the same result as above. We’re prompted to use the [Call] button. Let’s do that. We get a new page:

Image

This is XML again. It contains two pieces of information that were present in our web service:

  • the namespace st.istia.univ-angers.fr of our service

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
  • the value returned by the Bonjour method:

        Return "hello!"

What have we learned?

  • how to write an S web service
  • how to call it

We will now look at writing a web service without using VS.NET.

10.3.2. Version 2

In the previous example, VS.NET did a lot of things on its own. Is it possible to build a web service without this tool? The answer is yes, and we’ll show you how now. Using a text editor, we’ll build the following web service:


Imports System.Web.Services

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Hello2
    Inherits System.Web.Services.WebService

    <WebMethod()> Public Function getBonjour() As String
        Return "hello again!"
    End Function
End Class

The class is called Bonjour2 and has a method called getBonjour. It is located in the demo2.vb file, which is itself located in the IIS server directory structure in the folder E:\Inetpub\wwwroot\polyvbnet\demo2. It is a standard VB.NET class that can therefore be compiled:

dos>vbc /out:demo2 /t:library /r:system.dll /r:system.web.services.dll demo2.vb

dos>dir
03/02/2004  18:04                  286 demo2.vb
03/02/2004  18:10                   77 demo2.asmx
03/02/2004  18:12                3,072 demo2.dll

We place the demo2.dll assembly in a bin folder (this name is required):

dos>dir bin
03/02/2004  6:12 PM                3,072 demo2.dll

We now create the demo2.asmx file. This is the file that will be called by web clients. Its contents are as follows:

<%@ WebService Language="vb" class="Bonjour2,demo2"%>

We have already encountered this directive. It indicates that:

  • the web service class is called Bonjour2 and is located in the demo2.dll assembly. IIS will look for this assembly in various locations, including the web service’s bin folder. That is why we placed the demo2.dll assembly there.

Now we can perform various tests. We make sure IIS is running and request the URL http://localhost/polyvbnet/demo2/demo2.asmx in a browser:

Image

Then the URL http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Then the URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, where getBonjour is the name of the only method in our web service:

Image

We use the [Call] button above:

Image

We successfully obtain the result of the call to the web service’s getBonjour method. We now know how to build a web service without Visual Studio .NET. From here on, we will ignore the specifics of how the web service is built and focus solely on the fundamental files.

10.3.3. Version 3

The two previous versions of the [Hello] web service used two files:

  • an .asmx file, the web service's entry point
  • a .vb file, the web service source code

Here, we show that a single .asmx file is sufficient. The code for the demo3.asmx service is as follows:

<%@ WebService Language="vb" class="Bonjour3"%>

Imports System.Web.Services

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour3
    Inherits System.Web.Services.WebService

    <WebMethod()> Public Function getBonjour() As String
        Return "Hello, version 3!"
    End Function
End Class

We can see that the service's source code is now directly in the source file demo3.asmx. The directive

<%@ WebService Language="vb" class="Bonjour3"%>

no longer references a class in an external assembly, but a class located in the same source file. Let’s place this file in the <IISroot>\polyvbnet\demo3 folder:

Image

Let’s start IIS and request the URL http://localhost/polyvbnet/demo3/demo3.asmx:

Image

We notice a significant difference from the previous version: we did not have to compile the service’s VB code. IIS performed this compilation itself using the VB.NET compiler installed on the same machine. It then delivered the page. If there is a compilation error, IIS will report it:

Image

10.3.4. Version 4

Here we focus on the IIS server configuration. Until now, we have always placed our web services in the <IISroot> root directory of the IIS server, here [e:\inetpub\wwwroot]. We demonstrate here that we can place the web service anywhere. This is done using IIS virtual directories. Let’s place our service in the following directory:

Image

The folder [D:\data\devel\vbnet\poly\chap9\demo3] is not located in the IIS server directory tree. We must specify this to IIS by creating a virtual IIS folder. Let’s launch IIS and select the [Advanced] option below:

Image

A list of virtual directories is displayed. We won’t dwell on this list. We create a new virtual directory using the [Add] button above:

Image

Using the [Browse] button, we select the physical folder containing the web service, in this case the folder [D:\data\devel\vbnet\poly\chap9\demo3]. We give this folder a logical (virtual) name: [virdemo3]. This means that the documents inside the physical folder [D:\data\devel\vbnet\poly\chap9\demo3] will be accessible on the network via the URL [http://<machine>/virdemo3]. The dialog box above contains other settings that we leave as is. We click OK. The new virtual folder appears in the list of virtual folders in IIS:

Image

Now, we open a browser and request the URL [http://localhost/virdemo3/demo3.asmx]. We get the same result as before:

Image

10.3.5. Conclusion

We have demonstrated several ways to create a web service. Moving forward, we will use the method from version 3 to create the service and method 4 for its deployment. This way, we will not need VS.NET. Nevertheless, it is worth noting the benefits of using VS.NET for the debugging assistance it provides. There are free tools available for developing web applications, notably the WebMatrix product sponsored by Microsoft, which can be found at the URL [http://www.asp.net/webmatrix]. It is an excellent tool for getting started with web programming without any upfront investment.

10.4. A web service for operations

Consider a web service that offers five functions:

  1. add(a,b), which returns a+b
  2. subtract(a,b), which returns a-b
  3. multiply(a,b), which returns a*b
  4. divide(a,b), which returns a/b
  5. doAll(a,b), which returns the array [a+b, a-b, a*b, a/b]

The VB.NET code for this service is as follows:


<%@ WebService language="VB" class=operations %>

imports system.web.services

<WebService(Namespace:="st.istia.univ-angers.fr")> _
   Public Class operations
      Inherits WebService

      <WebMethod>  _
      Function add(a As Double, b As Double) As Double
         Return a + b
      End Function 
   
      <WebMethod>  _
      Function subtract(a As Double, b As Double) As Double
         Return a - b
      End Function 

      <WebMethod>  _
      Function multiply(a As Double, b As Double) As Double
         Return a * b
      End Function 

      <WebMethod>  _
      Function divide(a As Double, b As Double) As Double
         Return a / b
      End Function 

      <WebMethod>  _
      Function doAll(a As Double, b As Double) As Double()
         Return New Double() {a + b, a - b, a * b, a / b}
      End Function 
   End Class 

We are repeating some explanations here that have already been given but are worth revisiting or expanding upon. The operations class resembles a VB.NET class, with a few points to note:

  • methods are preceded by a <WebMethod()> attribute that tells the compiler which methods should be "published," i.e., made available to the client. A method not preceded by this attribute would be invisible to remote clients. This could be an internal method used by other methods but not intended for publication.
  • The class derives from the WebService class defined in the System.Web.Services namespace. This inheritance is not always mandatory. In this example, in particular, we could do without it.
  • The class itself is preceded by a <WebService(Namespace="st.istia.univ-angers.fr")> attribute intended to provide a namespace for the web service. A class vendor assigns a namespace to its classes to give them a unique name and thus avoid conflicts with classes from other vendors that might have the same name. The same applies to web services. Each web service must be identifiable by a unique name, in this case st.istia.univ-angers.fr.
  • We have not defined a constructor. Therefore, the constructor of the parent class will be used implicitly.

The source code above is not intended directly for the VB.NET compiler but for the IIS web server. It must have the .asmx extension and be saved in the web server directory structure. Here, we save it as operations.asmx in the <IISroot>\polyvbnet\operations folder:

Image

We associate the IIS virtual directory [operations] with this physical directory:

Let’s access the service using a browser. The URL to request is [http://localhost/operations/operations.asmx]:

Image

We get a web document with a link for each of the methods defined in the operations web service. Let’s follow the add link:

Image

The page that appears invites us to test the add method by providing the two arguments a and b that it requires. Recall the definition of the *add* method:

      <WebMethod>  _
      Function add(a As Double, b As Double) As Double
         Return a + b
      End Function 

Note that the page has used the argument names a and b from the method definition. Click the Call button, and the following response appears in a separate browser window:

Image

If you select [View/Source] above, you get the following code:

Image

Let’s repeat the process for the [toutfaire] method:

Image

We get the following page:

Image

Let’s use the [Call] button above:

Image

In all cases, the server’s response has the following format:

<?xml version="1.0" encoding="utf-8"?>
[response in XML format]
  • the response is in XML format
  • Line 1 is standard and is always present in the response
  • The following lines depend on the result type (double, ArrayOfDouble), the number of results, and the web service namespace (st.istia.univ-angers.fr in this case).

There are several methods for querying a web service and obtaining its response. Let’s return to the service’s URL:

Image

and follow the [Add] link. On the page that appears, two methods for querying the [Add] function of the web service are presented:

Image

Image

Image

These two methods for accessing a web service’s functions are called, respectively: HTTP-POST and SOAP. We will now examine them one by one.

Note: In early versions of VS.NET, there was a third method called HTTP-GET. As of the date of this document (March 2004), this method no longer appears to be available. This means that the web service generated by VS.NET does not accept GET requests. This does not mean that you cannot write web services that accept GET requests, particularly using tools other than VS.NET or simply by hand.

10.5. An HTTP-POST client

We’ll follow the method proposed by the web service:

Image

Let’s break down what’s written. First, the web client must send the following HTTP headers:

POST /operations/operations.asmx/add HTTP/1.1
The web client makes a POST request to the URL /operations/operations.asmx/add according to the HTTP version 1.1 protocol
HOST: localhost
We specify the target machine for the request. Here, localhost. This header was made mandatory by version 1.1 of the HTTP protocol
Content-Type: application/x-www-form-urlencoded
This specifies that after the HTTP headers, additional parameters will be sent in urlencoded format. This format replaces certain characters with their hexadecimal codes.
Content-Length: 7
This is the character count of the parameter string that will be sent after the HTTP headers.

The HTTP headers are followed by a blank line, then by the POST parameter string of [Content-Length] characters in the form a=XX&b=YY, where XX and YY are the "URL-encoded" strings of the values of parameters a and b. We know enough to reproduce the above with our generic TCP client already used in the chapter on TCP/IP programming:

  • We launch IIS
  • the service is available at the URL [http://localhost/operations/operations.asmx]
  • we use the generic TCP client in a DOS window
dos>generic-tcp-client localhost 80
Commands:
POST /operations/operations.asmx/add HTTP/1.1
HOST: localhost
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-length: 7

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--
a=2&b=3
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, Mar 3, 2004 1:55:26 PM GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>
[end of thread reading server responses]
end
[end of the thread sending commands to the server]

First, note that we have added the [Connection: close] header to instruct the server to close the connection after sending the response. This is necessary here. If we do not specify this, by default the server will keep the connection open. However, its response is a sequence of text lines, the last of which is not terminated by a line-end character. It turns out that our generic TCP client reads text lines terminated by an end-of-line character using the ReadLine method. If the server does not close the connection after sending the last line, the client gets blocked because it is waiting for an end-of-line character that never arrives. If the server closes the connection, the client’s ReadLine method completes, and the client does not get blocked.

Immediately after receiving the empty line signaling the end of the HTTP headers, the IIS server sends an initial response:

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--

This response, consisting solely of HTTP headers, tells the client that it can send the 7 characters it said it wanted to send. What we do:

a=2&b=3

Note that our TCP client sends more than 7 characters here, since it sends them with a line-end marker (WriteLine). This does not interfere with the server, which will only take the first 7 characters from those received, and because the connection is then closed (Connection: close). These extra characters would have been problematic if the connection had remained open, as they would then have been interpreted as coming from the client’s next command. Once the parameters are received, the server sends its response:

<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:26 GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>

We now have the elements to write a client program for our web service. It will be a console client called httpPost2 and used as follows:


dos>httpPost2 http://localhost/operations/operations.asmx

Type your commands in the following format: [add|subtract|multiply|divide] a b

add 6 7
--> POST /operations/operations.asmx/add HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, Mar 3, 2004 2:56:38 PM GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[result=13]

subtract 8 9
--> POST /operations/operations.asmx/subtract HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, Mar 3, 2004 2:56:47 PM GMT
<-- X-Powered-By: ASP.NET
<--
--> a=8&b=9
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, Mar 3, 2004 2:56:47 PM GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">-1</double>
[result=-1]

end

end>

The client is called by passing it the URL of the web service:

dos>httpPost2 http://localhost/operations/operations.asmx

Next, the client reads the commands typed on the keyboard and executes them. These are in the format:

function a b

where function is the web service function being called (add, subtract, multiply, divide) and a and b are the values on which this function will operate. For example:

add 6 7

From there, the client will send the necessary HTTP request to the web server and receive a response. The client-server exchanges are displayed on the screen to help you better understand the process:

add 6 7
--> POST /operations/operations.asmx/add HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, Mar 3, 2004 2:56:38 PM GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[result=13]

The exchange shown above is the same as the one we saw with the generic TCP client, with one difference: the HTTP header **Connection: Keep-Alive instructs the server not to close the connection. The connection therefore remains open for the client’s next operation, so the client does not need to reconnect to the server. However, this requires the client to use a method other than ReadLine() to read the server’s response, since we know that the response consists of a sequence of lines, the last of which is not terminated by a newline character. Once the entire server response has been received, the client parses it to find the result of the requested operation and display it:

[result=13]

Let’s examine our client’s code:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web

' Web service client operations
Public Module clientPOST

    Public Sub Main(ByVal args() As String)
        ' syntax
        Const syntax As String = "pg URI"
        Dim functions As String() = {"add", "subtract", "multiply", "divide"}

        ' number of arguments
        If args.Length <> 1 Then
            error(syntax, 1)
        End If
        ' note the requested URI
        Dim URIstring As String = args(0)

        ' we connect to the server
        Dim uri As Uri = Nothing        ' the web service URI
        Dim client As TcpClient = Nothing        ' The client's TCP connection to the server
        Dim [IN] As StreamReader = Nothing        ' the client's read stream
        Dim OUT As StreamWriter = Nothing        ' the client's write stream
        Try
            ' connection to the server
            uri = New Uri(URIstring)
            client = New TcpClient(uri.Host, uri.Port)
            ' Create the TCP client's input and output streams
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
        Catch ex As Exception
            ' Incorrect URI or other problem
            error("The following error occurred: " + ex.Message, 2)
        End Try

        ' Create a dictionary of web service functions
        Dim functionDictionary As New Hashtable
        Dim i As Integer
        For i = 0 To functions.Length - 1
            FunctionDictionary.Add(functions(i), True)
        Next i

        ' User input is typed on the keyboard
        ' in the form function a b
        ' they end with the command "end"
        Dim command As String = Nothing        ' command typed on the keyboard
        Dim fields As String() = Nothing        ' fields of a command line
        Dim function As String = Nothing        ' name of a web service function
        Dim a, b As String        ' arguments for web service functions

        ' prompt to the user
        Console.Out.WriteLine("Enter your commands in the format: [add|subtract|multiply|divide] a b")

        ' error handling
        Dim commandError As Boolean
        Try
            ' loop for capturing keyboard commands
            While True
                ' no error at the start
                commandError = False
                ' read command
                command = Console.In.ReadLine().Trim().ToLower()
                ' finished?
                If command Is Nothing Or command = "end" Then
                    Exit While
                End If
                ' Split the command into fields
                fields = Regex.Split(command, "\s+")
                Try
                    ' three fields are required
                    If fields.Length <> 3 Then
                        Throw New Exception
                    End If
                    ' Field 0 must be a recognized function
                    function = fields(0)
                    If Not functionDictionary.ContainsKey(function) Then
                        Throw New Exception
                    End If
                    ' Field 1 must be a valid number
                    a = fields(1)
                    Double.Parse(a)
                    ' Field 2 must be a valid number
                    b = fields(2)
                    Double.Parse(b)
                Catch
                    ' invalid command
                    Console.Out.WriteLine("syntax: [add|subtract|multiply|divide] a b")
                    commandError = True
                End Try
                ' Send the request to the web service
                If Not errorCommand Then executeFunction([IN], OUT, uri, function, a, b)
            End While
        Catch e As Exception
            Console.Out.WriteLine(("The following error occurred: " + e.Message))
        End Try
        ' end client-server connection
        Try
            [IN].Close()
            OUT.Close()
            client.Close()
        Catch
        End Try
    End Sub
...........
    ' display errors
    Public Sub error(ByVal msg As String, ByVal exitCode As Integer)
        ' display error
        System.Console.Error.WriteLine(msg)
        ' exit with error
        Environment.Exit(exitCode)
    End Sub
End Module

We’ve seen these elements several times before, and they don’t require any special comments. Let’s now examine the code for the executeFonction method, where the new elements are located:


    ' executeFonction
    Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal function As String, ByVal a As String, ByVal b As String)
        ' executes function(a,b) on the web service at URI uri
        ' client-server communication occurs via the IN and OUT streams
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server

        ' constructing the query string
        Dim request As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = query.Length

        ' constructing the array of HTTP headers to send
        Dim headers(5) As String
        headers(0) = "POST " + uri.AbsolutePath + "/" + function + " HTTP/1.1"
        headers(1) = "Host: " & uri.Host & ":" & uri.Port
        headers(2) = "Content-Type: application/x-www-form-urlencoded"
        headers(3) = "Content-Length: " & nbChars
        headers(4) = "Connection: Keep-Alive"
        headers(5) = ""

        ' send the HTTP headers to the server
        Dim i As Integer
        For i = 0 To headers.Length - 1
            ' send to the server
            OUT.WriteLine(headers(i))
            ' console output
            Console.Out.WriteLine(("--> " + headers(i)))
        Next i

        ' read the first response from the HTTP/1.1 100 web server
        Dim line As String = Nothing
        ' a line from the read stream
        line = [IN].ReadLine()
        While line <> ""
            'echo
            Console.Out.WriteLine(("<-- " + line))
            ' next line
            line = [IN].ReadLine()
        End While
        'print last line
        Console.Out.WriteLine(("<-- " + line))

        ' send request parameters
        OUT.Write(request)
        ' echo
        Console.Out.WriteLine(("--> " + request))

        ' build the regular expression to retrieve the size of the XML response
        ' in the web server response stream
        Dim patternLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(patternLength)        '
        Dim MatchLength As Match = Nothing
        Dim length As Integer = 0

        ' Read the second response from the web server after sending the request
        ' store the value of the Content-Length line
        line = [IN].ReadLine()
        While line <> ""
            ' display on screen
            Console.Out.WriteLine(("<-- " + line))
            ' Content-Length?
            MatchLength = RegexLength.Match(line)
            If MatchLength.Success Then
                length = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            line = [IN].ReadLine()
        End While
        ' echo last line
        Console.Out.WriteLine("<--")

        ' build the regular expression to find the result
        ' in the web server response stream
        Dim pattern As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
        Dim ResultPattern As New Regex(pattern)
        Dim ResultMatch As Match = Nothing

        ' read the rest of the web server's response
        Dim chrResponse(length) As Char
        [IN].Read(chrResponse, 0, length)
        Dim strResponse As String = New [String](chrResponse)

        ' split the response into lines of text
        Dim lines As String() = Regex.Split(strResponse, ControlChars.Lf)

        ' iterate through the text lines to find the result
        Dim strResult As String = "?"        ' result of the function
        For i = 0 To lines.Length - 1
            ' tracking
            Console.Out.WriteLine(("<-- " + lines(i)))
            ' compare current line to template
            MatchResult = TemplateResult.Match(lines(i))
            ' Was a match found?
            If MatchResult.Success Then
                ' record the result
                strResult = MatchResult.Groups(1).Value
            End If
        Next i
        ' display the result
        Console.Out.WriteLine(("[result=" + strResult + "]" + ControlChars.Lf))
    End Sub

First, the HTTP-POST client sends its request in POST format:


        ' constructing the request string
        Dim request As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = request.Length

        ' Constructing the HTTP header array to be sent
        Dim headers(5) As String
        headers(0) = "POST " + uri.AbsolutePath + "/" + function + " HTTP/1.1"
        headers(1) = "Host: " & uri.Host & ":" & uri.Port
        headers(2) = "Content-Type: application/x-www-form-urlencoded"
        headers(3) = "Content-Length: " & nbChars
        headers(4) = "Connection: Keep-Alive"
        headers(5) = ""

        ' send the HTTP headers to the server
        Dim i As Integer
        For i = 0 To headers.Length - 1
            ' send to the server
            OUT.WriteLine(headers(i))
            ' console output
            Console.Out.WriteLine(("--> " + headers(i)))
        Next i

In the header

--> Content-Length: 7

we must specify the size of the parameters that will be sent by the client behind the HTTP headers:

--> a=6&b=7

To do this, use the following code:


        ' constructing the query string
        Dim request As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = query.Length

The HttpUtility.UrlEncode(string string) method converts certain characters in the string to %n1n2, where n1n2 is the ASCII code of the converted character. The characters targeted by this conversion are all characters that have a specific meaning in a POST request (space, =, &, etc.). Here, the HttpUtility.UrlEncode method is normally unnecessary since a and b are numbers that do not contain any of these special characters. It is used here as an example. It requires the System.Web namespace. Once the client has sent its HTTP headers:

--> POST /operations/operations.asmx/add HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->

The server responds with the HTTP 100 Continue header:

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<--

The code simply reads and displays this first response on the screen:


        ' reading the first response from the HTTP/1.1 100 web server
        Dim line As String = Nothing
        ' a line from the read stream
        line = [IN].ReadLine()
        While line <> ""
            'echo
            Console.Out.WriteLine(("<-- " + line))
            ' next line
            line = [IN].ReadLine()
        End While
        'print last line
        Console.Out.WriteLine(("<-- " + line))

Once this initial response has been read, the client must send its parameters:

--> a=6&b=7

It does so with the following code:


        ' send request parameters
        OUT.Write(request)
        ' echo
        Console.Out.WriteLine(("--> " + request))

The server will then send its response. This response consists of two parts:

  1. HTTP headers followed by a blank line
  2. the response in XML format
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>

First, the client reads the HTTP headers to find the Content-Length line and retrieve the size of the XML response (here, 90). This is retrieved using a regular expression. We could have done this differently and likely more efficiently.


        ' construction of the regular expression used to determine the size of the XML response
        ' in the web server response stream
        Dim templateLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modelLength)        '
        Dim MatchLength As Match = Nothing
        Dim length As Integer = 0

        ' Read the second response from the web server after sending the request
        ' store the value of the Content-Length line
        line = [IN].ReadLine()
        While line <> ""
            ' display on screen
            Console.Out.WriteLine(("<-- " + line))
            ' Content-Length?
            MatchLength = RegexLength.Match(line)
            If MatchLength.Success Then
                length = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            line = [IN].ReadLine()
        End While
        ' print last line
        Console.Out.WriteLine("<--")

Once we have the length N of the XML response, we simply need to read N characters from the IN stream of the server's response. This string of N characters is broken down into lines of text for the purposes of screen monitoring. Among these lines, we look for the line containing the result:

<-- <double xmlns="st.istia.univ-angers.fr">13</double>

again using a regular expression. Once the result is found, it is displayed.

[result=13]

The end of the client code is as follows:


        ' construction of the regular expression used to find the result
        ' in the web server's response stream
        Dim template As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
        Dim ResultTemplate As New Regex(template)
        Dim ResultMatch As Match = Nothing

        ' read the rest of the response from the web server
        Dim chrResponse(length) As Char
        [IN].Read(chrResponse, 0, length)
        Dim strResponse As String = New [String](chrResponse)

        ' split the response into lines of text
        Dim lines As String() = Regex.Split(strResponse, ControlChars.Lf)

        ' iterate through the text lines to find the result
        Dim strResult As String = "?"        ' result of the function
        For i = 0 To lines.Length - 1
            ' tracking
            Console.Out.WriteLine(("<-- " + lines(i)))
            ' compare current line to template
            MatchResult = TemplateResult.Match(lines(i))
            ' Was a match found?
            If MatchResult.Success Then
                ' record the result
                strResult = MatchResult.Groups(1).Value
            End If
        Next i
        ' display the result
        Console.Out.WriteLine(("[result=" + strResult + "]" + ControlChars.Lf))
    End Sub

10.6. A SOAP client

Here we examine a second client that will use a SOAP (Simple Object Access Protocol) client-server dialogue. An example of such a dialogue is presented for the add function:

Image

Image

The client's request is a POST request. We will therefore see some of the same mechanisms as in the previous client. The main difference is that while the HTTP-POST client sent the parameters a and b in the form

    a=A&b=B

the SOAP client sends them in a more complex XML format:

POST /operations/operations.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "st.istia.univ-angers.fr/add"

<?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>
    <add xmlns="st.istia.univ-angers.fr">
      <a>double</a>
      <b>double</b>
    </add>
  </soap:Body>
</soap:Envelope>

It receives an XML response in return that is also more complex than the responses seen previously:

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length

<?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>
    <addResponse xmlns="st.istia.univ-angers.fr">
      <addResult>double</addResult>
    </addResponse>
  </soap:Body>
</soap:Envelope>

Even though the request and response are more complex, this is indeed the same HTTP mechanism as for the HTTP-POST client. The SOAP client code can therefore be modeled after that of the HTTP-POST client. Here is an example of execution:

dos>clientsoap1 http://localhost/operations/operations.asmx
Type your commands in the following format: [add|subtract|multiply|divide] a b

add 3 4
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/add"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--
--> <?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>
<add xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</add>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:33 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 345
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>7</ajouterResult></ajouterResponse
></soap:Body></soap:Envelope>
[result=7]

Only the executeFonction method changes. The SOAP client sends the HTTP headers for its request. They are simply a bit more complex than those of HTTP-POST:

add 3 4
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/add"
-->

The code that generates them:


    ' executeFonction
    Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
        ' executes function(a,b) on the web service at URI uri
        ' client-server communication occurs via the IN and OUT streams
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server
        ' construction of the SOAP request string

        Dim requestSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
        SOAPRequest += "<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/">" + ControlChars.Lf
        SOAPRequest += "<soap:Body>" + ControlChars.Lf
        SOAPRequest += "<" + function + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
        SOAPRequest += "<a>" + a + "</a>" + ControlChars.Lf
        SOAPRequest += "<b>" + b + "</b>" + ControlChars.Lf
        SOAPRequest += "</" + function + ">" + ControlChars.Lf
        SOAPRequest += "</soap:Body>" + ControlChars.Lf
        SOAPRequest += "</soap:Envelope>"
        Dim nbCharsSOAP As Integer = SOAPRequest.Length

        ' constructing the array of HTTP headers to send
        Dim headers(6) As String
        headers(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
        headers(1) = "Host: " & uri.Host & ":" & uri.Port
        headers(2) = "Content-Type: text/xml; charset=utf-8"
        headers(3) = "Content-Length: " & nbCharsSOAP
        headers(4) = "Connection: Keep-Alive"
        headers(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + function + """"
        headers(6) = ""

        ' send the HTTP headers to the server
        Dim i As Integer
        For i = 0 To headers.Length - 1
            ' send to the server
            OUT.WriteLine(headers(i))
            ' console output
            Console.Out.WriteLine(("--> " + headers(i)))
        Next i

Upon receiving this request, the server sends its first response, which the client displays:

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--

The code for reading this first response is as follows:


        ' read the first response from the HTTP/1.1 100 web server
        Dim line As String = Nothing
        ' a line from the read stream
        line = [IN].ReadLine()
        While line <> ""
            'echo
            Console.Out.WriteLine(("<-- " + line))
            ' next line
            line = [IN].ReadLine()
        End While        'while
        'echo last line
        Console.Out.WriteLine(("<-- " + line))

The client will now send its parameters in XML format in what is called a SOAP envelope:

--> <?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>
<add xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</add>
</soap:Body>
</soap:Envelope>

The code:


        ' send request parameters
        OUT.Write(soapRequest)
        ' echo
        Console.Out.WriteLine(("--> " + SOAPRequest))

The server will then send its final response:

<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:33 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 345
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>7</ajouterResult></ajouterResponse
></soap:Body></soap:Envelope>

The client displays the received HTTP headers on the screen while searching for the Content-Length line:


        ' constructing the regular expression to retrieve the size of the XML response
        ' in the web server's response stream
        Dim templateLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modelLength)        '
        Dim MatchLength As Match = Nothing
        Dim length As Integer = 0

        ' Read the second response from the web server after sending the request
        ' store the value of the Content-Length line
        line = [IN].ReadLine()
        While line <> ""
            ' print to screen
            Console.Out.WriteLine(("<-- " + line))
            ' Content-Length?
            MatchLength = RegexLength.Match(line)
            If MatchLength.Success Then
                length = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            line = [IN].ReadLine()
        End While        'while
        ' print last line
        Console.Out.WriteLine("<--")

Once the size N of the XML response is known, the client reads N characters from the server's response stream, splits the retrieved string into lines of text to display them on the screen, and searches for the XML tag of the result: <ajouterResult>7</ajouterResult> and displays it:


        ' constructing the regular expression to find the result
        ' in the web server response stream
        Dim pattern As String = "<" + function + "Result>(.+?)</" + function + "Result>"
        Dim ResultPattern As New Regex(pattern)
        Dim ResultMatch As Match = Nothing

        ' read the rest of the response from the web server
        Dim chrResponse(length) As Char
        [IN].Read(responseChar, 0, length)
        Dim strResponse As String = New [String](chrResponse)

        ' split the response into lines of text
        Dim lines As String() = Regex.Split(strResponse, ControlChars.Lf)

        ' iterate through the text lines to find the result
        Dim strResult As String = "?"        ' result of the function
        For i = 0 To lines.Length - 1
            ' tracking
            Console.Out.WriteLine(("<-- " + lines(i)))
            ' compare current line to template
            MatchResult = TemplateResult.Match(lines(i))
            ' Was a match found?
            If MatchResult.Success Then
                ' record the result
                strResult = MatchResult.Groups(1).Value
            End If
            'next line
        Next i
        ' display the result
        Console.Out.WriteLine(("[result=" + strResult + "]" + ControlChars.Lf))
    End Sub

10.7. Encapsulation of client-server communication

Let’s imagine that our web service operations is used by various applications. It would be useful to provide these applications with a class that acts as an interface between the client application and the web service, hiding most of the network communication, which is not trivial for most developers. This would result in the following architecture:

The client application would address the client-server interface to make its requests to the web service. The interface would handle all necessary network communication with the server and return the result to the client application. The client application would no longer have to deal with communication with the server, which would greatly simplify its development.

10.7.1. The Encapsulation Class

Based on what we’ve covered in the previous sections, we now have a good understanding of the network communication between the client and the server. We’ve even looked at three methods. We’ve chosen to encapsulate the SOAP method. The class is as follows:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic

' SOAP client for the Web service operations
Public Class SOAPClient

    ' instance variables
    Private uri As uri = Nothing    ' the Web service URI
    Private TcpClient As TcpClient = Nothing    ' the client's TCP connection to the server
    Private [IN] As StreamReader = Nothing    ' the client's read stream
    Private OUT As StreamWriter = Nothing    ' the client's write stream
    ' function dictionary
    Private functionDictionary As New Hashtable
    ' list of functions
    Private functions As String() = {"add", "subtract", "multiply", "divide"}
    ' verbose
    Private verbose As Boolean = False    ' When true, displays client-server exchanges on the screen

    ' constructor
    Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)

        ' Set verbose
        Me.verbose = verbose

        ' Connect to the server
        uri = New Uri(uriString)
        client = New TcpClient(uri.Host, uri.Port)

        ' Create the TCP client's input and output streams
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' Create the dictionary of web service functions
        Dim i As Integer
        For i = 0 To functions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
    End Sub

    ' Close the connection to the server
    Public Sub Close()
        ' End client-server connection
        [IN].Close()
        OUT.Close()
        client.Close()
    End Sub

    ' executeFunction
    Public Function executeFunction(ByVal function As String, ByVal a As String, ByVal b As String) As String
        ' executes function(a,b) on the web service at URI uri
        ' client-server communication occurs via the IN and OUT streams
        ' The result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server

        ' Is the function valid?
        function = function.Trim().ToLower()
        If Not functionDictionary.ContainsKey(function) Then
            Return "[function [" + function + "] unavailable: (add, subtract, multiply, divide)]"
        End If

        ' Are arguments a and b valid?
        Dim doubleA As Double = 0
        Try
            doubleA = Double.Parse(a)
        Catch
            Return "[argument [" + a + "] is incorrect (double)]"
        End Try
        Dim doubleB As Double = 0
        Try
            doubleB = Double.Parse(b)
        Catch
            Return "[argument [" + b + "] is incorrect (double)]"
        End Try

        ' division by zero?
        If function = "divide" And doubleB = 0 Then
            Return "[division by zero]"
        End If

        ' constructing the SOAP request string
        Dim SOAPRequest As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
        SOAPRequest += "<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/"">" + ControlChars.Lf
        SOAPRequest += "<soap:Body>" + ControlChars.Lf
        SOAPRequest += "<" + function + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
        SOAPRequest += "<a>" + a + "</a>" + ControlChars.Lf
        SOAPRequest += "<b>" + b + "</b>" + ControlChars.Lf
        SOAPRequest += "</" + function + ">" + ControlChars.Lf
        SOAPRequest += "</soap:Body>" + ControlChars.Lf
        SOAPRequest += "</soap:Envelope>"
        Dim nbCharsSOAP As Integer = SOAPRequest.Length

        ' constructing the array of HTTP headers to send
        Dim headers(6) As String
        headers(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
        headers(1) = "Host: " + uri.Host + ":" + uri.Port.ToString
        headers(2) = "Content-Type: text/xml; charset=utf-8"
        headers(3) = "Content-Length: " + nbCharsSOAP.ToString
        headers(4) = "Connection: Keep-Alive"
        headers(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + function + """"
        headers(6) = ""

        ' send the HTTP headers to the server
        Dim i As Integer
        For i = 0 To headers.Length - 1
            ' send to the server
            OUT.WriteLine(headers(i))
            ' Screen echo
            If verbose Then
                Console.Out.WriteLine(("--> " + headers(i)))
            End If
        Next i

        ' Read the first response from the HTTP/1.1 100 web server
        Dim line As String = Nothing
        ' a line from the read stream
        line = [IN].ReadLine()
        While line <> ""
            'echo
            If verbose Then
                Console.Out.WriteLine(("<-- " + line))
            End If
            ' next line
            line = [IN].ReadLine()
        End While
        'echo last line
        If verbose Then
            Console.Out.WriteLine(("<-- " + line))
        End If
        ' send request parameters
        OUT.Write(SOAPRequest)
        ' echo
        If verbose Then
            Console.Out.WriteLine(("--> " + SOAPRequest))
        End If

        ' constructing the regular expression to retrieve the size of the XML response
        ' in the web server response stream
        Dim patternLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(patternLength)        '
        Dim MatchLength As Match = Nothing
        Dim length As Integer = 0

        ' Read the second response from the web server after sending the request
        ' store the value of the Content-Length line
        line = [IN].ReadLine()
        While line <> ""
            ' print to screen
            If verbose Then
                Console.Out.WriteLine(("<-- " + line))
            End If
            ' Content-Length?
            MatchLength = RegexLength.Match(line)
            If MatchLength.Success Then
                length = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            line = [IN].ReadLine()
        End While

        ' echo last line
        If verbose Then
            Console.Out.WriteLine("<--")
        End If

        ' Construct the regular expression to extract the result
        ' in the web server response stream
        Dim pattern As String = "<" + function + "Result>(.+?)</" + function + "Result>"
        Dim ResultPattern As New Regex(pattern)
        Dim ResultMatch As Match = Nothing

        ' read the rest of the web server's response
        Dim chrResponse(length) As Char
        [IN].Read(responseChar, 0, length)
        Dim strResponse As String = New [String](chrResponse)

        ' split the response into lines of text
        Dim lines As String() = Regex.Split(strResponse, ControlChars.Lf)

        ' iterate through the text lines to find the result
        Dim resultStr As String = "?"        ' result of the function
        For i = 0 To lines.Length - 1
            ' tracking
            If verbose Then
                Console.Out.WriteLine(("<-- " + lines(i)))
            End If            ' compare current line to template
            MatchResult = TemplateResult.Match(lines(i))
            ' Was a match found?
            If MatchResult.Success Then
                ' record the result
                strResult = MatchResult.Groups(1).Value
            End If
        Next i

        ' return the result
        Return strResult
    End Function
End Class

There is nothing new here compared to what we have already seen. We simply took the code from the SOAP client we studied and rearranged it slightly to turn it into a class. This class has a constructor and two methods:


    ' constructor
    Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)

    ' executeFunction
    Public Function executeFunction(ByVal function As String, ByVal a As String, ByVal b As String) As String


    ' Close the connection to the server
    Public Sub Close()

and has the following attributes:


    ' instance variables
    Private uri As Uri = Nothing    ' the web service URI
    Private client As TcpClient = Nothing    ' the client's TCP connection to the server
    Private [IN] As StreamReader = Nothing    ' the client's read stream
    Private OUT As StreamWriter = Nothing    ' the client's write stream
    ' function dictionary
    Private functionDictionary As New Hashtable
    ' list of functions
    Private functions As String() = {"add", "subtract", "multiply", "divide"}
    ' verbose
    Private verbose As Boolean = False    ' When true, displays client-server exchanges on the screen

We pass two parameters to the constructor:

  1. the URI of the web service it must connect to
  2. a boolean verbose parameter which, when true, requests that network exchanges be displayed on the screen; otherwise, they will not be displayed.

During construction, the IN stream for network reading, the OUT stream for network writing, and the dictionary of functions managed by the service are created. Once the object is constructed, the client-server connection is open and its IN and OUT streams are ready for use.

The Close method closes the connection to the server.

The ExecuteFonction method is the one we wrote for the SOAP client we studied, with a few minor differences:

  1. The parameters `uri`, `IN`, and `OUT`, which were previously passed as parameters to the method, no longer need to be passed, since they are now instance attributes accessible to all methods of the instance
  2. The ExecuteFonction method, which previously returned a void type and displayed the function's result on the screen, now returns that result—and thus a string type.

Typically, a client will use the clientSOAP class as follows:

  1. creating a clientSOAP object that will establish the connection to the web service
  2. repeatedly calling the executeFonction method
  3. Closing the connection to the web service using the Close method.

Let’s examine a first client.

10.7.2. A console client

Here we revisit the SOAP client we studied when the clientSOAP class did not yet exist, and we redesign it so that it now uses this class:


' namespaces
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

Public Module testClientSoap

    ' requests the URI of the operations web service
    ' interactively executes commands typed on the keyboard
    Public Sub Main(ByVal args() As String)
        ' syntax
        Const syntax As String = "pg URI [verbose]"

        ' number of arguments
        If args.Length <> 1 And args.Length <> 2 Then
            error(syntax, 1)
        End If
        ' verbose?
        Dim verbose As Boolean = False
        If args.Length = 2 Then
            verbose = args(1).ToLower() = "verbose"
        End If
        ' Connect to the web service 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' connection error
            error("The following error occurred while connecting to the web service: " + ex.Message, 2)
        End Try

        ' user requests are typed on the keyboard
        ' in the form function a b - they end with the end command
        Dim command As String = Nothing        ' command typed on the keyboard
        Dim fields As String() = Nothing        ' fields of a command line

        ' prompt to the user
        Console.Out.WriteLine("Enter your commands in the format: [add|subtract|multiply|divide] a b" + ControlChars.Lf)

        ' error handling
        Dim errorCommand As Boolean
        Try
            ' loop for entering commands typed on the keyboard
            While True
                ' initially no error
                commandError = False
                ' read command
                command = Console.In.ReadLine().Trim().ToLower()
                ' finished?
                If command Is Nothing Or command = "end" Then
                    Exit While
                End If
                ' Split the command into fields
                fields = Regex.Split(command, "\s+")
                ' there must be three fields
                If fields.Length <> 3 Then
                    Console.Out.WriteLine("syntax: [add|subtract|multiply|divide] a b")
                    ' log the error
                    commandError = True
                End If
                ' send the request to the web service
                If Not commandError Then Console.Out.WriteLine(("result=" + client.executeFunction(fields(0).Trim().ToLower(), fields(1).Trim(), fields(2).Trim())))
                ' next request
            End While
        Catch e As Exception
            Console.Out.WriteLine(("The following error occurred: " + e.Message))
        End Try
        ' end client-server connection
        Try
            client.Close()
        Catch
        End Try
    End Sub

    ' display errors
    Public Sub error(ByVal msg As String, ByVal exitCode As Integer)
        ' display error
        System.Console.Error.WriteLine(msg)
        ' Exit with error
        Environment.Exit(exitCode)
    End Sub
End Module

The client is now considerably simpler and contains no network communication. The client accepts two parameters:

  1. the URI of the web service operations
  2. the optional verbose keyword. If present, network exchanges will be displayed on the screen.

These two parameters are used to construct a clientSOAP object that will handle communication with the web service.


        ' Connect to the web service 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' connection error
            error("The following error occurred while connecting to the web service: " + ex.Message, 2)
        End Try

Once the connection to the web service is established, the client can send its requests. These are typed on the keyboard, parsed, and then sent to the server by calling the executeFonction method of the clientSOAP object.


                ' Make the request to the web service
                If Not commandError Then Console.Out.WriteLine(("result=" + client.executeFunction(fields(0).Trim().ToLower(), fields(1).Trim(), fields(2).Trim())))

The clientSOAP class is compiled into an "assembly":

dos>vbc /r:clientSOAP.dll testClientSOAP.vb
dos>dir
03/04/2004  08:46                6 913 clientSOAP.vb
04/03/2004  09:07                7,168 clientSOAP.dll

The testClientSoap client application is then compiled using:

dos>vbc /r:clientSOAP.dll /r:system.dll testClientSOAP.vb
dos>dir
03/04/2004  09:08                2,711 testClientSOAP.vb
03/04/2004  09:08                4,608 testClientSOAP.exe

Here is an example of a non-verbose execution:


dos>testclientsoap http://localhost/st/operations/operations.asmx
Type your commands in the following format: [add|subtract|multiply|divide] a b

add 1 3
result=4
subtract 6 7
result=-1
multiply 4 5
result=20
divide 1 by 2
result=0.5
x
syntax: [add|subtract|multiply|divide] a b
x 1 2
result=[function [x] not available: (add, subtract, multiply, divide)]
add a b
result=[incorrect argument [a] (duplicate)]
add 1 b
result=[invalid argument [b] (duplicate)]
divide 1 by 0
result=[division by zero]
end

You can monitor network traffic by requesting a "verbose" execution:

dos>testClientSOAP http://localhost/operations/operations.asmx verbose
Type your commands in the following format: [add|subtract|multiply|divide] a b

add 4 8
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/add"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 08:15:25 GMT
<-- X-Powered-By: ASP.NET
<--
--> <?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>
<add xmlns="st.istia.univ-angers.fr">
<a>4</a>
<b>8</b>
</add>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 08:15:25 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 346
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>12</ajouterResult></ajouterResponse></soap:Body></soap:Envelope>
result=12
end

Now let's build a graphical client.

10.7.3. A Windows graphical client

We will now query our web service using a graphical client that will also use the clientSOAP class. The graphical interface will be as follows:

The controls are as follows:

No.
Type
name
role
1
TextBox
txtURI
the URI of the web service operations
2
Button
btnOpen
opens the connection to the web service
3
Button
btnClose
closes the connection to the web service
4
ComboBox
cmbFunctions
the list of functions (add, subtract, multiply, divide)
5
TextBox
txtA
the argument for functions
6
TextBox
txtB
argument b of the functions
7
TextBox
txtResult
the result of function(a,b)
8
Button
btnCalculate
starts the calculation of function(a,b)
9
TextBox
txtError
displays a message about the connection status

There are a few operational constraints:

  • The btnOpen button is only active if the txtURI field is not empty and a connection is not already open
  • The btnClose button is only active when a connection to the web service has been opened
  • The btnCalculate button is active only when a connection is open and the txtA and txtB fields are not empty
  • The txtResult and txtError fields have the ReadOnly attribute set to true

The client begins by opening the connection to the web service using the [Open] button:

Image

Next, the user can select a function and values for a and b:

Image

Image

Image

Image

Image

The application code follows. We have omitted the form code, which is not relevant here.


'namespaces
Imports System
Imports System.Windows.Forms

' the form class
Public Class FormClientSOAP
    Inherits System.Windows.Forms.Form

    ' instance attributes
    Dim client As clientSOAP    ' SOAP client for the operations web service

#Region " Code generated by Windows Form Designer "

    Public Sub New()
        MyBase.New()
        'This call is required by the Windows Form Designer.
        InitializeComponent()
        ' other initializations
        myInit()
    End Sub

    'The form's overridden Dispose method to clean up the component list.
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
    End Sub

...

    Private Sub InitializeComponent()
....
    End Sub

#End Region


    Private Sub myInit()
        ' initialize form
        cmbFunctions.SelectedIndex = 0
        btnOpen.Enabled = False
        btnClose.Enabled = True
        btnCalculate.Enabled = False
    End Sub

    Private Sub txtURI_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtURI.TextChanged
        ' The content of the text box has changed - set the state of the Open button
        btnOpen.Enabled = txtURI.Text.Trim <> ""
    End Sub

    Private Sub btnOpen_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOpen.Click
        ' Request to open a connection to the web service
        Try
            ' Create a [clientSOAP] object
            client = New clientSOAP(txtURI.Text.Trim, False)
            ' button states
            btnOpen.Enabled = False
            btnClose.Enabled = True
            'The URI can no longer be modified
            txtURI.ReadOnly = True
            ' client status
            txtError.Text = "Connection to the web service open"
        Catch ex As Exception
            ' An error occurred - display it
            txtError.Text = ex.Message
        End Try
    End Sub

    Private Sub btnClose_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnClose.Click
        ' Close the connection to the web service
        client.Close()
        ' button states
        btnOpen.Enabled = True
        btnClose.Enabled = False
        ' URI
        txtURI.ReadOnly = False
        ' client status
        txtError.Text = "Connection to the web service closed"
    End Sub

    Private Sub btnCalculate_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnCalculate.Click
        ' Calculate a function f(a,b)
        ' Clear the previous result
        txtResult.Text = ""
        Try
            txtResult.Text = client.executeFunction(cmbFunctions.Text, txtA.Text.Trim, txtB.Text.Trim)
        Catch ex As Exception
            ' A network error occurred
            txtError.Text = ex.Message
            ' closing the connection
            btnClose_Click(Nothing, Nothing)
        End Try
    End Sub

    Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
        ' Change the value of A
        btnCalculate.Enabled = txtA.Text.Trim <> "" And txtB.Text.Trim <> ""
    End Sub

    Private Sub txtB_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtB.TextChanged
        ' Change in the value of B
        txtA_TextChanged(Nothing, Nothing)
    End Sub

    ' main method
    Public Shared Sub main()
        Application.Run(New FormClientSOAP)
    End Sub
End Class

Once again, the clientSOAP class hides all network-related aspects of the application. The application was built as follows:

  • The clientSOAP.dll assembly containing the clientSOAP class was placed in the project folder
  • The clientsoapgui.vb GUI was built with VS.NET and then compiled in a DOS window:
dos>vbc /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll /r:clientSOAP.dll clientsoapgui.vb

dos>dir
03/04/2004  09:13                7 168 clientSOAP.dll
03/04/2004  16:44                9,866 clientsoapgui.vb
03/04/2004  16:44               11,264 clientsoapgui.exe

The graphical interface was then launched by:

dos>clientsoapgui

10.8. A proxy client

Let’s recap what we’ve just done. We created an intermediate class that encapsulates network exchanges between a client and a web service according to the diagram below:

The .NET platform takes this logic a step further. Once we know the Web service to access, we can automatically generate the class that will act as an intermediary to access the Web service’s functions and hide the entire network layer. This class is called a proxy for the Web service for which it was generated.

How do you generate a web service proxy class? A web service is always accompanied by a description file in XML format. If the URI of our web service operations is http://localhost/operations/operations.asmx, its description file is available at the URL http://localhost/operations/operations.asmx?wsdl, as shown in the following screenshot:

Image

This is an XML file that precisely describes all the functions of the web service, including for each one the type and number of parameters, and the type of the result. This file is called the service’s WSDL file because it uses the WSDL (Web Services Description Language). From this file, a proxy class can be generated using the wsdl tool:


dos>wsdl http://localhost/operations/operations.asmx?wsdl /language=vb
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Writing file 'D:\data\devel\vbnet\poly\chap9\clientproxy\operations.vb'.

dos>dir
03/04/2004  17:17                6,663 operations.vb

The wsdl tool generates a VB.NET source file (option /language=vb) named after the class implementing the web service, in this case operations. Let's examine part of the generated code:

'------------------------------------------------------------------------------
' <autogenerated>
'     This code was generated by a tool.
'     Runtime Version: 1.1.4322.573
'
'     Changes to this file may cause incorrect behavior and will be lost if 
'     the code is regenerated.
' </autogenerated>
'------------------------------------------------------------------------------

Option Strict Off
Option Explicit On

Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Xml.Serialization

'
'This source code was automatically generated by wsdl, Version=1.1.4322.573.
'

'<remarks/>
<System.Diagnostics.DebuggerStepThroughAttribute(),  _
 System.ComponentModel.DesignerCategoryAttribute("code"),  _
 System.Web.Services.WebServiceBindingAttribute(Name:="operationsSoap", [Namespace]:="st.istia.univ-angers.fr")>  _
Public Class operations
    Inherits System.Web.Services.Protocols.SoapHttpClientProtocol

    '<remarks/>
    Public Sub New()
        MyBase.New
        Me.Url = "http://localhost/operations/operations.asmx"
    End Sub

    '<remarks/>
    <System.Web.Services.Protocols.SoapDocumentMethodAttribute("st.istia.univ-angers.fr/ajouter", RequestNamespace:="st.istia.univ-angers.fr", ResponseNamespace:="st.istia.univ-angers.fr", Use:=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle:=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)>  _

    Public Function add(ByVal a As Double, ByVal b As Double) As Double
        Dim results() As Object = Me.Invoke("add", New Object() {a, b})
        Return CType(results(0), Double)
    End Function

    '<remarks/>
    Public Function BeginAdd(ByVal a As Double, ByVal b As Double, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult
        Return Me.BeginInvoke("add", New Object() {a, b}, callback, asyncState)
    End Function

    '<remarks/>
    Public Function EndAdd(ByVal asyncResult As System.IAsyncResult) As Double
        Dim results() As Object = Me.EndInvoke(asyncResult)
        Return CType(results(0), Double)
    End Function
....

This code may seem a bit complex at first glance. We don’t need to understand the details to be able to use it. Let’s first examine the class declaration:

Public Class operations
    Inherits System.Web.Services.Protocols.SoapHttpClientProtocol

The class bears the name operations of the web service for which it was built. It derives from the SoapHttpClientProtocol class:

Image

Our proxy class has a single constructor:

    Public Sub New()
        MyBase.New
        Me.Url = "http://localhost/operations/operations.asmx"
    End Sub

The constructor assigns the URL of the web service associated with the proxy to the url attribute. The operations class above does not define the url attribute itself. It is inherited from the class from which the proxy derives: System.Web.Services.Protocols.SoapHttpClientProtocol. Let’s now examine what relates to the add method:

    Public Function add(ByVal a As Double, ByVal b As Double) As Double
        Dim results() As Object = Me.Invoke("add", New Object() {a, b})
        Return CType(results(0), Double)
    End Function

We can see that it has the same signature as in the operations Web service, where it was defined as follows:

      <WebMethod>  _
      Function add(a As Double, b As Double) As Double
         Return a + b
      End Function 'add

The way this class communicates with the web service is not shown here. This communication is entirely handled by the parent class System.Web.Services.Protocols.SoapHttpClientProtocol. The proxy contains only what distinguishes it from other proxies:

  • the URL of the associated web service
  • the definition of the associated service's methods.

To use the methods of the operations web service, a client only needs the operations proxy class generated earlier. Let’s compile this class into an assembly file:

dos>vbc /t:library /r:system.web.services.dll /r:system.xml.dll /r:system.dll operations.vb
dos>dir
03/04/2004  17:17                6,663 operations.vb
04/03/2004  17:24                7 680 operations.dll

Now let's write a console client. It is called without parameters and executes the requests typed on the keyboard:

dos>testclientproxy
Enter your commands in the following format: [add|subtract|multiply|divide|doall] a b

add 4 5
result=9
subtract 9 8
result=1
multiply 10 4
result=40
divide 6 by 7
result=0.857142857142857
do-it-all 10 20
results=[30,-10,200,0,5]
divide 5 0
result=+Infinity
end

The client's code is as follows:


' namespaces
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic

Public Module testClientProxy

    ' interactively executes commands typed on the keyboard
    ' and sends them to the operations web service
    Public Sub Main()
        ' there are no more arguments—the web service URL is hardcoded in the proxy

        ' creates a dictionary of web service functions
        Dim functions As String() = {"add", "subtract", "multiply", "divide", "doall"}
        Dim functionDictionary As New Hashtable
        Dim i As Integer
        For i = 0 To functions.Length - 1
            dicoFonctions.Add(functions(i), True)
        Next i

        ' Create a proxy object named operations 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' connection error
            error("The following error occurred while connecting to the web service proxy: " + ex.Message, 2)
        End Try

        ' user requests are typed on the keyboard
        ' in the form function a b - they end with the end command
        Dim command As String = Nothing        ' command typed on the keyboard
        Dim fields As String() = Nothing        ' fields of a command line

        ' prompt to the user
        Console.Out.WriteLine("Enter your commands in the following format: [add|subtract|multiply|divide|doall] a b" + ControlChars.Lf)

        ' some local data
        Dim commandError As Boolean
        Dim function As String
        Dim a, b As Double
        ' loop for capturing keyboard commands
        While True
            ' initially no error
            commandError = False
            ' read command
            command = Console.In.ReadLine().Trim().ToLower()
            ' finished?
            If command Is Nothing Or command = "end" Then
                Exit While
            End If
            ' Split the command into fields
            fields = Regex.Split(command, "\s+")
            Try
                ' three fields are required
                If fields.Length <> 3 Then
                    Throw New Exception
                End If
                ' Field 0 must be a recognized function
                function = fields(0)
                If Not functionDict.ContainsKey(function) Then
                    Throw New Exception
                End If
                ' Field 1 must be a valid number
                a = Double.Parse(fields(1))
                ' Field 2 must be a valid number
                b = Double.Parse(fields(2))
            Catch
                ' invalid command
                Console.Out.WriteLine("syntax: [add|subtract|multiply|divide] a b")
                commandError = True
            End Try
            ' Send the request to the web service
            If Not commandError Then
                Try
                    Dim result As Double
                    Dim results() As Double
                    If function = "add" Then
                        result = myOperations.add(a, b)
                        Console.Out.WriteLine(("result=" + result.ToString))
                    End If
                    If function = "subtract" Then
                        result = myOperations.subtract(a, b)
                        Console.Out.WriteLine(("result=" + result.ToString))
                    End If
                    If function = "multiply" Then
                        result = myOperations.multiply(a, b)
                        Console.Out.WriteLine(("result=" + result.ToString))
                    End If
                    If function = "divide" Then
                        result = myOperations.divide(a, b)
                        Console.Out.WriteLine(("result=" + result.ToString))
                    End If
                    If function = "do-it-all" Then
                        results = myOperations.doAll(a, b)
                        Console.Out.WriteLine(("results=[" + results(0).ToString + "," + results(1).ToString + "," + _
                        results(2).ToString + "," + results(3).ToString + "]"))
                    End If
                Catch e As Exception
                    Console.Out.WriteLine(("The following error occurred: " + e.Message))
                End Try
            End If
        End While
    End Sub

    ' Display errors
    Public Sub error(ByVal msg As String, ByVal exitCode As Integer)
        ' display error
        System.Console.Error.WriteLine(msg)
        ' Exit with error
        Environment.Exit(exitCode)
    End Sub
End Module

We are only examining the code specific to using the proxy class. First, a proxy operations object is created:


        ' create a proxy operations object 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' connection error
            error("The following error occurred while connecting to the web service proxy: " + ex.Message, 2)
        End Try

Lines a and b are typed on the keyboard. Based on this information, the appropriate proxy methods are called:


            ' make the request to the web service
            If Not commandError Then
                Try
                    Dim result As Double
                    Dim results() As Double
                    If function = "add" Then
                        result = myOperations.add(a, b)
                        Console.Out.WriteLine(("result=" + result.ToString))
                    End If
                    If function = "subtract" Then
                        result = myOperations.subtract(a, b)
                        Console.Out.WriteLine(("result=" + result.ToString))
                    End If
                    If function = "multiply" Then
                        result = myOperations.multiply(a, b)
                        Console.Out.WriteLine(("result=" + result.ToString))
                    End If
                    If function = "divide" Then
                        result = myOperations.divide(a, b)
                        Console.Out.WriteLine(("result=" + result.ToString))
                    End If
                    If function = "do-it-all" Then
                        results = myOperations.doAll(a, b)
                        Console.Out.WriteLine(("results=[" + results(0).ToString + "," + results(1).ToString + "," + _
                        results(2).ToString + "," + results(3).ToString + "]"))
                    End If
                Catch e As Exception
                    Console.Out.WriteLine(("The following error occurred: " + e.Message))
                End Try

Here, we are dealing for the first time with the all-in-one operation that performs all four operations. It had been ignored until now because it returns an array of numbers encapsulated in an XML wrapper that is more complicated to handle than the simple XML responses from the other functions, which return only a single result. Here, with the proxy class, we see that using the all-in-one method is no more complicated than using the other methods. The application was compiled in a DOS window as follows:

dos>vbc /r:operations.dll /r:system.dll /r:system.web.services.dll testClientProxy.vb

dos>dir
03/04/2004  17:17                6,663 operations.vb
03/04/2004  17:24                7,680 operations.dll
03/04/2004  17:41                4,099 testClientProxy.vb
03/04/2004  5:41 PM                5,632 testClientProxy.exe

10.9. Configuring a Web Service

A web service may require configuration information to initialize correctly. With IIS, this information can be placed in a file called web.config located in the same folder as the web service. Suppose we want to create a web service that requires two pieces of information to initialize: a name and an age. These two pieces of information can be placed in the web.config file in the following format:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
  <appSettings>
    <add key="name" value="tintin"/>
    <add key="age" value="27"/>
  </appSettings>   
</configuration>

The initialization settings are placed in an XML container:

<configuration>
    <appSettings>
...
    </appSettings>
</configuration>

An initialization parameter named P with the value V will be declared with the line:


        <add key="P" value="V"/>

How does the web service retrieve this information? When IIS loads a web service, it checks to see if there is a web.config file in the same folder. If so, it reads it. The value V of a parameter P is obtained using the statement:

        String P = ConfigurationSettings.AppSettings["V"];

where ConfigurationSettings is a class in the System.Configuration namespace.

Let's test this technique on the following web service:


<%@ WebService language="VB" class=personne %>

Imports System.Web.Services
imports System.Configuration

<WebService([Namespace] := "st.istia.univ-angers.fr")> _
Public Class person
   Inherits WebService

   ' attributes
   Private name As String
   Private age As Integer

   ' constructor
   Public Sub New()
      ' Initialize attributes
      name = ConfigurationSettings.AppSettings("name")
      age = Integer.Parse(ConfigurationSettings.AppSettings("age"))
   End Sub

   <WebMethod>  _
   Function id() As String
      Return "[" + name + "," + age.ToString + "]"
   End Function 

End Class 

The Person web service has two attributes, name and age, which are initialized in its parameterless constructor using values read from the Person service's web.config configuration file. This file is as follows:


<configuration>
    <appSettings>
        <add key="name" value="tintin"/>
        <add key="age" value="27"/>
    </appSettings>
</configuration>

The web service also has a <WebMethod> with no parameters that simply returns the name and age attributes. The service is registered in the source file personne.asmx, which is located along with its configuration file in the folder c:\inetpub\wwwroot\st\personne:

dos>dir
03/09/2004  08:25               632 personne.asmx
03/09/2004  08:08               186 web.config

Let’s associate a virtual IIS folder /config with the physical folder above. Start IIS, then use a browser to request the URL http://localhost/config/personne.asmx for the person service:

Image

Follow the link for the single id method:

Image

The id method has no parameters. Let’s use the Call button:

Image

We have successfully retrieved the information stored in the service’s web.config file.

10.10. The tax calculation Web Service

We’ll revisit the now-familiar IMPOTS application. The last time we worked with it, we turned it into a remote server that could be accessed over the internet. We’ll now turn it into a web service.

10.10.1. The Web Service

We’ll start with the impôt class created in the chapter on databases, which is built from information contained in an ODBC database:


' options
Option Strict On
Option Explicit On 

' namespaces
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections

Public Class tax
    ' The data required to calculate the tax
    ' comes from an external source
    Private limits(), coeffR(), coeffN() As Decimal

    ' constructor
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' Check that the three arrays are the same size
        Dim OK As Boolean = LIMITS.Length = COEFFR.Length And LIMITS.Length = COEFFN.Length
        If Not OK Then
            Throw New Exception("The three arrays provided do not have the same size(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
        End If
        ' All good
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub

    ' constructor 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' initializes the three limit arrays, coeffR, and coeffN based on
        ' the contents of the Timpots table in the ODBC database DSNimpots
        ' colLimits, colCoeffR, and colCoeffN are the three columns of this table
        ' may throw an exception
        Dim connectString As String = "DSN=" + DSNimpots + ";"        ' connection string to the database
        Dim impotsConn As OdbcConnection = Nothing        ' the connection
        Dim sqlCommand As OdbcCommand = Nothing        ' the SQL command
        ' the SELECT query
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' arrays to retrieve the data
        Dim tLimits As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' attempt to access the database
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' Create a command object
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' execute the query
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' Process the retrieved table
        While myReader.Read()
            ' the data from the current row is placed in the arrays
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' Release resources
        myReader.Close()
        taxConn.Close()

        ' dynamic arrays are converted to static arrays
        Me.limits = New Decimal(tLimits.Count) {}
        Me.coeffR = New Decimal(tLimites.Count) {}
        Me.coeffN = New Decimal(tLimites.Count) {}
        Dim i As Integer
        For i = 0 To tLimits.Count - 1
            limits(i) = Decimal.Parse(tLimits(i).ToString())
            coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
            coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
        Next i
    End Sub

    ' tax calculation
    Public Function calculate(ByVal married As Boolean, ByVal numberOfChildren As Integer, ByVal salary As Integer) As Long
        ' Calculate the number of shares
        Dim nbShares As Decimal
        If married Then
            nbParts = CDec(nbChildren) / 2 + 2
        Else
            nbParts = CDec(nbChildren) / 2 + 1
        End If
        If nbChildren >= 3 Then
            nbParts += 0.5D
        End If
        ' Calculate taxable income & Family Quotient
        Dim income As Decimal = 0.72D * salary
        Dim QF As Decimal = income / nbParts
        ' Calculate tax
        limits((limits.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limits(i)
            i += 1
        End While
        ' return result
        Return CLng(revenue * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

In the web service, only a parameterless constructor can be used. Therefore, the class constructor will become the following:


    ' constructor
    Public Sub New()
        ' initializes the three arrays limits, coeffR, and coeffN based on
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' may throw an exception

        ' retrieves the service configuration settings
        Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
        Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
        Dim colLimits As String = ConfigurationSettings.AppSettings("COL_LIMITS")
        Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
        Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")

        ' query the database
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' database connection string

The five parameters of the constructor from the previous class are now read from the service's web.config file. The code in the impots.asmx source file is as follows. It includes most of the previous code. We have simply wrapped the portions of code specific to the web service:


<%@ WebService language="VB" class=impots %>

' creation of an impots web service
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Imports System.Configuration
Imports System.Web.Services

<WebService([Namespace]:="st.istia.univ-angers.fr")> _
Public Class tax
    Inherits WebService

    ' The data required to calculate the tax
    ' comes from an external source
    Private limits(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""


    ' constructor
    Public Sub New()
        ' initializes the three arrays limits, coeffR, and coeffN based on
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' may throw an exception

        ' retrieves the service configuration settings
        Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
        Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
        Dim colLimits As String = ConfigurationSettings.AppSettings("COL_LIMITS")
        Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
        Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")

        ' query the database
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' database connection string
        Dim impotsConn As OdbcConnection = Nothing     ' the connection
        Dim sqlCommand As OdbcCommand = Nothing     ' the SQL command
        Dim myReader As OdbcDataReader     ' ODBC data reader

        ' SELECT query
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots

        ' arrays to retrieve the data
        Dim tLimits As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' attempt to access the database
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' create a command object
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' execute the query
            myReader = sqlCommand.ExecuteReader()
            ' Working with the retrieved table
            While myReader.Read()
                ' the data from the current row is placed in the arrays
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' release resources
            myReader.Close()
            taxConn.Close()

            ' dynamic arrays are converted to static arrays
            Me.limits = New Decimal(tLimits.Count) {}
            Me.coeffR = New Decimal(tLimites.Count) {}
            Me.coeffN = New Decimal(tLimites.Count) {}
            Dim i As Integer
            For i = 0 To tLimits.Count - 1
                limits(i) = Decimal.Parse(tLimits(i).ToString())
                coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
                coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
            Next i
            ' OK
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' error
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub

    ' Calculate tax
    <WebMethod()> _
    Function calculate(ByVal married As Boolean, ByVal numberOfChildren As Integer, ByVal salary As Integer) As Long
        ' Calculate the number of shares
        Dim nbShares As Decimal
        If married Then
            nbParts = CDec(nbChildren) / 2 + 2
        Else
            nbParts = CDec(nbChildren) / 2 + 1
        End If
        If nbChildren >= 3 Then
            nbParts += 0.5D
        End If
        ' Calculate taxable income & Family Quotient
        Dim income As Decimal = 0.72D * salary
        Dim QF As Decimal = income / nbParts
        ' calculate tax
        limits((limits.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limits(i)
            i += 1
        End While
        ' return result
        Return CLng(revenue * coeffR(i) - nbParts * coeffN(i))
    End Function

    ' id
    <WebMethod()> _
    Function id() As String
        ' to check if everything is OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

Let's explain the few changes made to the impots class beyond those necessary to turn it into a web service:

  • Reading the database in the constructor may fail. So we have added two attributes to our class and a method:
    • the boolean OK is true if the database could be read, false otherwise
    • The string `errMessage` contains an error message if the database could not be read.
    • The parameterless id method retrieves the values of these two attributes.
  • To handle any potential database access errors, the portion of the constructor’s code related to this access has been enclosed in a try-catch block.

The web.config file for the service configuration is as follows:


<configuration>
    <appSettings>
        <add key="DSN" value="mysql-impots" />
        <add key="TABLE" value="timpots" />
        <add key="COL_LIMITES" value="limites" />
        <add key="COL_COEFFR" value="coeffr" />
        <add key="COL_COEFFN" value="coeffn" />
    </appSettings>
</configuration>

When first attempting to load the impots service, the compiler reported that it could not find the Microsoft.Data.Odbc namespace used in the directive:

Imports Microsoft.Data.Odbc

After consulting the documentation

  • a compilation directive was added to web.config to specify that the Microsoft.Data.odbc assembly should be used
  • a copy of the microsoft.data.odbc.dll file was placed in the project’s bin folder. This folder is systematically searched by the web service compiler when it looks for an “assembly.”

Other solutions seem possible but have not been explored here. The configuration file has therefore become:


<configuration>
    <appSettings>
        <add key="DSN" value="mysql-impots" />
        <add key="TABLE" value="timpots" />
        <add key="COL_LIMITES" value="limites" />
        <add key="COL_COEFFR" value="coeffr" />
        <add key="COL_COEFFN" value="coeffn" />
    </appSettings>
    <system.web>
        <compilation>
            <assemblies>
                <add assembly="Microsoft.Data.Odbc" />
            </assemblies>
        </compilation>
    </system.web>
</configuration>

The contents of the impots\bin folder:

dos>dir impots\bin
01/30/2002  02:02           327,680 Microsoft.Data.Odbc.dll

The service and its configuration file have been placed in impots:

dos>dir impots
03/09/2004  10:13             4,669 impots.asmx
03/09/2004  10:19               431 web.config

The physical folder for the web service has been mapped to the virtual folder /impots in IIS. The service page is then as follows:

Image

If you follow the id link:

Image

If you use the Call button:

Image

The previous result displays the values of the OK (true) and errMessage ("") attributes. In this example, the database was loaded successfully. This hasn't always been the case, which is why we added the id method to access the error message. The error was that the database DSN name had been defined as a user DSN when it should have been defined as a system DSN. This distinction is made in the 32-bit ODBC Source Manager:

Let’s go back to the service page:

Image

Let’s follow the “Calculate” link:

Image

We define the call parameters and execute the call:

Image

The result is correct.

10.10.2. Generate the proxy for the impots service

Now that we have a working impots web service, we can generate its proxy class. Remember that this will be used by client applications to access the impots web service transparently. First, we use the wsdl utility to generate the source file for the proxy class, which is then compiled into a DLL.

dos>wsdl /language=vb http://localhost/impots/impots.asmx
Microsoft® Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Writing file 'D:\data\serge\devel\vbnet\poly\chap9\impots\impots.vb'.

D:\data\serge\devel\vbnet\poly\chap9\impots>dir
03/09/2004  10:20    <REP>          bin
03/09/2004  10:58             4,651 impots.asmx
03/09/2004  11:05             3,364 impots.vb
03/09/2004  10:19               431 web.config

dos>vbc /t:library /r:system.dll /r:system.web.services.dll /r:system.xml.dll impots.vb
Microsoft® Visual Basic .NET Compiler version 7.10.3052.4
for Microsoft® .NET Framework version 1.1.4322.573
Copyright (C) Microsoft Corporation 1987-2002. All rights reserved.

dos>dir
03/09/2004  10:20    <REP>          bin
03/09/2004  10:58             4,651 impots.asmx
03/09/2004  11:09             5,120 impots.dll
03/09/2004  11:05             3,364 impots.vb
03/09/2004  10:19               431 web.config

10.10.3. Using the proxy with a client

In the chapter on databases, we created a console application to calculate taxes:

dos>dir
02/27/2004  16:56             5 120 impots.dll
02/27/2004  5:12 PM             3,586 impots.vb
02/27/2004  5:08 PM             6,144 testimpots.exe
02/27/2004  5:18 PM             3,328 testimpots.vb

dos>testimpots
pg DSNimpots tabImpots colLimits colCoeffR colCoeffN

dos>testimpots odbc-mysql-dbimpots taxes limits coeffr coeffn
Tax calculation parameters in married format nbChildren salary or nothing to stop :o 2 200000
tax=22504 F

The testimpots program then used the standard tax class contained in the impots.dll file. The code for the testimpots.vb program was as follows:


Option Explicit On 
Option Strict On

' namespaces
Imports System
Imports Microsoft.VisualBasic

' test page
Module testimports
    Sub Main(ByVal arguments() As String)
        ' Interactive tax calculation program
        ' The user enters three pieces of information via the keyboard: marital status, number of children, salary
        ' the program then displays the tax due
        Const syntax1 As String = "pg DSNimpots tabImpots colLimits colCoeffR colCoeffN"
        Const syntax2 As String = "syntax: married noChildren salary" + ControlChars.Lf + "married: o for married, n for unmarried" + ControlChars.Lf + "noChildren: number of children" + ControlChars.Lf + "salary: annual salary in F"

        ' Checking program parameters
        If arguments.Length <> 5 Then
            ' error message
            Console.Error.WriteLine(syntax1)
            ' end
            Environment.Exit(1)
        End If        'if
        ' retrieve the arguments
        Dim DSNimpots As String = arguments(0)
        Dim taxTab As String = arguments(1)
        Dim colLimits As String = arguments(2)
        Dim colCoeffR As String = arguments(3)
        Dim colCoeffN As String = arguments(4)

        ' Create a tax object
        Dim taxObj As Tax = Nothing
        Try
            taxObj = New Tax(TaxDSN, TaxTab, LimitCol, CoeffRCol, CoeffNCol)
        Catch ex As Exception
            Console.Error.WriteLine(("The following error occurred: " + ex.Message))
            Environment.Exit(2)
        End Try

        ' infinite loop
        While True
            ' initially no errors
            Dim error As Boolean = False

            ' Requesting tax calculation parameters
            Console.Out.Write("Tax calculation parameters in the format: married, number of children, salary, or 'nothing' to exit:")
            Dim parameters As String = Console.In.ReadLine().Trim()

            ' anything to do?
            If parameters Is Nothing Or parameters = "" Then
                Exit While
            End If

            ' Check the number of arguments in the entered line
            Dim args As String() = parameters.Split(Nothing)
            Dim nbParameters As Integer = args.Length
            If nbParameters <> 3 Then
                Console.Error.WriteLine(syntax2)
                error = True
            End If
            Dim husband As String
            Dim nbChildren As Integer
            Dim salary As Integer
            If Not error Then
                ' Checking the validity of the
                ' married
                married = args(0).ToLower()
                If married <> "o" And married <> "n" Then
                    Console.Error.WriteLine((syntax2 + ControlChars.Lf + "Invalid 'married' argument: enter 'y' or 'n'")
                    error = True
                End If
                ' nbChildren
                nbChildren = 0
                Try
                    nbChildren = Integer.Parse(args(1))
                    If nbChildren < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntax2 + "\nInvalid nbChildren argument: enter a positive integer or zero")
                    error = True
                End Try
                ' salary
                salary = 0
                Try
                    salary = Integer.Parse(args(2))
                    If salary < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntax2 + "\nInvalid salary argument: enter a positive integer or zero")
                    error = True
                End Try
            End If
            If Not error Then
                ' parameters are correct - calculate the tax
                Console.Out.WriteLine(("tax=" & objTax.calculate(married = "o", numChildren, salary).ToString + " F"))
            End If
        End While
    End Sub
End Module

We’ll use the same program to now have it use the impots web service through the impots proxy class created earlier. We have to modify the code slightly:

  • whereas the original tax class had a constructor with five arguments, the tax proxy class has a constructor with no parameters. As we have seen, the five parameters are now set in the web service configuration file.
  • therefore, there is no longer any need to pass these five parameters as arguments to the test program

The new code is as follows:


Imports System
Imports Microsoft.VisualBasic

' test page
Module testimpots

    Public Sub Main(ByVal arguments() As String)
        ' Interactive tax calculation program
        ' the user enters three pieces of data via the keyboard: married, numberOfChildren, salary
        ' the program then displays the tax due
        Const syntax2 As String = "syntax: married numberOfChildren salary" + ControlChars.Lf + "married: o for married, n for unmarried" + ControlChars.Lf + "numberOfChildren: number of children" + ControlChars.Lf + "salary: annual salary in F"

        ' Create a tax object
        Dim taxObj As Tax = Nothing
        Try
            taxObj = New tax
        Catch ex As Exception
            Console.Error.WriteLine(("The following error occurred: " + ex.Message))
            Environment.Exit(2)
        End Try

        ' infinite loop
        If Error Then As Boolean
        While True
            ' initially no error
            error = False
            ' Request the parameters for the tax calculation
            Console.Out.Write("Tax calculation parameters in the format: married, number of children, salary, or 'nothing' to exit:")
            Dim parameters As String = Console.In.ReadLine().Trim()
            ' anything to do?
            If parameters Is Nothing Or parameters = "" Then
                Exit While
            End If
            ' Check the number of arguments in the entered line
            Dim args As String() = parameters.Split(Nothing)
            Dim numParameters As Integer = args.Length
            If nbParameters <> 3 Then
                Console.Error.WriteLine(syntax2)
                error = True
            End If
            If Not error Then
                ' Checking the validity of the parameters
                ' groom
                Dim married As String = args(0).ToLower()
                If married <> "o" And married <> "n" Then
                    Console.Error.WriteLine((syntax2 + ControlChars.Lf + "Invalid 'married' argument: enter 'o' or 'n'")
                    error = True
                End If
                ' nbEnfants
                Dim nbEnfants As Integer = 0
                Try
                    nbChildren = Integer.Parse(args(1))
                    If nbChildren < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine((syntax2 + ControlChars.Lf + "Invalid number of children argument: enter a positive integer or zero"))
                    error = True
                End Try
                ' salary
                Dim salary As Integer = 0
                Try
                    salary = Integer.Parse(args(2))
                    If salary < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine((syntax2 + ControlChars.Lf + "Invalid salary argument: enter a positive integer or zero"))
                    error = True
                End Try
                ' if the parameters are correct - calculate the tax
                If Not error Then Console.Out.WriteLine(("tax=" + objTax.calculate(married = "o", numChildren, salary).ToString + " F"))
            End If
        End While
    End Sub
End Module

We have the impots.dll proxy and the testimpots source code in the same folder.

dos>dir
03/09/2004  11:28    <REP>          bin
03/09/2004  11:09             5,120 impots.dll
03/09/2004  11:34             3,396 testimpots.vb
03/09/2004  10:19               431 web.config

We compile the testimpots.vb source file:

dos>vbc /r:impots.dll /r:microsoft.visualbasic.dll /r:system.web.services.dll /r:system.dll testimpots.vb
dos>dir
03/09/2004  11:28    <REP>          bin
03/09/2004  11:09             5,120 impots.dll
03/09/2004  11:05             3,364 impots.vb
03/09/2004  11:35             5,632 testimpots.exe
03/09/2004  11:34             3,396 testimpots.vb
03/09/2004  10:19               431 web.config

then run it:

dos>testimpots
Tax calculation parameters in married format: number of children, salary, or nothing to stop :o 2 200000
tax=22,504 F

We get the expected result.