Skip to content

10. Servicios web

10.1. Introducción

En el capítulo anterior hemos presentado varias aplicaciones cliente-servidor TCP/IP. Dado que los clientes y el servidor intercambian líneas de texto, pueden estar escritas en cualquier lenguaje. El cliente solo debe conocer el protocolo de comunicación que espera el servidor. Los servicios web son aplicaciones de servidor TCP/IP que presentan las siguientes características:

  • Están alojados en servidores web y, por lo tanto, el protocolo de intercambio cliente-servidor es HTTP (HyperText Transport Protocol), un protocolo que se ejecuta sobre TCP-IP.
  • El servicio web tiene un protocolo de diálogo estándar independientemente del servicio prestado. Un servicio web ofrece diversos servicios S1, S2, ..., Sn. Cada uno de ellos espera los parámetros proporcionados por el cliente y le devuelve un resultado. Para cada servicio, el cliente necesita saber:
    • el nombre exacto del servicio Si
    • la lista de parámetros que debe proporcionar y su tipo
    • el tipo de resultado devuelto por el servicio

Una vez conocidos estos elementos, el diálogo cliente-servidor sigue el mismo formato independientemente del servicio web consultado. De este modo, la comunicación de los clientes queda estandarizada.

  • Por razones de seguridad frente a los ataques procedentes de Internet, muchas organizaciones disponen de redes privadas y solo abren al exterior determinados puertos de sus servidores: principalmente el puerto 80 del servicio web. Todos los demás puertos están bloqueados. Por lo tanto, las aplicaciones cliente-servidor tal y como se presentan en el capítulo anterior se construyen dentro de la red privada (intranet) y, por lo general, no son accesibles desde el exterior. Alojar un servicio en un servidor web lo hace accesible a toda la comunidad de Internet.
  • El servicio web puede modelarse como un objeto remoto. Los servicios ofrecidos se convierten entonces en métodos de este objeto. Un cliente puede acceder a este objeto remoto como si fuera local. Esto oculta toda la parte de comunicación de red y permite construir un cliente independiente de esta capa. Si esta cambia, no es necesario modificar el cliente. Esta es una enorme ventaja y probablemente el principal punto fuerte de los servicios web.
  • Al igual que en las aplicaciones cliente-servidor TCP/IP presentadas en el capítulo anterior, el cliente y el servidor pueden estar escritos en cualquier lenguaje. Intercambian líneas de texto. Estas constan de dos partes:
    • los encabezados necesarios para el protocolo HTTP
    • el cuerpo del mensaje. En el caso de una respuesta del servidor al cliente, este tiene el formato XML (eXtensible Markup Language). En el caso de una solicitud del cliente al servidor, el cuerpo del mensaje puede adoptar varias formas, entre ellas XML. La solicitud XML del cliente puede tener un formato específico denominado SOAP (Simple Object Access Protocol). En este caso, la respuesta del servidor también sigue el formato SOAP.

10.2. Los navegadores y XML

Los servicios web envían XML a sus clientes. Los navegadores pueden reaccionar de forma diferente al recibir este flujo XML. Internet Explorer tiene una hoja de estilo predefinida que permite mostrarlo. Netscape Communicator no dispone de esta hoja de estilo y no muestra el código XML recibido. Es necesario visualizar el código fuente de la página recibida para acceder al XML. A continuación se muestra un ejemplo para el siguiente código XML:

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

Internet Explorer mostrará la siguiente página:

Image

mientras que Netscape Navigator mostrará:

Image

Si se visualiza el código fuente de la página recibida por Netscape, se obtiene:

Image

Netscape ha recibido lo mismo que Internet Explorer, pero lo ha mostrado de forma diferente. A partir de ahora, utilizaremos Internet Explorer para las capturas de pantalla.

10.3. Un primer servicio web

Vamos a descubrir los servicios web a través de un ejemplo muy sencillo que se presenta en tres versiones.

10.3.1. Versión 1

Para esta primera versión utilizaremos VS.NET, que tiene la ventaja de poder generar un esqueleto de servicio web inmediatamente operativo. Una vez comprendida esta arquitectura, podremos empezar a valernos por nosotros mismos. Este será el objetivo de las siguientes versiones.

Con VS.NET, creemos un nuevo proyecto con la opción [Fichier/Nouveau/Projet]:

Image

Cabe destacar los siguientes puntos:

  • el tipo de proyecto es Visual Basic (cuadro de la izquierda)
  • la plantilla del proyecto es Servicio web ASP.NET (cuadro de la derecha)
  • La ubicación está libre. En este caso, el servicio web se alojará en un servidor web local IIS. Por lo tanto, su dirección será http://localhost/[chemin], donde URL debe definirse. Aquí elegimos la ruta http://localhost/polyvbnet/demo. VS.NET creará entonces una carpeta para este proyecto. ¿Dónde? El servidor IIS tiene una raíz para el árbol de documentos web que sirve. Llamemos a esta raíz <IISroot>. Corresponde a URL http://localhost. De ello se deduce que el URL http://localhost/polyvbnet/demo se asociará a la carpeta <IISroot>/polyvbnet/demo. <IISroot> es normalmente la carpeta \inetpub\wwwroot en el disco donde se instaló IIS. En nuestro ejemplo, es el disco E. Por lo tanto, la carpeta creada por VS.NET es la carpeta e:\inetpub\wwwroot\polyvbnet\demo:

Image

Como siempre, se han creado demasiadas carpetas. No todas son necesarias. Solo explicaremos aquellas que necesitemos en un momento dado. VS.NET ha creado un proyecto:

Image

Encontramos algunos de los archivos presentes en la carpeta física del proyecto. Lo más interesante para nosotros es el archivo con la extensión asmx. Es la extensión de los servicios web. Un servicio web es gestionado por VS.NET como una aplicación de Windows, c.a.d, una aplicación que tiene una interfaz gráfica y código para gestionarla. Por eso, tenemos una ventana de diseño:

Image

Un servicio web no suele tener interfaz gráfica. Representa un objeto al que se puede llamar de forma remota. Posee métodos y las aplicaciones los invocan. Por lo tanto, lo veremos como un objeto clásico con la particularidad de que puede instanciarse de forma remota a través de la red. Por lo tanto, no utilizaremos la ventana de diseño que presenta VS.NET. Centrémonos más bien en el código del servicio utilizando la opción Ver/Código:

Image

Hay varios puntos a destacar:

  • el archivo se llama Service1.asmx.vb y no Service1.asmx. Volveremos sobre el contenido del archivo Service1.asmx un poco más adelante.
  • Encontramos una ventana de código similar a la que teníamos cuando creábamos aplicaciones de Windows con VS.NET

El código generado por VS.NET es el siguiente:


Imports System.Web.Services

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

#Región «Código generado por el Diseñador de servicios web»

    Public Sub New()
        MyBase.New()

        'Esta llamada es requerida por el Diseñador de servicios web.
        InitializeComponent()

        'Añada su código de inicialización después de la llamada InitializeComponent()

    End Sub

    'Requerido por el Diseñador de servicios web
    Private components As System.ComponentModel.IContainer

    'REMARQUE: el Diseñador de servicios web requiere el siguiente procedimiento
    'Se puede modificar utilizando el Diseñador de servicios web.  
    'No lo modifique utilizando el editor de código.
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
    End Sub

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        'CODEGEN: este procedimiento es requerido por el Diseñador de servicios web
        'No lo modifique utilizando el editor de código.
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

#Fin de región

    ' EXEMPLE DE SERVICE WEB
    ' El ejemplo de servicio HelloWorld() devuelve la cadena «Hello World».
    ' Para generar, no comente las siguientes líneas, guarde y genere el proyecto.
    ' Para probar este servicio web, asegúrese de que el archivo .asmx sea la página de inicio
    ' y pulse F5.
    '
    '<WebMethod()> Función pública HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' Fin de la función

End Class

En primer lugar, observemos que aquí tenemos una clase, la clase Service1, que deriva de la clase WebService:

Public Class Service1
    Inherits System.Web.Services.WebService

Esto nos lleva a importar el espacio de nombres System.Web.Services:


Imports System.Web.Services

La declaración de la clase va precedida de un atributo de compilación:


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

El atributo System.Web.Services.WebService() indica que la clase que le sigue es un servicio web. Este atributo admite varios parámetros, entre ellos uno llamado NameSpace. Sirve para ubicar el servicio web en un espacio de nombres. De hecho, es posible que existan varios servicios web llamados «meteo» en todo el mundo. Necesitamos una forma de diferenciarlos. El espacio de nombres es lo que lo permite. Uno podría llamarse [espacenom1].meteo y otro [espacenom2].meteo. Aquí encontramos un concepto análogo a los espacios de nombres de las clases. VS.NET ha generado automáticamente código que ha colocado en una región del código fuente:

#Región «Código generado por el Diseñador de servicios web»

Si observamos este código, vemos el mismo que generaba el diseñador cuando se creaban aplicaciones de Windows. Es un código que podemos eliminar sin más si no tenemos una interfaz gráfica, lo cual será nuestro caso para los servicios web.

La clase termina con un ejemplo de lo que podría ser un servicio web:


#Fin de la región

    ' EXEMPLE DE SERVICE WEB
    ' El ejemplo de servicio HelloWorld() devuelve la cadena «Hello World».
    ' Para generar, no comente las siguientes líneas, guarde y genere el proyecto.
    ' Para probar este servicio web, asegúrese de que el archivo .asmx sea la página de inicio
    ' y pulse F5.
    '
    '<WebMethod()> Función pública HelloWorld() As String
    '    HelloWorld = "Hola mundo"
    ' Fin de la función

Teniendo en cuenta lo anterior, limpiamos el código para que quede así:


Imports System.Web.Services

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

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

Ahora lo vemos un poco más claro.

  • Un servicio web es una clase derivada de la clase WebService
  • La clase está calificada por el atributo <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. Por lo tanto, colocamos nuestro servicio en el espacio de nombres st.istia.univ-angers.fr.
  • Los métodos de la clase se califican mediante un atributo <WebMethod()> que indica que se trata de un método al que se puede llamar de forma remota a través de la red

La clase que proporciona nuestro servicio web se llama, por tanto, Bonjour y tiene un único método, también llamado Bonjour, que devuelve una cadena de caracteres. Estamos listos para una primera prueba.

  • Iniciemos el servidor web IIS si aún no lo hemos hecho
  • utilicemos la opción Depurar/Ejecutar sin depurar. VS.NET

VS.NET compilará entonces toda la aplicación, abrirá un navegador (a menudo Internet Explorer, si está presente) y mostrará la URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

¿Por qué la URL http://localhost/polyvbnet/demo/Service1.asmx? Porque era el único archivo .asmx del proyecto:

Image

Si hubiera habido varios archivos .asmx, habríamos tenido que especificar cuál debía ejecutarse primero. Para ello, hay que hacer clic con el botón derecho del ratón sobre el archivo .asmx en cuestión y seleccionar la opción [Définir comme page de démarrage].

Image

Podría interesarnos saber qué contiene el archivo service1.asmx. De hecho, con VS.NET hemos trabajado en el archivo service1.asmx.vb y no en el archivo service1.asmx. Este archivo se encuentra en la carpeta del proyecto:

Image

Abrámoslo con un editor de texto (Bloc de notas u otro). Obtenemos el siguiente contenido:

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

El archivo contiene una simple directiva dirigida al servidor IIS que indica:

  • que se trata de un servicio web (palabra clave WebService)
  • que el lenguaje de la clase de este servicio es Visual Basic (Language="vb")
  • que el código fuente de esta clase se encuentra en el archivo Service1.asmx.vb (Codebehind="Service1.asmx.vb")
  • que la clase que implementa el servicio se llama demo.Bonjour (Class="demo.Bonjour"). Cabe destacar que VS.NET ha colocado la clase Bonjour en el espacio de nombres demo, que es también el nombre del proyecto.

Volvamos a la página obtenida en la URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

¿Quién ha escrito el código HTML de la página anterior? Nosotros no, eso lo sabemos. Es IIS, que presenta los servicios web de forma estándar. Esta página nos ofrece dos enlaces. Sigamos el primero, [Description du service]:

Image

Ups... es un XML bastante críptico. Fíjate, sin embargo, en el URL

http://localhost/polyvbnet/demo/Service1.asmx?WSDL. Abre un navegador y escribe directamente esta URL. Obtendrás lo mismo que antes. Recordemos, pues, que la URL http://serviceweb?WSDL da acceso a la descripción XML del servicio web. Volvamos a la página de inicio y seleccionemos el enlace [Bonjour]. Recordemos que Bonjour es un método del servicio web. Si hubiéramos tenido varios métodos, todos se habrían mostrado aquí. Obtenemos la siguiente página:

Image

Hemos truncado deliberadamente la página obtenida para no alargar nuestra demostración. Fijémonos de nuevo en la URL obtenida:

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

Si escribimos directamente esta URL en un navegador, obtendremos lo mismo que antes. Se nos anima a utilizar el botón [Appeler]. Hagámoslo. Aparece una nueva página:

Image

De nuevo es XML. Encontramos allí dos datos que estaban presentes en nuestro servicio web:

  • el espacio de nombres st.istia.univ-angers.fr de nuestro servicio

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
  • el valor devuelto por el método Bonjour:

        Return "bonjour !"

¿Qué hemos aprendido?

  • cómo escribir un servicio web S
  • cómo llamarlo

Ahora nos centraremos en escribir un servicio web sin la ayuda de VS.NET.

10.3.2. Versión 2

En el ejemplo anterior, VS.NET hizo muchas cosas por sí solo. ¿Es posible crear un servicio web sin esta herramienta? La respuesta es sí, y ahora lo demostraremos. Con un editor de texto, crearemos el siguiente servicio web:


Imports System.Web.Services

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

    <WebMethod()> Public Function getBonjour() As String
        Return "bonjour de nouveau !"
    End Function
End Class

La clase se llama Bonjour2 y tiene un método que se llama getBonjour. Se ha colocado en el archivo demo2.vb, que a su vez se encuentra en el árbol de directorios del servidor IIS, dentro de la carpeta E:\Inetpub\wwwroot\polyvbnet\demo2. Se trata de una clase VB.NET clásica que, por lo tanto, se puede compilar:

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

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

Colocamos el ensamblado demo2.dll en una carpeta llamada «bin» (este nombre es obligatorio):

dos>dir bin
02/03/2004  18:12                3 072 demo2.dll

Ahora creamos el archivo demo2.asmx. Este es el que llamarán los clientes web. Su contenido es el siguiente:

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

Ya nos hemos encontrado con esta directiva. Indica que:

  • la clase del servicio web se llama Bonjour2 y se encuentra en el ensamblado demo2.dll. IIS buscará este ensamblado en diferentes ubicaciones, en particular en la carpeta bin del servicio web. Por eso hemos colocado allí el ensamblado demo2.dll.

Ahora podemos realizar diversas pruebas. Nos aseguramos de que IIS esté activo y solicitamos con un navegador la URL http://localhost/polyvbnet/demo2/demo2.asmx:

Image

A continuación, la URL http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Luego, URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, donde getBonjour es el nombre del único método de nuestro servicio web:

Image

Utilizamos el botón [Appeler] anterior:

Image

Obtenemos correctamente el resultado de la llamada al método getBonjour del servicio web. Ahora sabemos cómo construir un servicio web sin vs.net. A partir de ahora, dejaremos de lado la forma en que está construido el servicio web para centrarnos únicamente en los archivos fundamentales.

10.3.3. Versión 3

Las dos versiones anteriores del servicio web [Bonjour] utilizaban dos archivos:

  • un archivo .asmx, punto de entrada del servicio web
  • un archivo .vb, código fuente del servicio web

Aquí mostramos que basta con el único archivo .asmx. El código del servicio demo3.asmx es el siguiente:

<%@ 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 "bonjour en version3 !"
    End Function
End Class

Observamos que el código fuente del servicio se encuentra ahora directamente en el archivo fuente del archivo demo3.asmx. La directiva

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

ya no hace referencia a una clase en un ensamblado externo, sino a una clase que se encuentra en el mismo archivo fuente. Coloquemos este en la carpeta <IISroot>\polyvbnet\demo3:

Image

Ejecutemos IIS y solicitemos la URL http://localhost/polyvbnet/demo3/demo3.asmx:

Image

Observamos una diferencia importante con respecto a la versión anterior: no hemos tenido que compilar el código VB del servicio. IIS ha realizado esta compilación por sí mismo mediante el compilador VB.NET instalado en la misma máquina. A continuación, ha generado la página. Si se produce un error de compilación, este será señalado por IIS:

Image

10.3.4. Versión 4

En este caso, nos centramos en la configuración del servidor IIS. Hasta ahora, siempre hemos ubicado nuestros servicios web en el árbol de directorios raíz <IISroot> del servidor IIS, en este caso [e:\inetpub\wwwroot]. Aquí mostramos que podemos colocar el servicio web en cualquier lugar. Esto se hace mediante las carpetas virtuales de IIS. Coloquemos nuestro servicio en la siguiente carpeta:

Image

La carpeta [D:\data\devel\vbnet\poly\chap9\demo3] no se encuentra en el árbol de directorios del servidor IIS. Debemos indicárselo creando una carpeta virtual IIS. Iniciemos IIS y seleccionemos la opción [Avancé] que se muestra a continuación:

Image

Se nos muestra una lista de directorios virtuales. No nos detendremos en ella. Creamos un nuevo directorio virtual con el botón [Ajouter] anterior:

Image

Con el botón [Parcourir], designamos la carpeta física que contiene el servicio web, en este caso la carpeta [D:\data\devel\vbnet\poly\chap9\demo3]. Asignamos un nombre lógico (virtual) a esta carpeta: [virdemo3]. Esto significa que los documentos que se encuentran dentro de la carpeta física [D:\data\devel\vbnet\poly\chap9\demo3] serán accesibles en la red a través de la URL [http://<machine>/virdemo3]. El cuadro de diálogo anterior incluye otros parámetros que dejamos tal cual. Confirmamos el cuadro. La nueva carpeta virtual aparece en la lista de carpetas virtuales de IIS:

Image

Ahora, abrimos un navegador y solicitamos la URL [http://localhost/virdemo3/demo3.asmx]. Obtenemos lo mismo que antes:

Image

10.3.5. Conclusión

Hemos mostrado varias formas de proceder para crear un servicio web. A partir de ahora, utilizaremos el método de la versión 3 para la creación del servicio y el método 4 para su localización. De este modo, no necesitaremos VS.NET. No obstante, cabe destacar el interés de utilizar VS.NET por la ayuda que aporta a la depuración. Existen herramientas gratuitas para desarrollar aplicaciones web, en particular el producto WebMatrix patrocinado por Microsoft, que se puede encontrar en URL [http://www.asp.net/webmatrix]. Es una herramienta excelente para iniciarse en la programación web sin necesidad de inversión previa.

10.4. Un servicio web de operaciones

Consideramos un servicio web que ofrece cinco funciones:

  1. sumar(a,b), que devolverá a+b
  2. restar(a,b), que devolverá a-b
  3. multiplicar(a,b), que devolverá a*b
  4. dividir(a,b), que devolverá a/b
  5. todo(a,b), que devolverá la matriz [a+b,a-b,a*b,a/b]

El código VB.NET de este servicio es el siguiente:


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

imports system.web.services

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

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

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

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

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

Repasamos aquí algunas explicaciones ya dadas, pero que merecen ser recordadas o completadas. La clase operations se parece a una clase VB.NET, aunque hay algunos puntos a tener en cuenta:

  • los métodos van precedidos de un atributo <WebMethod()> que indica al compilador los métodos que deben «publicarse» c.a.d. y ponerse a disposición del cliente. Un método que no vaya precedido de este atributo sería invisible para los clientes remotos. Podría tratarse de un método interno utilizado por otros métodos, pero no destinado a ser publicado.
  • La clase deriva de la clase WebService definida en el espacio de nombres System.Web.Services. Esta herencia no siempre es obligatoria. En este ejemplo concreto, por ejemplo, se podría prescindir de ella.
  • La propia clase va precedida de un atributo <WebService(Namespace="st.istia.univ-angers.fr")> destinado a proporcionar un espacio de nombres al servicio web. Un proveedor de clases asigna un espacio de nombres a sus clases para darles un nombre único y evitar así conflictos con clases de otros proveedores que pudieran tener el mismo nombre. En el caso de los servicios web, ocurre lo mismo. Cada servicio web debe poder identificarse mediante un nombre único, en este caso st.istia.univ-angers.fr.
  • No hemos definido ningún constructor. Por lo tanto, se utilizará implícitamente el constructor de la clase padre.

El código fuente anterior no está destinado directamente al compilador VB.NET, sino al servidor web IIS. Debe llevar el sufijo .asmx y guardarse en el árbol de directorios del servidor web. Aquí lo guardamos con el nombre operations.asmx en la carpeta <IISroot>\polyvbnet\operations:

Image

Asociamos a esta carpeta física la carpeta virtual IIS [operations]:

Accedamos al servicio con un navegador. El URl que hay que solicitar es [http://localhost/operations/operations.asmx]:

Image

Obtenemos un documento web con un enlace para cada uno de los métodos definidos en el servicio web operations. Sigamos el enlace ajouter:

Image

La página obtenida nos propone probar el método ajouter proporcionándole los dos argumentos a y b que necesita. Recordemos la definición del método ajouter:

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

Cabe señalar que la página ha tomado los nombres de los argumentos a y b utilizados en la definición del método. Se utiliza el botón Appeler y se obtiene la siguiente respuesta en una ventana separada del navegador:

Image

Si en el ejemplo anterior se introduce [Affichage/Source], se obtiene el siguiente código:

Image

Repitamos la operación para el método [toutfaire]:

Image

Obtenemos la siguiente página:

Image

Utilicemos el botón [Appeler] de arriba:

Image

En cualquier caso, la respuesta del servidor tiene el siguiente formato:

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • la respuesta está en formato XML
  • la línea 1 es estándar y siempre está presente en la respuesta
  • las líneas siguientes dependen del tipo de resultado (doble, ArrayOfDouble), del número de resultados y del espacio de nombres del servicio web (st.istia.univ-angers.fr en este caso).

Existen varios métodos para consultar un servicio web y obtener su respuesta. Volvamos al URL del servicio:

Image

y sigamos el enlace [ajouter]. En la página que se muestra, se exponen dos métodos para consultar la función [ajouter] del servicio web:

Image

Image

Image

Estos dos métodos de acceso a las funciones de un servicio web se denominan, respectivamente: HTTP-POST y SOAP. Los examinaremos ahora uno tras otro.

Nota: en las primeras versiones de VS.NET, existía un tercer método denominado HTTP-GET. A la fecha de redacción de este documento (marzo de 2004), este método ya no parece estar disponible. Esto significa que el servicio web generado por VS.NET no acepta solicitudes GET. Esto no significa que no se puedan escribir servicios web que acepten solicitudes GET, en particular con otras herramientas distintas de VS.NET o simplemente a mano.

10.5. Un cliente HTTP-POST

Seguimos el método propuesto por el servicio web:

Image

Comentemos lo que está escrito. En primer lugar, el cliente web debe enviar los siguientes encabezados HTTP:

POST /operations/operations.asmx/ajouter HTTP/1.1
El cliente web realiza una solicitud POST a URL /operations/operations.asmx/ajouter según el protocolo HTTP versión 1.1
HOST: localhost
Se especifica la máquina de destino de la solicitud. En este caso, localhost. Este encabezado se ha convertido en obligatorio con la versión 1.1 del protocolo HTTP
Content-Type: application/x-www-form-urlencoded
Aquí se especifica que, tras los encabezados HTTP, se enviarán parámetros adicionales en formato urlencoded. Este formato sustituye ciertos caracteres por su código hexadecimal.
Content-length: 7
Es el tamaño en caracteres de la cadena de parámetros que se enviará tras los encabezados HTTP.

Los encabezados HTTP van seguidos de una línea en blanco y, a continuación, de la cadena de parámetros de POST de [Content-Length] caracteres en el formato a=XX&b=YY, donde XX y YY son las cadenas «codificadas por URL» de los valores de los parámetros a y b. Sabemos lo suficiente como para reproducir lo anterior con nuestro cliente TCP genérico ya utilizado en el capítulo sobre programación TCP-IP:

  • ejecutamos IIS
  • el servicio está disponible en la URL [http://localhost/operations/operations.asmx]
  • utilizamos el cliente TCP genérico en una ventana DOS
dos>clttcpgenerique localhost 80
Commandes :
POST /operations/operations.asmx/ajouter 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, 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>
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

En primer lugar, cabe destacar que hemos añadido el encabezado [Connection: close] para solicitar al servidor que cierre la conexión tras enviar la respuesta. Esto es necesario en este caso. Si no se indica, por defecto el servidor mantendrá la conexión abierta. Sin embargo, su respuesta es una secuencia de líneas de texto cuya última línea no termina con un carácter de fin de línea. Resulta que nuestro cliente genérico TCP lee líneas de texto que terminan con el carácter de fin de línea mediante el método ReadLine. Si el servidor no cierra la conexión tras enviar la última línea, el cliente se bloquea porque espera un carácter de fin de línea que no llega. Si el servidor cierra la conexión, el método ReadLine del cliente finaliza y el cliente no se queda bloqueado.

Inmediatamente después de recibir la línea vacía que indica el final de los encabezados HTTP, el servidor IIS envía una primera respuesta:

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

Esta respuesta, compuesta únicamente por encabezados HTTP, indica al cliente que puede enviar los 7 caracteres que dijo que quería enviar. Lo que hacemos:

a=2&b=3

Hay que tener en cuenta aquí que nuestro cliente TCP envía más de 7 caracteres, ya que los envía con un marcador de fin de línea (WriteLine). Esto no supone ningún problema para el servidor, ya que de los caracteres recibidos solo tomará los 7 primeros y, a continuación, se cerrará la conexión (Connection: close). Estos caracteres sobrantes habrían sido un problema si la conexión se hubiera mantenido abierta, ya que entonces se habrían interpretado como parte del siguiente comando del cliente. Una vez recibidos los parámetros, el servidor envía su respuesta:

<-- 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>

Ahora disponemos de los elementos necesarios para escribir un cliente programado para nuestro servicio web. Será un cliente de consola llamado httpPost2 y se utilizará de la siguiente manera:

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

Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b

ajouter 6 7
--> POST /operations/operations.asmx/ajouter 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, 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>
[résultat=13]

soustraire 8 9
--> POST /operations/operations.asmx/soustraire 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:47 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=8&b=9
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 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>
[résultat=-1]

fin

dos>

Se llama al cliente pasándole el URL del servicio web:

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

A continuación, el cliente lee los comandos introducidos mediante el teclado y los ejecuta. Estos tienen el siguiente formato:

fonction a b

donde «función» es la función del servicio web que se invoca (sumar, restar, multiplicar, dividir) y «a» y «b» son los valores sobre los que va a operar dicha función. Por ejemplo:

ajouter 6 7

A partir de ahí, el cliente realizará la solicitud HTTP necesaria al servidor web y obtendrá una respuesta. Los intercambios entre el cliente y el servidor se muestran en pantalla para facilitar la comprensión del proceso:

ajouter 6 7
--> POST /operations/operations.asmx/ajouter 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, 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>
[résultat=13]

Arriba se muestra el intercambio que ya hemos visto con el cliente TCP genérico, con una diferencia: el encabezado HTTP Connection: Keep-Alive solicita al servidor que no cierre la conexión. Por lo tanto, esta permanece abierta para la siguiente operación del cliente, que así no necesita volver a conectarse al servidor. Sin embargo, esto le obliga a utilizar un método distinto de ReadLine() para leer la respuesta del servidor, ya que sabemos que esta es una secuencia de líneas cuya última no termina con un carácter de fin de línea. Una vez obtenida toda la respuesta del servidor, el cliente la analiza para encontrar el resultado de la operación solicitada y mostrarlo:

[résultat=13]

Examinemos el código de nuestro cliente:


' espacios de nombres
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web

' cliente de un servicio web de operaciones
Public Module clientPOST

    Public Sub Main(ByVal args() As String)
        ' sintaxis
        Const syntaxe As String = "pg URI"
        Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}

        ' número de argumentos
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' se anota la URI solicitada
        Dim URIstring As String = args(0)

        ' se conecta al servidor
        Dim uri As Uri = Nothing        ' l'URI du service web
        Dim client As TcpClient = Nothing        ' la liaison tcp du client avec le serveur
        Dim [IN] As StreamReader = Nothing        ' le flux de lecture du client
        Dim OUT As StreamWriter = Nothing        ' le flux d'écriture du client
        Try
            ' conexión al servidor
            uri = New Uri(URIstring)
            client = New TcpClient(uri.Host, uri.Port)
            ' se crean los flujos de entrada-salida del cliente TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
        Catch ex As Exception
            ' URI incorrecto u otro problema
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

        ' creación de un diccionario de funciones del servicio web
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i

        ' las solicitudes del usuario se introducen mediante el teclado
        ' en la forma función a b
        ' terminan con el comando fin
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande
        Dim fonction As String = Nothing        ' nom d'une fonction du service web
        Dim a, b As String        ' les arguments des fonctions du service web

        ' solicita al usuario
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")

        ' gestión de errores
        Dim erreurCommande As Boolean
        Try
            ' bucle de introducción de comandos tecleados
            While True
                ' sin errores al inicio
                erreurCommande = False
                ' lectura del comando
                commande = Console.In.ReadLine().Trim().ToLower()
                ' ¿Terminado?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' descomposición del comando en campos
                champs = Regex.Split(commande, "\s+")
                Try
                    ' se necesitan tres campos
                    If champs.Length <> 3 Then
                        Throw New Exception
                    End If
                    ' el campo 0 debe ser una función reconocida
                    fonction = champs(0)
                    If Not dicoFonctions.ContainsKey(fonction) Then
                        Throw New Exception
                    End If
                    ' el campo 1 debe ser un número válido
                    a = champs(1)
                    Double.Parse(a)
                    ' el campo 2 debe ser un número válido
                    b = champs(2)
                    Double.Parse(b)
                Catch
                    ' comando no válido
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    erreurCommande = True
                End Try
                ' se envía la solicitud al servicio web
                If Not erreurCommande Then executeFonction([IN], OUT, uri, fonction, a, b)
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' fin de la conexión cliente-servidor
        Try
            [IN].Close()
            OUT.Close()
            client.Close()
        Catch
        End Try
    End Sub
...........
    ' visualización de errores
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub
End Module

Aquí encontramos elementos que ya hemos visto varias veces y que no requieren comentarios especiales. Examinemos ahora el código del método executeFonction, donde se encuentran las novedades:


    ' 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)
        ' ejecuta la función (a,b) en el servicio web de URI uri
        ' los intercambios cliente-servidor se realizan a través de los flujos IN y OUT
        ' el resultado de la función se encuentra en la línea
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' enviada por el servidor

        ' construcción de la cadena de consulta
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' construcción de la tabla de encabezados HTTP a enviar
        Dim entetes(5) As String
        entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
        entetes(1) = "Host: " & uri.Host & ":" & uri.Port
        entetes(2) = "Content-Type: application/x-www-form-urlencoded"
        entetes(3) = "Content-Length: " & nbChars
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = ""

        ' se envían los encabezados HTTP al servidor
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' envío al servidor
            OUT.WriteLine(entetes(i))
            ' salida a pantalla
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

        ' se lee la primera respuesta del servidor web HTTP/1.1 100
        Dim ligne As String = Nothing
        ' una línea del flujo de lectura
        ligne = [IN].ReadLine()
        While ligne <> ""
            'eco
            Console.Out.WriteLine(("<-- " + ligne))
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While
        'eco de la última línea
        Console.Out.WriteLine(("<-- " + ligne))

        ' envío de los parámetros de la solicitud
        OUT.Write(requête)
        ' eco
        Console.Out.WriteLine(("--> " + requête))

        ' construcción de la expresión regular que permite obtener el tamaño de la respuesta XML
        ' en el flujo de la respuesta del servidor web
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0

        ' lectura de la segunda respuesta del servidor web tras el envío de la solicitud
        ' se almacena el valor de la línea Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' salida a pantalla
            Console.Out.WriteLine(("<-- " + ligne))
            ' ¿Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While
        ' salida de la última línea
        Console.Out.WriteLine("<--")

        ' construcción de la expresión regular que permite recuperar el resultado
        ' en el flujo de la respuesta del servidor web
        Dim modèle As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' se lee el resto de la respuesta del servidor web
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' se descompone la respuesta en líneas de texto
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' se recorren las líneas de texto en busca del resultado
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' seguimiento
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' comparación de la línea actual con el patrón
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' ¿Se ha encontrado?
            If MatchRésultat.Success Then
                ' se anota el resultado
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' se muestra el resultado
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

En primer lugar, el cliente HTTP-POST envía su solicitud en el formato POST:


        ' construcción de la cadena de consulta
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' construcción de la tabla de encabezados HTTP que se va a enviar
        Dim entetes(5) As String
        entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
        entetes(1) = "Host: " & uri.Host & ":" & uri.Port
        entetes(2) = "Content-Type: application/x-www-form-urlencoded"
        entetes(3) = "Content-Length: " & nbChars
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = ""

        ' se envían los encabezados HTTP al servidor
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' envío al servidor
            OUT.WriteLine(entetes(i))
            ' salida a pantalla
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

En el encabezado

--> Content-Length: 7

se debe indicar el tamaño de los parámetros que el cliente enviará tras los encabezados HTTP:

--> a=6&b=7

Para ello se utiliza el siguiente código:


        ' construcción de la cadena de consulta
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

El método HttpUtility.UrlEncode(cadena de caracteres) transforma algunos de los caracteres de chaîne en %n1n2, donde n1n2 es el código ASCII del carácter transformado. Los caracteres afectados por esta transformación son todos aquellos que tienen un significado especial en una consulta POST (el espacio, el signo =, el signo &, etc.). En este caso, el método HttpUtility.UrlEncode suele ser innecesario, ya que a y b son números que no contienen ninguno de estos caracteres especiales. Aquí se utiliza a modo de ejemplo. Necesita el espacio de nombres System.Web. Una vez que el cliente ha enviado sus encabezados HTTP:

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

el servidor responde con el encabezado HTTP 100 Continue:

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

El código se limita a leer y mostrar en pantalla esta primera respuesta:


        ' se lee la primera respuesta del servidor web HTTP/1.1 100
        Dim ligne As String = Nothing
        ' una línea del flujo de lectura
        ligne = [IN].ReadLine()
        While ligne <> ""
            'eco
            Console.Out.WriteLine(("<-- " + ligne))
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While
        'eco de la última línea
        Console.Out.WriteLine(("<-- " + ligne))

Una vez leída esta primera respuesta, el cliente debe enviar sus parámetros:

--> a=6&b=7

Lo hace con el siguiente código:

        ' envío de los parámetros de la solicitud
        OUT.Write(requête)
        ' eco
        Console.Out.WriteLine(("--> " + requête))

A continuación, el servidor enviará su respuesta. Esta se compone de dos partes:

  1. encabezados HTTP terminados en una línea en blanco
  2. la respuesta en formato XML
<-- 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>

En primer lugar, el cliente lee los encabezados HTTP para encontrar la línea Content-Length y recuperar el tamaño de la respuesta XML (en este caso, 90). Este se recupera mediante una expresión regular. Se podría haber hecho de otra manera y, sin duda, de forma más eficaz.


        ' construcción de la expresión regular que permite obtener el tamaño de la respuesta XML
        ' en el flujo de la respuesta del servidor web
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0

        ' lectura de la segunda respuesta del servidor web tras el envío de la solicitud
        ' se almacena el valor de la línea Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' salida a pantalla
            Console.Out.WriteLine(("<-- " + ligne))
            ' ¿Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While
        ' salida de la última línea
        Console.Out.WriteLine("<--")

Una vez que tenemos la longitud N de la respuesta XML, solo tenemos que leer N caracteres en el flujo IN de la respuesta del servidor. Esta cadena de N caracteres se descompone en líneas de texto para facilitar el seguimiento en pantalla. Entre estas líneas buscamos la línea del resultado:

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

utilizando, una vez más, una expresión regular. Una vez encontrado el resultado, se muestra.

[résultat=13]

El final del código del cliente es el siguiente:


        ' construcción de la expresión regular que permite recuperar el resultado
        ' en el flujo de la respuesta del servidor web
        Dim modèle As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' se lee el resto de la respuesta del servidor web
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' se descompone la respuesta en líneas de texto
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' se recorren las líneas de texto en busca del resultado
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' seguimiento
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' comparación de la línea actual con el patrón
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' ¿Se ha encontrado?
            If MatchRésultat.Success Then
                ' se anota el resultado
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' se muestra el resultado
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.6. Un cliente SOAP

Aquí analizamos un segundo cliente que utilizará un diálogo cliente-servidor de tipo SOAP (Simple Object Access Protocol). Se nos presenta un ejemplo de diálogo para la función ajouter:

Image

Image

La solicitud del cliente es una solicitud POST. Por lo tanto, vamos a encontrar algunos de los mecanismos del cliente anterior. La principal diferencia es que, mientras que el cliente HTTP-POST enviaba los parámetros a y b en forma de

    a=A&b=B

, el cliente SOAP los envía en un formato XML más complejo:

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/ajouter"

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

A cambio, recibe una respuesta XML, también más compleja que las respuestas vistas anteriormente:

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

Aunque la solicitud y la respuesta son más complejas, se trata del mismo mecanismo HTTP que para el cliente HTTP-POST. La escritura del cliente SOAP puede copiarse así de la del cliente HTTP-POST. A continuación se muestra un ejemplo de ejecución:

dos>clientsoap1 http://localhost/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b

ajouter 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/ajouter"
-->
<-- 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>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</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>
[résultat=7]

Solo cambia el método executeFonction. El cliente SOAP envía los encabezados HTTP de su solicitud. Son simplemente un poco más complejos que los de HTTP-POST:

ajouter 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/ajouter"
-->

El código que los genera:


    ' 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)
        ' ejecuta la función (a,b) en el servicio web de URI uri
        ' los intercambios cliente-servidor se realizan a través de los flujos IN y OUT
        ' el resultado de la función se encuentra en la línea
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' enviada por el servidor
        ' construcción de la cadena de consulta SOAP

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

        ' construcción de la tabla de encabezados HTTP que se va a enviar
        Dim entetes(6) As String
        entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
        entetes(1) = "Host: " & uri.Host & ":" & uri.Port
        entetes(2) = "Content-Type: text/xml; charset=utf-8"
        entetes(3) = "Content-Length: " & nbCharsSOAP
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
        entetes(6) = ""

        ' se envían los encabezados HTTP al servidor
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' envío al servidor
            OUT.WriteLine(entetes(i))
            ' salida a pantalla
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

Al recibir esta solicitud, el servidor envía su primera respuesta, que el cliente muestra:

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

El código de lectura de esta primera respuesta es el siguiente:


        ' se lee la primera respuesta del servidor web HTTP/1.1 100
        Dim ligne As String = Nothing
        ' una línea del flujo de lectura
        ligne = [IN].ReadLine()
        While ligne <> ""
            'eco
            Console.Out.WriteLine(("<-- " + ligne))
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While        'while
        'eco de la última línea
        Console.Out.WriteLine(("<-- " + ligne))

El cliente enviará ahora sus parámetros en formato XML en lo que se denomina un sobre SOAP:

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

El código:

1
2
3
4
        ' envío de los parámetros de la solicitud
        OUT.Write(requêteSOAP)
        ' eco
        Console.Out.WriteLine(("--> " + requêteSOAP))

A continuación, el servidor enviará su respuesta definitiva:

<-- 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>

El cliente muestra en pantalla los encabezados HTTP recibidos mientras busca la línea Content-Length:


        ' construcción de la expresión regular que permite obtener el tamaño de la respuesta XML
        ' en el flujo de la respuesta del servidor web
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0

        ' lectura de la segunda respuesta del servidor web tras el envío de la solicitud
        ' se almacena el valor de la línea Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' salida a pantalla
            Console.Out.WriteLine(("<-- " + ligne))
            ' ¿Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While        'while
        ' salida de la última línea
        Console.Out.WriteLine("<--")

Una vez conocido el tamaño N de la respuesta XML, el cliente lee N caracteres del flujo de la respuesta del servidor, descompone la cadena recuperada en líneas de texto para mostrarlas en pantalla y busca en ellas la etiqueta XML del resultado: <ajouterResult>7</ajouterResult> y muestra este último:


        ' construcción de la expresión regular que permite recuperar el resultado
        ' en el flujo de la respuesta del servidor web
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' se lee el resto de la respuesta del servidor web
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' se descompone la respuesta en líneas de texto
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' se recorren las líneas de texto en busca del resultado
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' seguimiento
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' comparación de la línea actual con el patrón
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' ¿Se ha encontrado?
            If MatchRésultat.Success Then
                ' se anota el resultado
                strRésultat = MatchRésultat.Groups(1).Value
            End If
            'línea siguiente
        Next i
        ' se muestra el resultado
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.7. Encapsulación de los intercambios cliente-servidor

Imaginemos que nuestro servicio web operations es utilizado por diversas aplicaciones. Sería interesante poner a disposición de estas una clase que actuara como interfaz entre la aplicación cliente y el servicio web y que ocultara la mayor parte de los intercambios de red, que para la mayoría de los desarrolladores no son triviales. De este modo, tendríamos el siguiente esquema:

La aplicación cliente se dirigiría a la interfaz cliente-servidor para realizar sus solicitudes al servicio web. Esta se encargaría de todos los intercambios de red necesarios con el servidor y devolvería el resultado obtenido a la aplicación cliente. Esta ya no tendría que ocuparse de los intercambios con el servidor, lo que facilitaría enormemente su programación.

10.7.1. La clase de encapsulación

Tras lo visto en los párrafos anteriores, ahora conocemos bien los intercambios de red entre el cliente y el servidor. Incluso hemos visto tres métodos. Optamos por encapsular el método SOAP. La clase es la siguiente:


' espacios de nombres
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic

' clientSOAP del servicio web operations
Public Class clientSOAP

    ' variables de instancia
    Private uri As uri = Nothing    ' l'URI du service web
    Private client As TcpClient = Nothing    ' la liaison tcp du client avec le serveur
    Private [IN] As StreamReader = Nothing    ' le flux de lecture du client
    Private OUT As StreamWriter = Nothing    ' le flux d'écriture du client
    ' diccionario de funciones
    Private dicoFonctions As New Hashtable
    ' lista de funciones
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' verboso
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

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

        ' se nota verboso
        Me.verbose = verbose

        ' conexión al servidor
        uri = New Uri(uriString)
        client = New TcpClient(uri.Host, uri.Port)

        ' se crean los flujos de entrada-salida del cliente TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' creación del diccionario de funciones del servicio web
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
    End Sub

    ' cierre de la conexión al servidor
    Public Sub Close()
        ' fin de la conexión cliente-servidor
        [IN].Close()
        OUT.Close()
        client.Close()
    End Sub

    ' executeFonction
    Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String
        ' ejecuta la función (a,b) en el servicio web de URI uri
        ' los intercambios cliente-servidor se realizan a través de los flujos IN y OUT
        ' el resultado de la función se encuentra en la línea
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' enviada por el servidor

        ' ¿función válida?
        fonction = fonction.Trim().ToLower()
        If Not dicoFonctions.ContainsKey(fonction) Then
            Return "[fonction [" + fonction + "] indisponible : (ajouter, soustraire,multiplier,diviser)]"
        End If

        ' ¿Son válidos los argumentos a y b?
        Dim doubleA As Double = 0
        Try
            doubleA = Double.Parse(a)
        Catch
            Return "[argument [" + a + "] incorrect (double)]"
        End Try
        Dim doubleB As Double = 0
        Try
            doubleB = Double.Parse(b)
        Catch
            Return "[argument [" + b + "] incorrect (double)]"
        End Try

        ' ¿división por cero?
        If fonction = "diviser" And doubleB = 0 Then
            Return "[division par zéro]"
        End If

        ' construcción de la cadena de consulta SOAP
        Dim requêteSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
        requêteSOAP += "<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
        requêteSOAP += "<soap:Body>" + ControlChars.Lf
        requêteSOAP += "<" + fonction + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
        requêteSOAP += "<a>" + a + "</a>" + ControlChars.Lf
        requêteSOAP += "<b>" + b + "</b>" + ControlChars.Lf
        requêteSOAP += "</" + fonction + ">" + ControlChars.Lf
        requêteSOAP += "</soap:Body>" + ControlChars.Lf
        requêteSOAP += "</soap:Envelope>"
        Dim nbCharsSOAP As Integer = requêteSOAP.Length

        ' construcción de la tabla de encabezados HTTP que se va a enviar
        Dim entetes(6) As String
        entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
        entetes(1) = "Host: " + uri.Host + ":" + uri.Port.ToString
        entetes(2) = "Content-Type: text/xml; charset=utf-8"
        entetes(3) = "Content-Length: " + nbCharsSOAP.ToString
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
        entetes(6) = ""

        ' se envían los encabezados HTTP al servidor
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' envío al servidor
            OUT.WriteLine(entetes(i))
            ' salida a pantalla
            If verbose Then
                Console.Out.WriteLine(("--> " + entetes(i)))
            End If
        Next i

        ' se lee la primera respuesta del servidor web HTTP/1.1 100
        Dim ligne As String = Nothing
        ' una línea del flujo de lectura
        ligne = [IN].ReadLine()
        While ligne <> ""
            'eco
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While
        'eco de la última línea
        If verbose Then
            Console.Out.WriteLine(("<-- " + ligne))
        End If
        ' envío de los parámetros de la solicitud
        OUT.Write(requêteSOAP)
        ' eco
        If verbose Then
            Console.Out.WriteLine(("--> " + requêteSOAP))
        End If

        ' construcción de la expresión regular que permite obtener el tamaño de la respuesta XML
        ' en el flujo de la respuesta del servidor web
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0

        ' lectura de la segunda respuesta del servidor web tras el envío de la solicitud
        ' se almacena el valor de la línea Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' salida a pantalla
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' ¿Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While

        ' salida de la última línea
        If verbose Then
            Console.Out.WriteLine("<--")
        End If

        ' construcción de la expresión regular que permite recuperar el resultado
        ' en el flujo de la respuesta del servidor web
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' se lee el resto de la respuesta del servidor web
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' se descompone la respuesta en líneas de texto
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' se recorren las líneas de texto en busca del resultado
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' seguimiento
            If verbose Then
                Console.Out.WriteLine(("<-- " + lignes(i)))
            End If            ' comparaison ligne courante au modèle
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' ¿se ha encontrado?
            If MatchRésultat.Success Then
                ' se anota el resultado
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i

        ' se devuelve el resultado
        Return strRésultat
    End Function
End Class

No encontramos nada nuevo con respecto a lo que ya hemos visto. Simplemente hemos tomado el código del cliente SOAP que hemos estudiado y lo hemos reorganizado un poco para convertirlo en una clase. Esta clase tiene un constructor y dos métodos:


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

    ' executeFonction
    Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String


    ' cierre de la conexión con el servidor
    Public Sub Close()

y tiene los siguientes atributos:


    ' variables de instancia
    Private uri As Uri = Nothing    ' l'URI du service web
    Private client As TcpClient = Nothing    ' la liaison tcp du client avec le serveur
    Private [IN] As StreamReader = Nothing    ' le flux de lecture du client
    Private OUT As StreamWriter = Nothing    ' le flux d'écriture du client
    ' diccionario de funciones
    Private dicoFonctions As New Hashtable
    ' lista de funciones
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' detallado
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

Se pasan dos parámetros al constructor:

  1. el URI del servicio web al que debe conectarse
  2. un valor booleano verbose que, en realidad, solicita que los intercambios de red se muestren en pantalla; de lo contrario, no se mostrarán.

Durante la construcción, se crean los flujos IN de lectura de red, OUT de escritura de red, así como el diccionario de funciones gestionadas por el servicio. Una vez construido el objeto, se abre la conexión cliente-servidor y sus flujos IN y OUT quedan disponibles.

El método Close permite cerrar la conexión con el servidor.

El método ExecuteFonction es el que hemos escrito para el cliente SOAP estudiado, salvo por algunos detalles:

  1. Los parámetros uri, IN y OUT, que antes se pasaban como parámetros al método, ya no es necesario pasarlos, ya que ahora son atributos de instancia accesibles para todos los métodos de la instancia
  2. El método ExecuteFonction, que antes devolvía un tipo void y mostraba el resultado de la función en pantalla, ahora devuelve este resultado y, por lo tanto, un tipo string.

Normalmente, un cliente utilizará la clase clientSOAP de la siguiente manera:

  1. creación de un objeto clientSOAP que establecerá la conexión con el servicio web
  2. uso repetido del método executeFonction
  3. cierre de la conexión con el servicio web mediante el método Close.

Analicemos un primer cliente.

10.7.2. Un cliente de consola

Retomamos aquí el cliente SOAP estudiado cuando la clase clientSOAP aún no existía y lo reestructuramos para que ahora utilice esta clase:


' espacios de nombres
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

Public Module testClientSoap

    ' solicita el URI del servicio web de operaciones
    ' ejecuta de forma interactiva los comandos introducidos mediante el teclado
    Public Sub Main(ByVal args() As String)
        ' sintaxis
        Const syntaxe As String = "pg URI [verbose]"

        ' número de argumentos
        If args.Length <> 1 And args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' ¿detallado?
        Dim verbose As Boolean = False
        If args.Length = 2 Then
            verbose = args(1).ToLower() = "verbose"
        End If
        ' se conecta al servicio web 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' error de conexión
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

        ' las solicitudes del usuario se introducen mediante el teclado
        ' en el formato función a b; terminan con el comando fin
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

        ' solicita al usuario
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b" + ControlChars.Lf)

        ' gestión de errores
        Dim erreurCommande As Boolean
        Try
            ' bucle de introducción de comandos tecleados
            While True
                ' al principio no hay error
                erreurCommande = False
                ' lectura del comando
                commande = Console.In.ReadLine().Trim().ToLower()
                ' ¿Terminado?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' descomposición del comando en campos
                champs = Regex.Split(commande, "\s+")
                ' se necesitan tres campos
                If champs.Length <> 3 Then
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    ' se registra el error
                    erreurCommande = True
                End If
                ' se envía la solicitud al servicio web
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
                ' siguiente solicitud
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' fin de la conexión cliente-servidor
        Try
            client.Close()
        Catch
        End Try
    End Sub

    ' visualización de los errores
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub
End Module

El cliente es ahora considerablemente más sencillo y no contiene ninguna comunicación de red. El cliente admite dos parámetros:

  1. el URI del servicio web operations
  2. la palabra clave opcional verbose. Si está presente, los intercambios de red se mostrarán en pantalla.

Estos dos parámetros se utilizan para crear un objeto clientSOAP que se encargará de las comunicaciones con el servicio web.


        ' se conecta al servicio web 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' error de conexión
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

Una vez establecida la conexión con el servicio web, el cliente puede enviar sus solicitudes. Estas se introducen mediante el teclado, se analizan y, a continuación, se envían al servidor mediante una llamada al método executeFonction del objeto clientSOAP.


                ' se realiza la solicitud al servicio web
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))

La clase clientSOAP se compila en un «ensamblado»:

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

A continuación, la aplicación cliente testClientSoap se compila mediante:

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

A continuación se muestra un ejemplo de ejecución concisa:

dos>testclientsoap http://localhost/st/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b

ajouter 1 3
résultat=4
soustraire 6 7
résultat=-1
multiplier 4 5
résultat=20
diviser 1 2
résultat=0.5
x
syntaxe : [ajouter|soustraire|multiplier|diviser] a b
x 1 2
résultat=[fonction [x] indisponible : (ajouter, soustraire,multiplier,diviser)]
ajouter a b
résultat=[argument [a] incorrect (double)]
ajouter 1 b
résultat=[argument [b] incorrect (double)]
diviser 1 0
résultat=[division par zéro]
fin

Se puede seguir el intercambio de datos de red solicitando una ejecución «detallada»:

dos>testClientSOAP http://localhost/operations/operations.asmx verboso
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b

ajouter 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/ajouter"
-->
<-- 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>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>4</a>
<b>8</b>
</ajouter>
</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>
résultat=12
fin

Ahora vamos a crear un cliente gráfico.

10.7.3. Un cliente gráfico de Windows

Ahora vamos a consultar nuestro servicio web con un cliente gráfico que también utilizará la clase clientSOAP. La interfaz gráfica será la siguiente:

Los controles son los siguientes:

n.º
tipo
nombre
función
1
TextBox
txtURI
URI del servicio web de operaciones
2
Botón
btnOuvrir
abre la conexión con el servicio web
3
Botón
btnFermer
cierra la conexión con el servicio web
4
ComboBox
cmbFonctions
la lista de funciones (sumar, restar, multiplicar, dividir)
5
TextBox
txtA
los argumentos de las funciones
6
TextBox
txtB
el argumento b de las funciones
7
TextBox
txtRésultat
el resultado de la función (a, b)
8
Botón
btnCalculer
inicia el cálculo de la función(a,b)
9
TextBox
txtErreur
muestra un mensaje de estado de la conexión

Existen algunas restricciones de funcionamiento:

  • el botón btnOuvrir solo está activo si el campo txtURI no está vacío y no hay ninguna conexión ya abierta
  • el botón btnFermer solo está activo cuando se ha abierto una conexión con el servicio web
  • el botón btnCalculer solo está activo cuando hay una conexión abierta y los campos txtA y txtB no están vacíos
  • los campos txtRésultat y txtErreur tienen el atributo ReadOnly establecido en verdadero

El cliente comienza abriendo la conexión con el servicio web mediante el botón [Ouvrir]:

Image

A continuación, el usuario puede elegir una función y valores para a y b:

Image

Image

Image

Image

Image

A continuación se muestra el código de la aplicación. Hemos omitido el código del formulario, ya que no es relevante en este contexto.


'espacios de nombres
Imports System
Imports System.Windows.Forms

' la clase del formulario
Public Class FormClientSOAP
    Inherits System.Windows.Forms.Form

    ' atributos de instancia
    Dim client As clientSOAP    ' client SOAP du service web operations

#Región «Código generado por el Diseñador de formularios de Windows»

    Public Sub New()
        MyBase.New()
        'Esta llamada es requerida por el Diseñador de Windows Form.
        InitializeComponent()
        ' otras inicializaciones
        myInit()
    End Sub

    'El método sustituido Dispose del formulario para limpiar la lista de componentes.
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
    End Sub

...

    Private Sub InitializeComponent()
....
    End Sub

#Fin de la región


    Private Sub myInit()
        ' inicialización del formulario
        cmbFonctions.SelectedIndex = 0
        btnOuvrir.Enabled = False
        btnFermer.Enabled = True
        btnCalculer.Enabled = False
    End Sub

    Private Sub txtURI_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtURI.TextChanged
        ' el contenido del campo de entrada ha cambiado: se establece el estado del botón «Abrir»
        btnOuvrir.Enabled = txtURI.Text.Trim <> ""
    End Sub

    Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
        ' solicitud de apertura de una conexión con el servicio web
        Try
            ' creación de un objeto de tipo [clientSOAP]
            client = New clientSOAP(txtURI.Text.Trim, False)
            ' Estados de los botones
            btnOuvrir.Enabled = False
            btnFermer.Enabled = True
            ' el URI ya no se puede modificar
            txtURI.ReadOnly = True
            ' estado del cliente
            txtErreur.Text = "Liaison au service web ouverte"
        Catch ex As Exception
            ' se ha producido un error; se muestra
            txtErreur.Text = ex.Message
        End Try
    End Sub

    Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
        ' cierre de la conexión al servicio web
        client.Close()
        ' estados de los botones
        btnOuvrir.Enabled = True
        btnFermer.Enabled = False
        ' URI
        txtURI.ReadOnly = False
        ' estado del cliente
        txtErreur.Text = "Liaison au service web fermée"
    End Sub

    Private Sub btnCalculer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnCalculer.Click
        ' cálculo de una función f(a,b)
        ' se borra el resultado anterior
        txtRésultat.Text = ""
        Try
            txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
        Catch ex As Exception
            ' se ha producido un error de red
            txtErreur.Text = ex.Message
            ' se cierra la conexión
            btnFermer_Click(Nothing, Nothing)
        End Try
    End Sub

    Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
        ' cambio del valor de A
        btnCalculer.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
        ' cambio del valor de B
        txtA_TextChanged(Nothing, Nothing)
    End Sub

    ' método principal
    Public Shared Sub main()
        Application.Run(New FormClientSOAP)
    End Sub
End Class

Una vez más, la clase clientSOAP oculta todo el aspecto de red de la aplicación. La aplicación se ha construido de la siguiente manera:

  • el ensamblado clientSOAP.dll, que contiene la clase clientSOAP, se ha colocado en la carpeta del proyecto
  • la interfaz gráfica clientsoapgui.vb se ha creado con VS.NET y luego se ha compilado en una ventana de DOS:
dos>vbc /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll /r:clientSOAP.dll clientsoapgui.vb

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

A continuación, se inició la interfaz gráfica mediante:

dos>clientsoapgui

10.8. Un cliente proxy

Recordemos lo que acabamos de hacer. Hemos creado una clase intermedia que encapsula los intercambios de red entre un cliente y un servicio web según el esquema siguiente:

La plataforma .NET lleva esta lógica un paso más allá. Una vez conocido el servicio web al que nos queremos conectar, podemos generar automáticamente la clase que nos servirá de intermediario para acceder a las funciones del servicio web y que ocultará toda la parte de red. A esta clase se le denomina proxy del servicio web para el que se ha generado.

¿Cómo generar la clase proxy de un servicio web? Un servicio web siempre va acompañado de un archivo de descripción en formato XML. Si el URI de nuestro servicio web operations es http://localhost/operations/operations.asmx, su archivo de descripción está disponible en http://localhost/operations/operations.asmx?wsdl, tal y como se muestra en la siguiente captura de pantalla:

Image

Aquí tenemos un archivo XML que describe con precisión todas las funciones del servicio web, indicando para cada una de ellas el tipo y el número de parámetros, así como el tipo de resultado. A este archivo se le denomina archivo WSDL del servicio porque utiliza el lenguaje WSDL (Web Services Description Language). A partir de este archivo, se puede generar una clase proxy con la ayuda de la herramienta wsdl:

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.

Écriture du fichier 'D:\data\devel\vbnet\poly\chap9\clientproxy\operations.vb'.

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

La herramienta wsdl genera un archivo fuente VB.NET (opción /language=vb) con el nombre de la clase que implementa el servicio web, en este caso operations. Veamos una parte del código generado:

'------------------------------------------------------------------------------
' <autogenerado>
'     Este código fue generado por una herramienta.
'     Versión de tiempo de ejecución: 1.1.4322.573
'
'     Los cambios en este archivo pueden provocar un comportamiento incorrecto y se perderán si 
'     se vuelve a generar el código.
' </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

'
'Este código fuente ha sido té generado automáticamente généré por wsdl, Versión=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 ajouter(ByVal a As Double, ByVal b As Double) As Double
        Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
        Return CType(results(0),Double)
    End Function

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

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

Este código resulta un poco complejo a primera vista. No es necesario comprender sus detalles para poder utilizarlo. Examinemos primero la declaración de la clase:

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

La clase lleva el nombre operations del servicio web para el que se ha creado. Deriva de la clase SoapHttpClientProtocol:

Image

Nuestra clase proxy tiene un único constructor:

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

El constructor asigna al atributo url el valor URL del servicio web asociado al proxy. La clase operations anterior no define por sí misma el atributo url. Este se hereda de la clase de la que deriva el proxy: System.Web.Services.Protocols.SoapHttpClientProtocol. Veamos ahora lo que se refiere al método ajouter:

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

Se puede observar que tiene la misma firma que en el servicio web operations, donde se definía de la siguiente manera:

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

La forma en que esta clase interactúa con el servicio web no aparece aquí. Esta interacción está totalmente gestionada por la clase padre System.Web.Services.Protocols.SoapHttpClientProtocol. En el proxy solo se encuentra lo que lo diferencia de los demás proxies:

  • el URL del servicio web asociado
  • la definición de los métodos del servicio asociado.

Para utilizar los métodos del servicio web operations, el cliente solo necesita la clase proxy operations generada anteriormente. Compilemos esta clase en un archivo assembly:

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

Ahora escribamos un cliente de consola. Se invoca sin parámetros y ejecuta las solicitudes introducidas mediante el teclado:

dos>testclientproxy
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b

ajouter 4 5
résultat=9
soustraire 9 8
résultat=1
multiplier 10 4
résultat=40
diviser 6 7
résultat=0,857142857142857
toutfaire 10 20
résultats=[30,-10,200,0,5]
diviser 5 0
résultat=+Infini
fin

El código del cliente es el siguiente:


' espacios de nombres
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic

Public Module testClientProxy

    ' ejecuta de forma interactiva los comandos introducidos mediante el teclado
    ' y los envía al servicio web de operaciones
    Public Sub Main()
        ' ya no hay argumentos: el URL del servicio web está codificado de forma fija en el proxy

        ' creación de un diccionario de funciones del servicio web
        Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser", "toutfaire"}
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i

        ' se crea un objeto proxy de operaciones 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' error de conexión
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

        ' las solicitudes del usuario se introducen mediante el teclado
        ' en la forma función a b; terminan con el comando fin
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

        ' solicita al usuario
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)

        ' algunos datos locales
        Dim erreurCommande As Boolean
        Dim fonction As String
        Dim a, b As Double
        ' bucle de introducción de comandos tecleados
        While True
            ' al principio no hay error
            erreurCommande = False
            ' lectura del comando
            commande = Console.In.ReadLine().Trim().ToLower()
            ' ¿Terminado?
            If commande Is Nothing Or commande = "fin" Then
                Exit While
            End If
            ' descomposición del comando en campos
            champs = Regex.Split(commande, "\s+")
            Try
                ' se necesitan tres campos
                If champs.Length <> 3 Then
                    Throw New Exception
                End If
                ' el campo 0 debe ser una función reconocida
                fonction = champs(0)
                If Not dicoFonctions.ContainsKey(fonction) Then
                    Throw New Exception
                End If
                ' el campo 1 debe ser un número válido
                a = Double.Parse(champs(1))
                ' el campo 2 debe ser un número válido
                b = Double.Parse(champs(2))
            Catch
                ' comando no válido
                Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                erreurCommande = True
            End Try
            ' se envía la solicitud al servicio web
            If Not erreurCommande Then
                Try
                    Dim résultat As Double
                    Dim résultats() As Double
                    If fonction = "ajouter" Then
                        résultat = myOperations.ajouter(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "soustraire" Then
                        résultat = myOperations.soustraire(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "multiplier" Then
                        résultat = myOperations.multiplier(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "diviser" Then
                        résultat = myOperations.diviser(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "toutfaire" Then
                        résultats = myOperations.toutfaire(a, b)
                        Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
                        résultats(2).ToString + "," + résultats(3).ToString + "]"))
                    End If
                Catch e As Exception
                    Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
                End Try
            End If
        End While
    End Sub

    ' visualización de errores
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' visualización del error
        System.Console.Error.WriteLine(msg)
        ' parada con error
        Environment.Exit(exitCode)
    End Sub
End Module

Solo examinamos el código específico del uso de la clase proxy. En primer lugar, se crea un objeto proxy operations:


        ' se crea un objeto proxy de operaciones 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' error de conexión
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

Se escriben en el teclado las líneas de función a y b. A partir de esta información, se invocan los métodos adecuados del proxy:


            ' se realiza la solicitud al servicio web
            If Not erreurCommande Then
                Try
                    Dim résultat As Double
                    Dim résultats() As Double
                    If fonction = "ajouter" Then
                        résultat = myOperations.ajouter(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "soustraire" Then
                        résultat = myOperations.soustraire(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "multiplier" Then
                        résultat = myOperations.multiplier(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "diviser" Then
                        résultat = myOperations.diviser(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "toutfaire" Then
                        résultats = myOperations.toutfaire(a, b)
                        Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
                        résultats(2).ToString + "," + résultats(3).ToString + "]"))
                    End If
                Catch e As Exception
                    Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
                End Try

Aquí se procesa por primera vez la operación toutfaire, que realiza las cuatro operaciones. Hasta ahora se había ignorado porque envía una matriz de números encapsulada en un contenedor XML, más complicado de gestionar que las respuestas XML simples de las otras funciones, que solo devuelven un único resultado. Se observa que aquí, con la clase proxy, utilizar el método toutfaire no es más complicado que utilizar los demás métodos. La aplicación se ha compilado en una ventana de DOS de la siguiente manera:

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

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

10.9. Configurar un servicio web

Un servicio web puede necesitar información de configuración para inicializarse correctamente. Con el servidor IIS, esta información se puede colocar en un archivo llamado web.config y ubicado en la misma carpeta que el servicio web. Supongamos que queremos crear un servicio web que necesita dos datos para inicializarse: un nombre y una edad. Estos dos datos se pueden colocar en el archivo web.config con el siguiente formato:

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

Los parámetros de inicialización se colocan en un contenedor XML:

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

Se declarará un parámetro de inicialización con el nombre P y el valor V con la línea:


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

¿Cómo recupera el servicio web esta información? Cuando IIS carga un servicio web, comprueba si hay un archivo web.config en la misma carpeta. Si lo hay, lo lee. El valor V de un parámetro P se obtiene mediante la instrucción:

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

donde ConfigurationSettings es una clase en el espacio de nombres System.Configuration.

Comprobemos esta técnica en el siguiente servicio web:


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

Imports System.Web.Services
imports System.Configuration

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

   ' atributos
   Private nom As String
   Private age As Integer

   ' constructor
   Public Sub New()
      ' inicialización de atributos
      nom = ConfigurationSettings.AppSettings("nom")
      age = Integer.Parse(ConfigurationSettings.AppSettings("age"))
   End Sub

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

End Class 

El servicio web personne tiene dos atributos, nom y age, que se inicializan en su constructor sin parámetros a partir de los valores leídos en el archivo de configuración web.config del servicio personne. Este archivo es el siguiente:


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

El servicio web tiene además un <WebMethod> id sin parámetros y que se limita a devolver los atributos nom y age. El servicio está registrado en el archivo fuente personne.asmx, que se encuentra junto con su archivo de configuración en la carpeta c:\inetpub\wwwroot\st\personne:

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

Asociemos una carpeta virtual IIS /config a la carpeta física anterior. Iniciemos IIS y, a continuación, desde un navegador, solicitemos la URL http://localhost/config/personne.asmx del servicio personne:

Image

Sigamos el enlace del único método id:

Image

El método id no tiene parámetros. Utilicemos el botón Appeler:

Image

Hemos recuperado correctamente la información contenida en el archivo web.config del servicio.

10.10. El servicio web IMPOTS

Retomamos la ya conocida aplicación IMPOTS. La última vez que trabajamos con ella, la convertimos en un servidor remoto al que se podía acceder a través de Internet. Ahora la convertimos en un servicio web.

10.10.1. El servicio web

Partimos de la clase impôt creada en el capítulo sobre bases de datos y que se construye a partir de la información contenida en una base de datos ODBC:


' opciones
Option Strict On
Option Explicit On 

' espacios de nombres
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections

Public Class impôt
    ' los datos necesarios para el cálculo del impuesto
    ' proceden de una fuente externa
    Private limites(), coeffR(), coeffN() As Decimal

    ' creador
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' se comprueba que las 3 tablas tengan el mismo tamaño
        Dim OK As Boolean = LIMITES.Length = COEFFR.Length And LIMITES.Length = COEFFN.Length
        If Not OK Then
            Throw New Exception("Les 3 tableaux fournis n'ont pas la même taille(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
        End If
        ' Todo correcto
        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)
        ' inicializa los tres tableros límite, coeffR, coeffN a partir
        ' del contenido de la tabla Timpots de la base ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
        ' puede lanzar una excepción
        Dim connectString As String = "DSN=" + DSNimpots + ";"        ' chaîne de connexion à la base
        Dim impotsConn As OdbcConnection = Nothing        ' la connexion
        Dim sqlCommand As OdbcCommand = Nothing        ' la commande SQL
        ' la consulta SELECT
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' tablas para recuperar los datos
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' se intenta acceder a la base de datos
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' se crea un objeto de comando
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' se ejecuta la consulta
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' Análisis de la tabla recuperada
        While myReader.Read()
            ' los datos de la línea actual se introducen en las tablas
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' liberación de recursos
        myReader.Close()
        impotsConn.Close()

        ' las tablas dinámicas se convierten en tablas estáticas
        Me.limites = New Decimal(tLimites.Count) {}
        Me.coeffR = New Decimal(tLimites.Count) {}
        Me.coeffN = New Decimal(tLimites.Count) {}
        Dim i As Integer
        For i = 0 To tLimites.Count - 1
            limites(i) = Decimal.Parse(tLimites(i).ToString())
            coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
            coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
        Next i
    End Sub

    ' cálculo del impuesto
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' cálculo del número de participaciones
        Dim nbParts As Decimal
        If marié Then
            nbParts = CDec(nbEnfants) / 2 + 2
        Else
            nbParts = CDec(nbEnfants) / 2 + 1
        End If
        If nbEnfants >= 3 Then
            nbParts += 0.5D
        End If
        ' cálculo de la renta imponible y del coeficiente familiar
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' cálculo del impuesto
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' Devolución del resultado
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

En el servicio web, solo se puede utilizar un constructor sin parámetros. Por lo tanto, el constructor de la clase quedará así:


    ' constructor
    Public Sub New()
        ' inicializa las tres tablas de límites, coeffR, coeffN a partir
        ' del contenido de la tabla Timpots de la base ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
        ' puede lanzar una excepción

        ': se recuperan los parámetros de configuración del servicio
        Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
        Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
        Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
        Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
        Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")

        ': se consulta la base de datos
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' chaîne de connexion à la base

Los cinco parámetros del constructor de la clase anterior se leen ahora en el archivo web.config del servicio. El código del archivo fuente impots.asmx es el siguiente. Recoge la mayor parte del código anterior. Nos hemos limitado a enmarcar las partes del código propias del servicio web:


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

' creación de un servicio web de impuestos
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 impôt
    Inherits WebService

    ' los datos necesarios para el cálculo del impuesto
    ' provienen de una fuente externa
    Private limites(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""


    ' constructor
    Public Sub New()
        ' inicializa las tres tablas de límites, coeffR, coeffN a partir
        ' del contenido de la tabla Timpots de la base ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
        ' puede lanzar una excepción

        ': se recuperan los parámetros de configuración del servicio
        Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
        Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
        Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
        Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
        Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")

        ': se consulta la base de datos
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' chaîne de connexion à la base
        Dim impotsConn As OdbcConnection = Nothing     ' la connexion
        Dim sqlCommand As OdbcCommand = Nothing     ' la commande SQL
        Dim myReader As OdbcDataReader     ' lecteur de données Odbc

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

        ' tablas para recuperar los datos
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' se intenta acceder a la base de datos
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' se crea un objeto de comando
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' se ejecuta la consulta
            myReader = sqlCommand.ExecuteReader()
            ' Explotación de la tabla recuperada
            While myReader.Read()
                ' los datos de la línea actual se colocan en las tablas
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' Liberación de recursos
            myReader.Close()
            impotsConn.Close()

            ' las tablas dinámicas se colocan en tablas estáticas
            Me.limites = New Decimal(tLimites.Count) {}
            Me.coeffR = New Decimal(tLimites.Count) {}
            Me.coeffN = New Decimal(tLimites.Count) {}
            Dim i As Integer
            For i = 0 To tLimites.Count - 1
                limites(i) = Decimal.Parse(tLimites(i).ToString())
                coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
                coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
            Next i
            ' Todo listo
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' error
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub

    ' cálculo del impuesto
    <WebMethod()> _
    Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' cálculo del número de participaciones
        Dim nbParts As Decimal
        If marié Then
            nbParts = CDec(nbEnfants) / 2 + 2
        Else
            nbParts = CDec(nbEnfants) / 2 + 1
        End If
        If nbEnfants >= 3 Then
            nbParts += 0.5D
        End If
        ' cálculo de la renta imponible y del coeficiente familiar
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' cálculo del impuesto
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' Devolución del resultado
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function

    ' id
    <WebMethod()> _
    Function id() As String
        ' para comprobar si todo está bien OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

Explicaremos las pocas modificaciones realizadas en la clase impots, aparte de las necesarias para convertirla en un servicio web:

  • la lectura de la base de datos en el constructor puede fallar. Por ello, hemos añadido dos atributos a nuestra clase y un método:
    • el booleano OK toma el valor vrai si se ha podido leer la base de datos, y faux en caso contrario
    • la cadena errMessage contiene un mensaje de error si no se ha podido leer la base de datos.
    • El método id sin parámetros permite obtener el valor de estos dos atributos.
  • Para gestionar el posible error de acceso a la base de datos, la parte del código del constructor relacionada con este acceso se ha rodeado de un try-catch.

El archivo web.config de configuración del servicio es el siguiente:


<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>

Durante un primer intento de carga del servicio impots, el compilador indicó que no encontraba el espacio de nombres Microsoft.Data.Odbc utilizado en la directiva:

Imports Microsoft.Data.Odbc

Tras consultar la documentación

  • se añadió una directiva de compilación en web.config para indicar que se debía utilizar el ensamblador Microsoft.Data.odbc
  • Se ha colocado una copia del archivo microsoft.data.odbc.dll en la carpeta bin del proyecto. El compilador de un servicio web explora sistemáticamente esta carpeta cuando busca un «assembly».

Parecen posibles otras soluciones, pero no se han analizado en profundidad aquí. Por lo tanto, el archivo de configuración ha pasado a ser:


<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>

Contenido de la carpeta impots\bin:

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

El servicio y su archivo de configuración se han colocado en impots:

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

La carpeta física del servicio web se ha asociado a la carpeta virtual /impots de IIS. La página del servicio es entonces la siguiente:

Image

Si seguimos el enlace id:

Image

Si se utiliza el botón Appeler:

Image

El resultado anterior muestra los valores de los atributos OK (true) y errMessage (""). En este ejemplo, la base de datos se ha cargado correctamente. No siempre ha sido así y por eso hemos añadido el método id para tener acceso al mensaje de error. El error era que el nombre DSN de la base de datos se había definido como usuario DSN, cuando debía definirse como sistema DSN. Esta distinción se realiza en el gestor de fuentes ODBC de 32 bits:

Volvamos a la página del servicio:

Image

Sigamos el enlace calculer:

Image

Definimos los parámetros de la llamada y la ejecutamos:

Image

El resultado es correcto.

10.10.2. Generar el proxy del servicio impots

Ahora que tenemos un servicio web impots operativo, podemos generar su clase proxy. Recordemos que esta será utilizada por las aplicaciones cliente para acceder al servicio web impots de forma transparente. En primer lugar, utilizamos la utilidad wsdl para generar el archivo fuente de la clase proxy y, a continuación, este se compila en una DLL.

dos>wsdl /language=vb http://localhost/impuestos/impots.asmx
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.

Écriture du fichier 'D:\data\serge\devel\vbnet\poly\chap9\impots\impots.vb'.

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

dos>vbc /t:library /r:system.dll /r:system.web.services.dll /r:system.xml.dll impots.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4
pour Microsoft (R) .NET Framework version 1.1.4322.573
Copyright (C) Microsoft Corporation 1987-2002. Tous droits réservés.

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

10.10.3. Usar el proxy con un cliente

En el capítulo sobre bases de datos creamos una aplicación de consola que permite calcular el impuesto:

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

dos>testimpots
pg DSNimpots tabImpots colLimites colCoeffR colCoeffN

dos>testimpots odbc-mysql-dbimpots impots limites coeffr coeffn
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F

El programa testimpots utilizaba entonces la clase impôt clásica, la contenida en el archivo impots.dll. El código del programa testimpots.vb era el siguiente:


Option Explicit On 
Option Strict On

' espacios de nombres
Imports System
Imports Microsoft.VisualBasic

' página de prueba
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' programa interactivo de cálculo de impuestos
        ' el usuario introduce tres datos con el teclado: casado nbEnfants salario
        ' el programa muestra entonces el impuesto a pagar
        Const syntaxe1 As String = "pg DSNimpots tabImpots colLimites colCoeffR colCoeffN"
        Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

        ' verificación de los parámetros del programa
        If arguments.Length <> 5 Then
            ' mensaje de error
            Console.Error.WriteLine(syntaxe1)
            ' fin
            Environment.Exit(1)
        End If        'if
        ' se recuperan los argumentos
        Dim DSNimpots As String = arguments(0)
        Dim tabImpots As String = arguments(1)
        Dim colLimites As String = arguments(2)
        Dim colCoeffR As String = arguments(3)
        Dim colCoeffN As String = arguments(4)

        ' creación de un objeto de impuesto
        Dim objImpôt As impôt = Nothing
        Try
            objImpôt = New impôt(DSNimpots, tabImpots, colLimites, colCoeffR, colCoeffN)
        Catch ex As Exception
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
            Environment.Exit(2)
        End Try

        ' bucle infinito
        While True
            ' al principio no hay errores
            Dim erreur As Boolean = False

            ' se solicitan los parámetros para el cálculo del impuesto
            Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
            Dim paramètres As String = Console.In.ReadLine().Trim()

            ' ¿Hay algo que hacer?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If

            ' verificación del número de argumentos en la línea introducida
            Dim args As String() = paramètres.Split(Nothing)
            Dim nbParamètres As Integer = args.Length
            If nbParamètres <> 3 Then
                Console.Error.WriteLine(syntaxe2)
                erreur = True
            End If
            Dim marié As String
            Dim nbEnfants As Integer
            Dim salaire As Integer
            If Not erreur Then
                ' Comprobación de la validez de los parámetros
                ' casado
                marié = args(0).ToLower()
                If marié <> "o" And marié <> "n" Then
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                    erreur = True
                End If
                ' nbEnfants
                nbEnfants = 0
                Try
                    nbEnfants = Integer.Parse(args(1))
                    If nbEnfants < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
                    erreur = True
                End Try
                ' salario
                salaire = 0
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
                    erreur = True
                End Try
            End If
            If Not erreur Then
                ' los parámetros son correctos: se calcula el impuesto
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

Retomamos el mismo programa para que ahora utilice el servicio web impots a través de la clase proxy impots creada anteriormente. Nos vemos obligados a modificar ligeramente el código:

  • mientras que la clase impôt original tenía un constructor con cinco argumentos, la clase proxy impots tiene un constructor sin parámetros. Los cinco parámetros, como hemos visto, ahora están fijados en el archivo de configuración del servicio web.
  • Por lo tanto, ya no es necesario pasar estos cinco parámetros como argumentos al programa de prueba

El nuevo código es el siguiente:


Imports System
Imports Microsoft.VisualBasic

' página de prueba
Module testimpots

    Public Sub Main(ByVal arguments() As String)
        ' programa interactivo de cálculo de impuestos
        ' el usuario introduce tres datos con el teclado: casado nbEnfants salario
        ' el programa muestra entonces el impuesto a pagar
        Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"

        ' creación de un objeto de impuestos
        Dim objImpôt As impôt = Nothing
        Try
            objImpôt = New impôt
        Catch ex As Exception
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
            Environment.Exit(2)
        End Try

        ' bucle infinito
        Dim erreur As Boolean
        While True
            ' al principio no hay error
            erreur = False
            ' se solicitan los parámetros para el cálculo del impuesto
            Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
            Dim paramètres As String = Console.In.ReadLine().Trim()
            ' ¿Hay algo que hacer?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' verificación del número de argumentos en la línea introducida
            Dim args As String() = paramètres.Split(Nothing)
            Dim nbParamètres As Integer = args.Length
            If nbParamètres <> 3 Then
                Console.Error.WriteLine(syntaxe2)
                erreur = True
            End If
            If Not erreur Then
                ' verificación de la validez de los parámetros
                ' casado
                Dim marié As String = args(0).ToLower()
                If marié <> "o" And marié <> "n" Then
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                    erreur = True
                End If
                ' nbEnfants
                Dim nbEnfants As Integer = 0
                Try
                    nbEnfants = Integer.Parse(args(1))
                    If nbEnfants < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument nbEnfants incorrect : tapez un entier positif ou nul"))
                    erreur = True
                End Try
                ' salario
                Dim salaire As Integer = 0
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument salaire incorrect : tapez un entier positif ou nul"))
                    erreur = True
                End Try
                ' si los parámetros son correctos, se calcula el impuesto
                If Not erreur Then Console.Out.WriteLine(("impôt=" + objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

Tenemos el proxy impots.dll y el código fuente testimpots en la misma carpeta.

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

Compilamos el código fuente testimpots.vb:

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

y luego lo ejecutamos:

dos>testimpots
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F

Obtenemos el resultado esperado.