Skip to content

10. Servicios web

10.1. Introducción

En el capítulo anterior presentamos varias aplicaciones cliente-servidor TCP/IP. Dado que los clientes y el servidor intercambian líneas de texto, pueden escribirse en cualquier lenguaje. El cliente simplemente 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 alojadas en servidores web y, por lo tanto, el protocolo de intercambio cliente-servidor es HTTP (Protocolo de transporte HyperText), un protocolo que se ejecuta sobre TCP-IP.
  • El servicio web cuenta con un protocolo de comunicación estándar, independientemente del servicio que preste. Un servicio web ofrece diversos servicios: S1, S2, …, Sn. Cada uno de ellos espera 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 proporcionarle y su tipo
    • el tipo de resultado que devuelve el servicio

Una vez conocidos estos elementos, el diálogo entre el cliente y el servidor sigue el mismo formato, independientemente del servicio web al que se consulte. De esta manera, la comunicación de los clientes queda estandarizada.

  • Por razones de seguridad frente a ataques procedentes de Internet, muchas organizaciones cuentan con redes privadas y solo abren al exterior ciertos 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 como se presentaron en el capítulo anterior, se desarrollan 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 que ofrece se convierten así en métodos de ese objeto. Un cliente puede acceder a ese objeto remoto como si fuera local. Esto oculta toda la parte de comunicación de red y permite desarrollar un cliente independiente de esa capa. Si esta llegara a cambiar, no sería necesario modificar el cliente. Esta es una ventaja enorme 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, tanto el cliente como 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 (Protocolo de Acceso a Objetos Simples). 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 manera diferente al recibir este flujo XML. Internet Explorer cuenta con una hoja de estilo predefinida que permite mostrarlo. Netscape Communicator, por su parte, no cuenta con 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 tener acceso 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 revisa el código fuente de la página recibida por Netscape, se obtiene:

Image

Netscape recibió lo mismo que Internet Explorer, pero lo mostró de manera 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 que comprendamos esta arquitectura, podremos empezar a valernos por nosotros mismos. Ese 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 (panel de la derecha)
  • la ubicación es libre. En este caso, el servicio web será alojado por un servidor web local IIS. Por lo tanto, su URL será http://localhost/[chemin], donde [chemin] 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 publica. Llamemos a esta raíz <IISroot>. Esta 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> suele ser 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 que están en la carpeta física del proyecto. Lo más interesante para nosotros es el archivo con la extensión asmx. Esa es la extensión de los servicios web. Un servicio web es administrado por VS.NET como una aplicación de Windows, c.a.d: una aplicación que cuenta con una interfaz gráfica y código para administrarla. Por eso, tenemos una ventana de diseño:

Image

Un servicio web normalmente no tiene interfaz gráfica. Representa un objeto al que se puede llamar de forma remota. Cuenta con 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 ello, no utilizaremos la ventana de diseño que presenta VS.NET. Centrémonos mejor 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 al contenido del archivo Service1.asmx un poco más adelante.
  • Aparece 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()

        'Agregue su código de inicialización después de la llamada a InitializeComponent()

    End Sub

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

    'REMARQUE: el siguiente procedimiento es requerido por el Diseñador de servicios web
    '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 generarlo, no comente las siguientes líneas, luego guarde y genere el proyecto.
    ' Para probar este servicio web, asegúrate de que el archivo .asmx sea la página de inicio
    ' y presione F5.
    '
    '<WebMethod()> Función pública HelloWorld() As String
    '    HelloWorld = "Hola, mundo"
    ' 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 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 similar a los espacios de nombres de las clases. VS.NET generó automáticamente código que colocó 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 al crear aplicaciones de Windows. Es un código que podemos eliminar sin más si no contamos con 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 generarlo, no comente las siguientes líneas, luego guarde y genere el proyecto.
    ' Para probar este servicio web, asegúrate de que el archivo .asmx sea la página de inicio
    ' y presione F5.
    '
    '<WebMethod()> Función pública HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' Fin de la función

Con base en lo que acabamos de ver, 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 se ve un poco más claro.

  • Un servicio web es una clase que deriva 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 con 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 implementa nuestro servicio web se llama Bonjour y tiene un único método, que también se llama Bonjour, el cual 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 (por lo general, Internet Explorer, si está disponible) 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. Esto se hace haciendo clic con el botón derecho del ratón sobre el archivo .asmx en cuestión y seleccionando la opción [Définir comme page de démarrage].

Image

Quizás nos interese 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 también es el nombre del proyecto.

Volvamos a la página a la que se accede mediante la URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

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

Image

Ups... esto es un XML bastante críptico. Sin embargo, fijémonos 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. Regresemos a la página inicial 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 acortado deliberadamente la página obtenida para no sobrecargar nuestra demostración. Observemos nuevamente la URL obtenida:

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

Si escribimos directamente esta URL en un navegador, obtendremos lo mismo que antes. Se nos invita a usar el botón [Appeler]. Hagámoslo. Obtenemos 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 enfocaremos 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 llamado getBonjour. Se ha ubicado en el archivo demo2.vb, el cual a su vez se encuentra en la estructura 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 ensamblaje 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 archivo al que accederán los clientes web. Su contenido es el siguiente:

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

Ya hemos visto 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, especialmente 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 accedemos con un navegador a la URL http://localhost/polyvbnet/demo2/demo2.asmx:

Image

Luego, la URL http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Luego, el 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] que aparece arriba:

Image

Efectivamente, obtenemos 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í demostramos que basta con un solo 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 ahora se encuentra 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 archivo en la carpeta <IISroot>\polyvbnet\demo3:

Image

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

Image

Notamos una diferencia importante con respecto a la versión anterior: no tuvimos que compilar el código VB del servicio. IIS realizó esta compilación por sí mismo mediante el compilador VB.NET instalado en la misma máquina. Luego, generó la página. Si se produce un error de compilación, este será reportado por IIS:

Image

10.3.4. Versión 4

Aquí nos enfocamos 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í demostramos que podemos ubicar 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. Ejecutemos 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] que aparece arriba:

Image

Con el botón [Parcourir], seleccionamos la carpeta física que contiene el servicio web, en este caso la carpeta [D:\data\devel\vbnet\poly\chap9\demo3]. Le damos un nombre lógico (virtual) a esta carpeta: [virdemo3]. Esto significa que se podrá acceder a los documentos que se encuentran dentro de la carpeta física [D:\data\devel\vbnet\poly\chap9\demo3] 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 como están. Confirmamos el cuadro de diálogo. La nueva carpeta virtual aparece en la lista de carpetas virtuales de IIS:

Image

Ahora abrimos un navegador y accedemos a 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. En adelante, 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 esta manera, no necesitaremos VS.NET. Sin embargo, cabe destacar la utilidad de utilizar VS.NET por la ayuda que brinda en 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

Consideremos un servicio web que ofrece cinco funciones:

  1. sumar(a,b), que devuelve 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 

Aquí retomamos algunas explicaciones que ya se han dado, pero que vale la pena recordar o completar. La clase operations se parece a una clase VB.NET, aunque hay algunos puntos a destacar:

  • los métodos van precedidos por un atributo <WebMethod()> que indica al compilador cuáles son los métodos que deben «publicarse» (c.a.d) y ponerse a disposición del cliente. Un método que no esté precedido por este atributo sería invisible para los clientes remotos. Podría tratarse de un método interno utilizado por otros métodos, pero que no está 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, por ejemplo, se podría prescindir de ella.
  • La clase misma va precedida de un atributo <WebService(Namespace="st.istia.univ-angers.fr")> destinado a asignar 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 así evitar conflictos con clases de otros proveedores que podrían 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 un 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 se debe 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 que aparece nos invita a 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. Al hacer clic en el botón Appeler, se obtiene la siguiente respuesta en una ventana separada del navegador:

Image

Si en el ejemplo anterior se utiliza [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

Usemos el botón [Appeler] que aparece 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 presentan 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. Ahora los analizaremos uno por uno.

Nota: en las primeras versiones de VS.NET, existía un tercer método llamado 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 crear servicios web que acepten solicitudes GET, especialmente con otras herramientas distintas a VS.NET o simplemente de forma manual.

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 la ruta URL /operations/operations.asmx/añadir, de acuerdo con 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 volvió obligatorio a partir de la versión 1.1 del protocolo HTTP
Content-Type: application/x-www-form-urlencoded
Aquí se especifica que, después de los encabezados HTTP, se enviarán parámetros adicionales en formato urlencoded. Este formato reemplaza ciertos caracteres por su código hexadecimal.
Content-length: 7
Este es el tamaño en caracteres de la cadena de parámetros que se enviará después de los encabezados HTTP.

Los encabezados HTTP van seguidos de una línea en blanco y, a continuación, de la cadena de parámetros del POST de [Content-Length] caracteres en el formato a=XX&b=YY, donde XX y YY son las cadenas «codificadas como URL» de los valores de los parámetros a y b. Sabemos lo suficiente para reproducir lo anterior con nuestro cliente TCP genérico que ya utilizamos 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 señalar que hemos agregado el encabezado [Connection: close] para indicarle al servidor que cierre la conexión después de enviar la respuesta. Esto es necesario en este caso. Si no se especifica, 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 después de 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 bloquea.

Inmediatamente después de recibir la línea vacía que indica el fin 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, le 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 afecta al servidor, ya que de los caracteres recibidos solo tomará los primeros 7 y, además, la conexión se cierra a continuación (Connection: close). Estos caracteres de más habrían sido un problema si la conexión se hubiera mantenido abierta, ya que en ese caso 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 contamos con 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/operaciones/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 invoca al cliente pasándole el URL del servicio web:

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

A continuación, el cliente lee los comandos ingresados desde 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 operará dicha función. Por ejemplo:

ajouter 6 7

A partir de ahí, el cliente enviará 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 vimos con el cliente TCP genérico, con una diferencia: el encabezado HTTP Connection: Keep-Alive le pide al servidor que no cierre la conexión. Por lo tanto, esta permanece abierta para la siguiente operación del cliente, quien así no necesita volver a conectarse al servidor. Sin embargo, esto lo obliga a utilizar un método distinto al 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]

Analicemos 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 toma nota de la URI solicitada
        Dim URIstring As String = args(0)

        ' se establece la conexión con el 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 y 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 ingresan 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
        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 entrada de comandos escritos en el teclado
            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 invá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)
        ' cierre con error
        Environment.Exit(exitCode)
    End Sub
End Module

Aquí hay elementos que ya hemos visto varias veces y que no requieren comentarios especiales. Analicemos 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 entre cliente y 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 para 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 para determinar 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 algo?
            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 para 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á detrás de 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 específico en una consulta POST (el espacio, el signo =, el signo &, etc.). En este caso, el método HttpUtility.UrlEncode normalmente no es necesario, ya que a y b son números que no contienen ninguno de estos caracteres especiales. Aquí se utiliza a modo de ejemplo. Requiere 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 simplemente lee y muestra 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 consta de dos partes:

  1. los encabezados HTTP, que terminan con 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 obtener el tamaño de la respuesta XML (en este caso, 90). Este valor se obtiene mediante una expresión regular. Se podría haber hecho de otra manera y, sin duda, de forma más eficiente.


        ' construcción de la expresión regular para determinar 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 en 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 del 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 en pantalla.

[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 algo?
            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 del tipo SOAP (Protocolo de Acceso a Objetos Simples). 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, volveremos 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 el formato

    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, que también es 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 sean más complejas, se trata del mismo mecanismo HTTP que para el cliente HTTP-POST. Por lo tanto, el código del cliente SOAP puede basarse en el 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 entre cliente y 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 para 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 <> ""
            'salida en pantalla
            Console.Out.WriteLine(("<-- " + ligne))
            ' línea siguiente
            ligne = [IN].ReadLine()
        End While        'while
        'eco de la última línea
        Console.Out.WriteLine(("<-- " + ligne))

Ahora el cliente enviará sus parámetros en formato XML dentro de lo que se conoce como 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))

El servidor enviará entonces 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 enviar la solicitud
        ' se almacena el valor de la línea Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' salida en 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 que se conoce 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 algo?
            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 entre cliente y servidor

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

La aplicación cliente se comunicaría con la interfaz cliente-servidor para realizar sus solicitudes al servicio web. Esta interfaz se encargaría de todas las comunicaciones de red necesarias con el servidor y devolvería el resultado obtenido a la aplicación cliente. De esta manera, la aplicación cliente ya no tendría que ocuparse de las comunicaciones con el servidor, lo que facilitaría enormemente su desarrollo.

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. Decidimos 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"}
    ' detallado
    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)

        ' modo detallado
        Me.verbose = verbose

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

        ' se crean los flujos de entrada y 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 con el servidor
    Public Sub Close()
        ' fin de la conexión entre el cliente y el 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 la URI URI
        ' los intercambios entre cliente y 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

        ' ¿Se está construyendo la tabla de encabezados HTTP que se 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 <> ""
            'salida en pantalla
            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 determinar 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 en 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
            ' ¿qué se encontró?
            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 encontró?
            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 en comparación con lo que ya hemos visto. Simplemente tomamos el código del cliente SOAP que analizamos y lo reestructuramos un poco para convertirlo en una clase. Esta 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 le pasan dos parámetros al constructor:

  1. el URI del servicio web al que debe conectarse
  2. un valor booleano verbose que, en realidad, indica 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 administradas 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 escribimos para el cliente SOAP que analizamos, 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 ese resultado y, por lo tanto, un tipo string.

Por lo general, 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 que se analizó cuando la clase clientSOAP aún no existía y lo reestructuramos para que ahora utilice dicha 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 manera interactiva los comandos ingresados desde 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 ingresan con 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

        ' solicitud 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 entrada de comandos escritos con el teclado
            While True
                ' al inicio 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

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

El cliente ahora es considerablemente más sencillo y no incluye 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 está conectando 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 ingresan con el teclado, se analizan y luego se envían al servidor mediante una llamada al método executeFonction del objeto clientSOAP.


                 ' 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())))

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

La aplicación cliente testClientSoap se compila luego 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 sin comentarios:

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 dar seguimiento a los intercambios de red solicitando una ejecución «detallada»:

dos>testClientSOAP http://localhost/operations/operations.asmx detallado
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 construyamos un cliente gráfico.

10.7.3. Un cliente gráfico para 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
el 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 operaciones (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 una 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 en «verdadero»

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

Image

A continuación, el usuario puede elegir una función y los 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 Windows Forms»

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

    'El método sobreescrito Dispose del formulario limpia 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 para establecer 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 produjo 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
        ' Cerrando 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 produjo 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 en el 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 en el 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 construyó de la siguiente manera:

  • el ensamblado clientSOAP.dll, que contiene la clase clientSOAP, se colocó en la carpeta del proyecto
  • la interfaz gráfica clientsoapgui.vb se creó con VS.NET y luego se compiló 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 las comunicaciones de red entre un cliente y un servicio web según el esquema que se muestra a continuación:

La plataforma .NET lleva esta lógica un paso más allá. Una vez que sabemos a qué servicio web queremos acceder, 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 llama un proxy para el servicio web para el cual fue generada.

¿Cómo generar la clase proxy de un servicio web? Un servicio web siempre viene 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 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 el 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.
' </autogenerado>
'------------------------------------------------------------------------------

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 fue generado automáticamente 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

     '<observaciones/>
    <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

     '<observaciones/>
    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 parece un poco complejo a primera vista. No necesitamos entender los detalles para poder usarlo. Veamos 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 fue creada. 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 mencionada anteriormente 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 se comunica con el servicio web no se muestra aquí. Esta comunicación es manejada íntegramente 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, un 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 que se ingresan desde 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 manera interactiva los comandos ingresados desde el teclado
    ' y los envía al servicio web de operaciones
    Public Sub Main()
        ' ya no hay argumentos; la URL del servicio web está codificada 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 ingresan con 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

        ' se le pide 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 entrada de los comandos escritos con el teclado
        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 invá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 de error
        System.Console.Error.WriteLine(msg)
        ' interrupción con error
        Environment.Exit(exitCode)
    End Sub
End Module

Solo analizamos 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 ingresan las líneas de función a y b mediante el teclado. A partir de esta información, se invocan los métodos apropiados 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 manejar que las respuestas simples XML de las otras funciones, que solo entregan un único resultado. Se observa que aquí, con la clase proxy, utilizar el método toutfaire no es más complicado que utilizar los otros métodos. La aplicación se compiló 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, 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 pueden colocarse en el archivo web.config de la siguiente manera:

<?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 encuentran en un paquete XML:

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

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


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

¿Cómo recupera el servicio web esta información? Cuando IIS carga un servicio web, verifica si en la misma carpeta hay un archivo web.config. Si es así, 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.

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

Además, el servicio web tiene un identificador <WebMethod> sin parámetros que simplemente devuelve 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. Ejecutemos IIS y, luego, desde un navegador, accedamos a la URL http://localhost/config/personne.asmx del servicio personne:

Image

Sigamos el enlace del único método con el identificador:

Image

El método id no tiene parámetros. Usemos 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, la cual 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
    ' provienen 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 verifica que las tres 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
        ' está bien
        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 de límites, coeffR, coeffN a partir
        ' del contenido de la tabla Timpots de la base de datos ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
        ' puede generar 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 fila actual se colocan en las tablas
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' Se liberan los recursos
        myReader.Close()
        impotsConn.Close()

        ' Las tablas dinámicas se transfieren a 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
        ' 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 de datos ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
        ' puede generar 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 procesa 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 ahora se leen del archivo web.config del servicio. El código del archivo fuente impots.asmx es el siguiente. Incluye la mayor parte del código anterior. Nos hemos limitado a resaltar las partes del código específicas 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 de datos ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
        ' puede generar 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 procesa 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()
            ' Análisis de la tabla recuperada
            While myReader.Read()
                ' Los datos de la fila actual se colocan en las tablas
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' Se liberan los recursos
            myReader.Close()
            impotsConn.Close()

            ' Las tablas dinámicas se transfieren a 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
        ' devuelve el resultado
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function

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

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

  • la lectura de la base de datos en el constructor puede fallar. Por eso, hemos agregado dos atributos a nuestra clase y un método:
    • El valor booleano OK es vrai si se pudo leer la base de datos, y faux en caso contrario
    • la cadena errMessage contiene un mensaje de error si no se pudo leer la base de datos.
    • El método id sin parámetros permite obtener el valor de estos dos atributos.
  • Para manejar un 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 de configuración del servicio web.config 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>

Al intentar cargar por primera vez el servicio impots, el compilador informó que no pudo encontrar el espacio de nombres Microsoft.Data.Odbc utilizado en la directiva:

Imports Microsoft.Data.Odbc

Tras consultar la documentación

  • se agregó una directiva de compilación en web.config para indicar que se debía utilizar el ensamblador Microsoft.Data.odbc
  • se colocó 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 «ensamblado».

Parecen existir otras soluciones, pero no se han analizado a fondo en este caso. Por lo tanto, el archivo de configuración quedó así:


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

El 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 cargó correctamente. No siempre ha sido así y por eso hemos agregado el método id para tener acceso al mensaje de error. El error radicaba en 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 administrador 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 contamos con 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 manera transparente. Primero utilizamos la utilidad wsdl para generar el archivo fuente de la clase proxy; luego, 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 que se encuentra 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 ingresa 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 inicio 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 que hacer algo?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If

            ' verificación del número de argumentos en la línea ingresada
            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
                ' Verificació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 original impôt 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 configurados 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 ingresa 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 que hacer algo?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' verificación del número de argumentos en la línea ingresada
            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

Efectivamente, obtenemos el resultado esperado.