10. Servicios web
10.1. Introducción
En el capítulo anterior hemos presentado varias aplicaciones cliente-servidor TCP/IP. Dado que los clientes y el servidor intercambian líneas de texto, pueden estar escritas en cualquier lenguaje. El cliente solo debe conocer el protocolo de comunicación que espera el servidor. Los servicios web son aplicaciones de servidor TCP/IP que presentan las siguientes características:
- Están alojados en servidores web y, por lo tanto, el protocolo de intercambio cliente-servidor es HTTP (HyperText Transport Protocol), un protocolo que se ejecuta sobre TCP-IP.
- El servicio web tiene un protocolo de diálogo estándar independientemente del servicio prestado. Un servicio web ofrece diversos servicios S1, S2, ..., Sn. Cada uno de ellos espera los parámetros proporcionados por el cliente y le devuelve un resultado. Para cada servicio, el cliente necesita saber:
- el nombre exacto del servicio Si
- la lista de parámetros que debe proporcionar y su tipo
- el tipo de resultado devuelto por el servicio
Una vez conocidos estos elementos, el diálogo cliente-servidor sigue el mismo formato independientemente del servicio web consultado. De este modo, la comunicación de los clientes queda estandarizada.
- Por razones de seguridad frente a los ataques procedentes de Internet, muchas organizaciones disponen de redes privadas y solo abren al exterior determinados puertos de sus servidores: principalmente el puerto 80 del servicio web. Todos los demás puertos están bloqueados. Por lo tanto, las aplicaciones cliente-servidor tal y como se presentan en el capítulo anterior se construyen dentro de la red privada (intranet) y, por lo general, no son accesibles desde el exterior. Alojar un servicio en un servidor web lo hace accesible a toda la comunidad de Internet.
- El servicio web puede modelarse como un objeto remoto. Los servicios ofrecidos se convierten entonces en métodos de este objeto. Un cliente puede acceder a este objeto remoto como si fuera local. Esto oculta toda la parte de comunicación de red y permite construir un cliente independiente de esta capa. Si esta cambia, no es necesario modificar el cliente. Esta es una enorme ventaja y probablemente el principal punto fuerte de los servicios web.
- Al igual que en las aplicaciones cliente-servidor TCP/IP presentadas en el capítulo anterior, el cliente y el servidor pueden estar escritos en cualquier lenguaje. Intercambian líneas de texto. Estas constan de dos partes:
- los encabezados necesarios para el protocolo HTTP
- el cuerpo del mensaje. En el caso de una respuesta del servidor al cliente, este tiene el formato XML (eXtensible Markup Language). En el caso de una solicitud del cliente al servidor, el cuerpo del mensaje puede adoptar varias formas, entre ellas XML. La solicitud XML del cliente puede tener un formato específico denominado SOAP (Simple Object Access Protocol). En este caso, la respuesta del servidor también sigue el formato SOAP.
10.2. Los navegadores y XML
Los servicios web envían XML a sus clientes. Los navegadores pueden reaccionar de forma diferente al recibir este flujo XML. Internet Explorer tiene una hoja de estilo predefinida que permite mostrarlo. Netscape Communicator no dispone de esta hoja de estilo y no muestra el código XML recibido. Es necesario visualizar el código fuente de la página recibida para acceder al XML. A continuación se muestra un ejemplo para el siguiente código XML:
<?xml version="1.0" encoding="utf-8"?>
<string xmlns="st.istia.univ-angers.fr">bonjour de nouveau !</string>
Internet Explorer mostrará la siguiente página:

mientras que Netscape Navigator mostrará:

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

Netscape ha recibido lo mismo que Internet Explorer, pero lo ha mostrado de forma diferente. A partir de ahora, utilizaremos Internet Explorer para las capturas de pantalla.
10.3. Un primer servicio web
Vamos a descubrir los servicios web a través de un ejemplo muy sencillo que se presenta en tres versiones.
10.3.1. Versión 1
Para esta primera versión utilizaremos VS.NET, que tiene la ventaja de poder generar un esqueleto de servicio web inmediatamente operativo. Una vez comprendida esta arquitectura, podremos empezar a valernos por nosotros mismos. Este será el objetivo de las siguientes versiones.
Con VS.NET, creemos un nuevo proyecto con la opción [Fichier/Nouveau/Projet]:

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

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:

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

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

Hay varios puntos a destacar:
- el archivo se llama Service1.asmx.vb y no Service1.asmx. Volveremos sobre el contenido del archivo Service1.asmx un poco más adelante.
- Encontramos una ventana de código similar a la que teníamos cuando creábamos aplicaciones de Windows con VS.NET
El código generado por VS.NET es el siguiente:
Imports System.Web.Services
<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
Inherits System.Web.Services.WebService
#Región «Código generado por el Diseñador de servicios web»
Public Sub New()
MyBase.New()
'Esta llamada es requerida por el Diseñador de servicios web.
InitializeComponent()
'Añada su código de inicialización después de la llamada InitializeComponent()
End Sub
'Requerido por el Diseñador de servicios web
Private components As System.ComponentModel.IContainer
'REMARQUE: el Diseñador de servicios web requiere el siguiente procedimiento
'Se puede modificar utilizando el Diseñador de servicios web.
'No lo modifique utilizando el editor de código.
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
components = New System.ComponentModel.Container()
End Sub
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
'CODEGEN: este procedimiento es requerido por el Diseñador de servicios web
'No lo modifique utilizando el editor de código.
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
#Fin de región
' EXEMPLE DE SERVICE WEB
' El ejemplo de servicio HelloWorld() devuelve la cadena «Hello World».
' Para generar, no comente las siguientes líneas, guarde y genere el proyecto.
' Para probar este servicio web, asegúrese de que el archivo .asmx sea la página de inicio
' y pulse F5.
'
'<WebMethod()> Función pública HelloWorld() As String
' HelloWorld = "Hello World"
' Fin de la función
End Class
En primer lugar, observemos que aquí tenemos una clase, la clase Service1, que deriva de la clase WebService:
Esto nos lleva a importar el espacio de nombres System.Web.Services:
Imports System.Web.Services
La declaración de la clase va precedida de un atributo de compilación:
<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
Inherits System.Web.Services.WebService
El atributo System.Web.Services.WebService() indica que la clase que le sigue es un servicio web. Este atributo admite varios parámetros, entre ellos uno llamado NameSpace. Sirve para ubicar el servicio web en un espacio de nombres. De hecho, es posible que existan varios servicios web llamados «meteo» en todo el mundo. Necesitamos una forma de diferenciarlos. El espacio de nombres es lo que lo permite. Uno podría llamarse [espacenom1].meteo y otro [espacenom2].meteo. Aquí encontramos un concepto análogo a los espacios de nombres de las clases. VS.NET ha generado automáticamente código que ha colocado en una región del código fuente:
Si observamos este código, vemos el mismo que generaba el diseñador cuando se creaban aplicaciones de Windows. Es un código que podemos eliminar sin más si no tenemos una interfaz gráfica, lo cual será nuestro caso para los servicios web.
La clase termina con un ejemplo de lo que podría ser un servicio web:
#Fin de la región
' EXEMPLE DE SERVICE WEB
' El ejemplo de servicio HelloWorld() devuelve la cadena «Hello World».
' Para generar, no comente las siguientes líneas, guarde y genere el proyecto.
' Para probar este servicio web, asegúrese de que el archivo .asmx sea la página de inicio
' y pulse F5.
'
'<WebMethod()> Función pública HelloWorld() As String
' HelloWorld = "Hola mundo"
' Fin de la función
Teniendo en cuenta lo anterior, limpiamos el código para que quede así:
Imports System.Web.Services
<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour
Inherits System.Web.Services.WebService
<WebMethod()> Public Function Bonjour() As String
Return "bonjour !"
End Function
End Class
Ahora lo vemos un poco más claro.
- Un servicio web es una clase derivada de la clase WebService
- La clase está calificada por el atributo <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. Por lo tanto, colocamos nuestro servicio en el espacio de nombres st.istia.univ-angers.fr.
- Los métodos de la clase se califican mediante un atributo <WebMethod()> que indica que se trata de un método al que se puede llamar de forma remota a través de la red
La clase que proporciona nuestro servicio web se llama, por tanto, Bonjour y tiene un único método, también llamado Bonjour, que devuelve una cadena de caracteres. Estamos listos para una primera prueba.
- Iniciemos el servidor web IIS si aún no lo hemos hecho
- utilicemos la opción Depurar/Ejecutar sin depurar. VS.NET
VS.NET compilará entonces toda la aplicación, abrirá un navegador (a menudo Internet Explorer, si está presente) y mostrará la URL http://localhost/polyvbnet/demo/Service1.asmx:

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

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

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

Abrámoslo con un editor de texto (Bloc de notas u otro). Obtenemos el siguiente contenido:
El archivo contiene una simple directiva dirigida al servidor IIS que indica:
- que se trata de un servicio web (palabra clave WebService)
- que el lenguaje de la clase de este servicio es Visual Basic (Language="vb")
- que el código fuente de esta clase se encuentra en el archivo Service1.asmx.vb (Codebehind="Service1.asmx.vb")
- que la clase que implementa el servicio se llama demo.Bonjour (Class="demo.Bonjour"). Cabe destacar que VS.NET ha colocado la clase Bonjour en el espacio de nombres demo, que es también el nombre del proyecto.
Volvamos a la página obtenida en la URL http://localhost/polyvbnet/demo/Service1.asmx:

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

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

Hemos truncado deliberadamente la página obtenida para no alargar nuestra demostración. Fijémonos de nuevo en la URL obtenida:
Si escribimos directamente esta URL en un navegador, obtendremos lo mismo que antes. Se nos anima a utilizar el botón [Appeler]. Hagámoslo. Aparece una nueva página:

De nuevo es XML. Encontramos allí dos datos que estaban presentes en nuestro servicio web:
- el espacio de nombres st.istia.univ-angers.fr de nuestro servicio
<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
- el valor devuelto por el método Bonjour:
Return "bonjour !"
¿Qué hemos aprendido?
- cómo escribir un servicio web S
- cómo llamarlo
Ahora nos centraremos en escribir un servicio web sin la ayuda de VS.NET.
10.3.2. Versión 2
En el ejemplo anterior, VS.NET hizo muchas cosas por sí solo. ¿Es posible crear un servicio web sin esta herramienta? La respuesta es sí, y ahora lo demostraremos. Con un editor de texto, crearemos el siguiente servicio web:
Imports System.Web.Services
<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour2
Inherits System.Web.Services.WebService
<WebMethod()> Public Function getBonjour() As String
Return "bonjour de nouveau !"
End Function
End Class
La clase se llama Bonjour2 y tiene un método que se llama getBonjour. Se ha colocado en el archivo demo2.vb, que a su vez se encuentra en el árbol de directorios del servidor IIS, dentro de la carpeta E:\Inetpub\wwwroot\polyvbnet\demo2. Se trata de una clase VB.NET clásica que, por lo tanto, se puede compilar:
dos>vbc /out:demo2 /t:library /r:system.dll /r:system.web.services.dll demo2.vb
dos>dir
02/03/2004 18:04 286 demo2.vb
02/03/2004 18:10 77 demo2.asmx
02/03/2004 18:12 3 072 demo2.dll
Colocamos el ensamblado demo2.dll en una carpeta llamada «bin» (este nombre es obligatorio):
Ahora creamos el archivo demo2.asmx. Este es el que llamarán los clientes web. Su contenido es el siguiente:
Ya nos hemos encontrado con esta directiva. Indica que:
- la clase del servicio web se llama Bonjour2 y se encuentra en el ensamblado demo2.dll. IIS buscará este ensamblado en diferentes ubicaciones, en particular en la carpeta bin del servicio web. Por eso hemos colocado allí el ensamblado demo2.dll.
Ahora podemos realizar diversas pruebas. Nos aseguramos de que IIS esté activo y solicitamos con un navegador la URL http://localhost/polyvbnet/demo2/demo2.asmx:

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

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

Utilizamos el botón [Appeler] anterior:

Obtenemos correctamente el resultado de la llamada al método getBonjour del servicio web. Ahora sabemos cómo construir un servicio web sin vs.net. A partir de ahora, dejaremos de lado la forma en que está construido el servicio web para centrarnos únicamente en los archivos fundamentales.
10.3.3. Versión 3
Las dos versiones anteriores del servicio web [Bonjour] utilizaban dos archivos:
- un archivo .asmx, punto de entrada del servicio web
- un archivo .vb, código fuente del servicio web
Aquí mostramos que basta con el único archivo .asmx. El código del servicio demo3.asmx es el siguiente:
Observamos que el código fuente del servicio se encuentra ahora directamente en el archivo fuente del archivo demo3.asmx. La directiva
ya no hace referencia a una clase en un ensamblado externo, sino a una clase que se encuentra en el mismo archivo fuente. Coloquemos este en la carpeta <IISroot>\polyvbnet\demo3:

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

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

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

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

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

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

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

10.3.5. Conclusión
Hemos mostrado varias formas de proceder para crear un servicio web. A partir de ahora, utilizaremos el método de la versión 3 para la creación del servicio y el método 4 para su localización. De este modo, no necesitaremos VS.NET. No obstante, cabe destacar el interés de utilizar VS.NET por la ayuda que aporta a la depuración. Existen herramientas gratuitas para desarrollar aplicaciones web, en particular el producto WebMatrix patrocinado por Microsoft, que se puede encontrar en URL [http://www.asp.net/webmatrix]. Es una herramienta excelente para iniciarse en la programación web sin necesidad de inversión previa.
10.4. Un servicio web de operaciones
Consideramos un servicio web que ofrece cinco funciones:
- sumar(a,b), que devolverá a+b
- restar(a,b), que devolverá a-b
- multiplicar(a,b), que devolverá a*b
- dividir(a,b), que devolverá a/b
- todo(a,b), que devolverá la matriz [a+b,a-b,a*b,a/b]
El código VB.NET de este servicio es el siguiente:
<%@ WebService language="VB" class=operations %>
imports system.web.services
<WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class operations
Inherits WebService
<WebMethod> _
Function ajouter(a As Double, b As Double) As Double
Return a + b
End Function
<WebMethod> _
Function soustraire(a As Double, b As Double) As Double
Return a - b
End Function
<WebMethod> _
Function multiplier(a As Double, b As Double) As Double
Return a * b
End Function
<WebMethod> _
Function diviser(a As Double, b As Double) As Double
Return a / b
End Function
<WebMethod> _
Function toutfaire(a As Double, b As Double) As Double()
Return New Double() {a + b, a - b, a * b, a / b}
End Function
End Class
Repasamos aquí algunas explicaciones ya dadas, pero que merecen ser recordadas o completadas. La clase operations se parece a una clase VB.NET, aunque hay algunos puntos a tener en cuenta:
- los métodos van precedidos de un atributo <WebMethod()> que indica al compilador los métodos que deben «publicarse» c.a.d. y ponerse a disposición del cliente. Un método que no vaya precedido de este atributo sería invisible para los clientes remotos. Podría tratarse de un método interno utilizado por otros métodos, pero no destinado a ser publicado.
- La clase deriva de la clase WebService definida en el espacio de nombres System.Web.Services. Esta herencia no siempre es obligatoria. En este ejemplo concreto, por ejemplo, se podría prescindir de ella.
- La propia clase va precedida de un atributo <WebService(Namespace="st.istia.univ-angers.fr")> destinado a proporcionar un espacio de nombres al servicio web. Un proveedor de clases asigna un espacio de nombres a sus clases para darles un nombre único y evitar así conflictos con clases de otros proveedores que pudieran tener el mismo nombre. En el caso de los servicios web, ocurre lo mismo. Cada servicio web debe poder identificarse mediante un nombre único, en este caso st.istia.univ-angers.fr.
- No hemos definido ningún constructor. Por lo tanto, se utilizará implícitamente el constructor de la clase padre.
El código fuente anterior no está destinado directamente al compilador VB.NET, sino al servidor web IIS. Debe llevar el sufijo .asmx y guardarse en el árbol de directorios del servidor web. Aquí lo guardamos con el nombre operations.asmx en la carpeta <IISroot>\polyvbnet\operations:

Asociamos a esta carpeta física la carpeta virtual IIS [operations]:
![]() | ![]() |
Accedamos al servicio con un navegador. El URl que hay que solicitar es [http://localhost/operations/operations.asmx]:

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

La página obtenida nos propone probar el método ajouter proporcionándole los dos argumentos a y b que necesita. Recordemos la definición del método ajouter:
Cabe señalar que la página ha tomado los nombres de los argumentos a y b utilizados en la definición del método. Se utiliza el botón Appeler y se obtiene la siguiente respuesta en una ventana separada del navegador:

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

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

Obtenemos la siguiente página:

Utilicemos el botón [Appeler] de arriba:

En cualquier caso, la respuesta del servidor tiene el siguiente formato:
- 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:

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



Estos dos métodos de acceso a las funciones de un servicio web se denominan, respectivamente: HTTP-POST y SOAP. Los examinaremos ahora uno tras otro.
Nota: en las primeras versiones de VS.NET, existía un tercer método denominado HTTP-GET. A la fecha de redacción de este documento (marzo de 2004), este método ya no parece estar disponible. Esto significa que el servicio web generado por VS.NET no acepta solicitudes GET. Esto no significa que no se puedan escribir servicios web que acepten solicitudes GET, en particular con otras herramientas distintas de VS.NET o simplemente a mano.
10.5. Un cliente HTTP-POST
Seguimos el método propuesto por el servicio web:

Comentemos lo que está escrito. En primer lugar, el cliente web debe enviar los siguientes encabezados HTTP:
El cliente web realiza una solicitud POST a URL /operations/operations.asmx/ajouter según el protocolo HTTP versión 1.1 | |
Se especifica la máquina de destino de la solicitud. En este caso, localhost. Este encabezado se ha convertido en obligatorio con la versión 1.1 del protocolo HTTP | |
Aquí se especifica que, tras los encabezados HTTP, se enviarán parámetros adicionales en formato urlencoded. Este formato sustituye ciertos caracteres por su código hexadecimal. | |
Es el tamaño en caracteres de la cadena de parámetros que se enviará tras los encabezados HTTP. |
Los encabezados HTTP van seguidos de una línea en blanco y, a continuación, de la cadena de parámetros de POST de [Content-Length] caracteres en el formato a=XX&b=YY, donde XX y YY son las cadenas «codificadas por URL» de los valores de los parámetros a y b. Sabemos lo suficiente como para reproducir lo anterior con nuestro cliente TCP genérico ya utilizado en el capítulo sobre programación TCP-IP:
- ejecutamos IIS
- el servicio está disponible en la URL [http://localhost/operations/operations.asmx]
- utilizamos el cliente TCP genérico en una ventana DOS
dos>clttcpgenerique localhost 80
Commandes :
POST /operations/operations.asmx/ajouter HTTP/1.1
HOST: localhost
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-length: 7
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--
a=2&b=3
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:26 GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
En primer lugar, cabe destacar que hemos añadido el encabezado [Connection: close] para solicitar al servidor que cierre la conexión tras enviar la respuesta. Esto es necesario en este caso. Si no se indica, por defecto el servidor mantendrá la conexión abierta. Sin embargo, su respuesta es una secuencia de líneas de texto cuya última línea no termina con un carácter de fin de línea. Resulta que nuestro cliente genérico TCP lee líneas de texto que terminan con el carácter de fin de línea mediante el método ReadLine. Si el servidor no cierra la conexión tras enviar la última línea, el cliente se bloquea porque espera un carácter de fin de línea que no llega. Si el servidor cierra la conexión, el método ReadLine del cliente finaliza y el cliente no se queda bloqueado.
Inmediatamente después de recibir la línea vacía que indica el final de los encabezados HTTP, el servidor IIS envía una primera respuesta:
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--
Esta respuesta, compuesta únicamente por encabezados HTTP, indica al cliente que puede enviar los 7 caracteres que dijo que quería enviar. Lo que hacemos:
Hay que tener en cuenta aquí que nuestro cliente TCP envía más de 7 caracteres, ya que los envía con un marcador de fin de línea (WriteLine). Esto no supone ningún problema para el servidor, ya que de los caracteres recibidos solo tomará los 7 primeros y, a continuación, se cerrará la conexión (Connection: close). Estos caracteres sobrantes habrían sido un problema si la conexión se hubiera mantenido abierta, ya que entonces se habrían interpretado como parte del siguiente comando del cliente. Una vez recibidos los parámetros, el servidor envía su respuesta:
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:26 GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>
Ahora disponemos de los elementos necesarios para escribir un cliente programado para nuestro servicio web. Será un cliente de consola llamado httpPost2 y se utilizará de la siguiente manera:
dos>httpPost2 http://localhost/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 6 7
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[résultat=13]
soustraire 8 9
--> POST /operations/operations.asmx/soustraire HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=8&b=9
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">-1</double>
[résultat=-1]
fin
dos>
Se llama al cliente pasándole el URL del servicio web:
A continuación, el cliente lee los comandos introducidos mediante el teclado y los ejecuta. Estos tienen el siguiente formato:
donde «función» es la función del servicio web que se invoca (sumar, restar, multiplicar, dividir) y «a» y «b» son los valores sobre los que va a operar dicha función. Por ejemplo:
A partir de ahí, el cliente realizará la solicitud HTTP necesaria al servidor web y obtendrá una respuesta. Los intercambios entre el cliente y el servidor se muestran en pantalla para facilitar la comprensión del proceso:
ajouter 6 7
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[résultat=13]
Arriba se muestra el intercambio que ya hemos visto con el cliente TCP genérico, con una diferencia: el encabezado HTTP Connection: Keep-Alive solicita al servidor que no cierre la conexión. Por lo tanto, esta permanece abierta para la siguiente operación del cliente, que así no necesita volver a conectarse al servidor. Sin embargo, esto le obliga a utilizar un método distinto de ReadLine() para leer la respuesta del servidor, ya que sabemos que esta es una secuencia de líneas cuya última no termina con un carácter de fin de línea. Una vez obtenida toda la respuesta del servidor, el cliente la analiza para encontrar el resultado de la operación solicitada y mostrarlo:
Examinemos el código de nuestro cliente:
' espacios de nombres
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web
' cliente de un servicio web de operaciones
Public Module clientPOST
Public Sub Main(ByVal args() As String)
' sintaxis
Const syntaxe As String = "pg URI"
Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
' número de argumentos
If args.Length <> 1 Then
erreur(syntaxe, 1)
End If
' se anota la URI solicitada
Dim URIstring As String = args(0)
' se conecta al servidor
Dim uri As Uri = Nothing ' l'URI du service web
Dim client As TcpClient = Nothing ' la liaison tcp du client avec le serveur
Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
Try
' conexión al servidor
uri = New Uri(URIstring)
client = New TcpClient(uri.Host, uri.Port)
' se crean los flujos de entrada-salida del cliente TCP
[IN] = New StreamReader(client.GetStream())
OUT = New StreamWriter(client.GetStream())
OUT.AutoFlush = True
Catch ex As Exception
' URI incorrecto u otro problema
erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
End Try
' creación de un diccionario de funciones del servicio web
Dim dicoFonctions As New Hashtable
Dim i As Integer
For i = 0 To fonctions.Length - 1
dicoFonctions.Add(fonctions(i), True)
Next i
' las solicitudes del usuario se introducen mediante el teclado
' en la forma función a b
' terminan con el comando fin
Dim commande As String = Nothing ' commande tapée au clavier
Dim champs As String() = Nothing ' champs d'une ligne de commande
Dim fonction As String = Nothing ' nom d'une fonction du service web
Dim a, b As String ' les arguments des fonctions du service web
' solicita al usuario
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")
' gestión de errores
Dim erreurCommande As Boolean
Try
' bucle de introducción de comandos tecleados
While True
' sin errores al inicio
erreurCommande = False
' lectura del comando
commande = Console.In.ReadLine().Trim().ToLower()
' ¿Terminado?
If commande Is Nothing Or commande = "fin" Then
Exit While
End If
' descomposición del comando en campos
champs = Regex.Split(commande, "\s+")
Try
' se necesitan tres campos
If champs.Length <> 3 Then
Throw New Exception
End If
' el campo 0 debe ser una función reconocida
fonction = champs(0)
If Not dicoFonctions.ContainsKey(fonction) Then
Throw New Exception
End If
' el campo 1 debe ser un número válido
a = champs(1)
Double.Parse(a)
' el campo 2 debe ser un número válido
b = champs(2)
Double.Parse(b)
Catch
' comando no válido
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
erreurCommande = True
End Try
' se envía la solicitud al servicio web
If Not erreurCommande Then executeFonction([IN], OUT, uri, fonction, a, b)
End While
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
' fin de la conexión cliente-servidor
Try
[IN].Close()
OUT.Close()
client.Close()
Catch
End Try
End Sub
...........
' visualización de errores
Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
' visualización del error
System.Console.Error.WriteLine(msg)
' parada con error
Environment.Exit(exitCode)
End Sub
End Module
Aquí encontramos elementos que ya hemos visto varias veces y que no requieren comentarios especiales. Examinemos ahora el código del método executeFonction, donde se encuentran las novedades:
' executeFonction
Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
' ejecuta la función (a,b) en el servicio web de URI uri
' los intercambios cliente-servidor se realizan a través de los flujos IN y OUT
' el resultado de la función se encuentra en la línea
' <double xmlns="st.istia.univ-angers.fr">double</double>
' enviada por el servidor
' construcción de la cadena de consulta
Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
Dim nbChars As Integer = requête.Length
' construcción de la tabla de encabezados HTTP a enviar
Dim entetes(5) As String
entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
entetes(1) = "Host: " & uri.Host & ":" & uri.Port
entetes(2) = "Content-Type: application/x-www-form-urlencoded"
entetes(3) = "Content-Length: " & nbChars
entetes(4) = "Connection: Keep-Alive"
entetes(5) = ""
' se envían los encabezados HTTP al servidor
Dim i As Integer
For i = 0 To entetes.Length - 1
' envío al servidor
OUT.WriteLine(entetes(i))
' salida a pantalla
Console.Out.WriteLine(("--> " + entetes(i)))
Next i
' se lee la primera respuesta del servidor web HTTP/1.1 100
Dim ligne As String = Nothing
' una línea del flujo de lectura
ligne = [IN].ReadLine()
While ligne <> ""
'eco
Console.Out.WriteLine(("<-- " + ligne))
' línea siguiente
ligne = [IN].ReadLine()
End While
'eco de la última línea
Console.Out.WriteLine(("<-- " + ligne))
' envío de los parámetros de la solicitud
OUT.Write(requête)
' eco
Console.Out.WriteLine(("--> " + requête))
' construcción de la expresión regular que permite obtener el tamaño de la respuesta XML
' en el flujo de la respuesta del servidor web
Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
Dim RegexLength As New Regex(modèleLength) '
Dim MatchLength As Match = Nothing
Dim longueur As Integer = 0
' lectura de la segunda respuesta del servidor web tras el envío de la solicitud
' se almacena el valor de la línea Content-Length
ligne = [IN].ReadLine()
While ligne <> ""
' salida a pantalla
Console.Out.WriteLine(("<-- " + ligne))
' ¿Content-Length?
MatchLength = RegexLength.Match(ligne)
If MatchLength.Success Then
longueur = Integer.Parse(MatchLength.Groups(1).Value)
End If
' línea siguiente
ligne = [IN].ReadLine()
End While
' salida de la última línea
Console.Out.WriteLine("<--")
' construcción de la expresión regular que permite recuperar el resultado
' en el flujo de la respuesta del servidor web
Dim modèle As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
Dim ModèleRésultat As New Regex(modèle)
Dim MatchRésultat As Match = Nothing
' se lee el resto de la respuesta del servidor web
Dim chrRéponse(longueur) As Char
[IN].Read(chrRéponse, 0, longueur)
Dim strRéponse As String = New [String](chrRéponse)
' se descompone la respuesta en líneas de texto
Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
' se recorren las líneas de texto en busca del resultado
Dim strRésultat As String = "?" ' résultat de la fonction
For i = 0 To lignes.Length - 1
' seguimiento
Console.Out.WriteLine(("<-- " + lignes(i)))
' comparación de la línea actual con el patrón
MatchRésultat = ModèleRésultat.Match(lignes(i))
' ¿Se ha encontrado?
If MatchRésultat.Success Then
' se anota el resultado
strRésultat = MatchRésultat.Groups(1).Value
End If
Next i
' se muestra el resultado
Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
End Sub
En primer lugar, el cliente HTTP-POST envía su solicitud en el formato POST:
' construcción de la cadena de consulta
Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
Dim nbChars As Integer = requête.Length
' construcción de la tabla de encabezados HTTP que se va a enviar
Dim entetes(5) As String
entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
entetes(1) = "Host: " & uri.Host & ":" & uri.Port
entetes(2) = "Content-Type: application/x-www-form-urlencoded"
entetes(3) = "Content-Length: " & nbChars
entetes(4) = "Connection: Keep-Alive"
entetes(5) = ""
' se envían los encabezados HTTP al servidor
Dim i As Integer
For i = 0 To entetes.Length - 1
' envío al servidor
OUT.WriteLine(entetes(i))
' salida a pantalla
Console.Out.WriteLine(("--> " + entetes(i)))
Next i
En el encabezado
se debe indicar el tamaño de los parámetros que el cliente enviará tras los encabezados HTTP:
Para ello se utiliza el siguiente código:
' construcción de la cadena de consulta
Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
Dim nbChars As Integer = requête.Length
El método HttpUtility.UrlEncode(cadena de caracteres) transforma algunos de los caracteres de chaîne en %n1n2, donde n1n2 es el código ASCII del carácter transformado. Los caracteres afectados por esta transformación son todos aquellos que tienen un significado especial en una consulta POST (el espacio, el signo =, el signo &, etc.). En este caso, el método HttpUtility.UrlEncode suele ser innecesario, ya que a y b son números que no contienen ninguno de estos caracteres especiales. Aquí se utiliza a modo de ejemplo. Necesita el espacio de nombres System.Web. Una vez que el cliente ha enviado sus encabezados HTTP:
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
el servidor responde con el encabezado HTTP 100 Continue:
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<--
El código se limita a leer y mostrar en pantalla esta primera respuesta:
' se lee la primera respuesta del servidor web HTTP/1.1 100
Dim ligne As String = Nothing
' una línea del flujo de lectura
ligne = [IN].ReadLine()
While ligne <> ""
'eco
Console.Out.WriteLine(("<-- " + ligne))
' línea siguiente
ligne = [IN].ReadLine()
End While
'eco de la última línea
Console.Out.WriteLine(("<-- " + ligne))
Una vez leída esta primera respuesta, el cliente debe enviar sus parámetros:
Lo hace con el siguiente código:
' envío de los parámetros de la solicitud
OUT.Write(requête)
' eco
Console.Out.WriteLine(("--> " + requête))
A continuación, el servidor enviará su respuesta. Esta se compone de dos partes:
- encabezados HTTP terminados en una línea en blanco
- la respuesta en formato XML
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
En primer lugar, el cliente lee los encabezados HTTP para encontrar la línea Content-Length y recuperar el tamaño de la respuesta XML (en este caso, 90). Este se recupera mediante una expresión regular. Se podría haber hecho de otra manera y, sin duda, de forma más eficaz.
' construcción de la expresión regular que permite obtener el tamaño de la respuesta XML
' en el flujo de la respuesta del servidor web
Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
Dim RegexLength As New Regex(modèleLength) '
Dim MatchLength As Match = Nothing
Dim longueur As Integer = 0
' lectura de la segunda respuesta del servidor web tras el envío de la solicitud
' se almacena el valor de la línea Content-Length
ligne = [IN].ReadLine()
While ligne <> ""
' salida a pantalla
Console.Out.WriteLine(("<-- " + ligne))
' ¿Content-Length?
MatchLength = RegexLength.Match(ligne)
If MatchLength.Success Then
longueur = Integer.Parse(MatchLength.Groups(1).Value)
End If
' línea siguiente
ligne = [IN].ReadLine()
End While
' salida de la última línea
Console.Out.WriteLine("<--")
Una vez que tenemos la longitud N de la respuesta XML, solo tenemos que leer N caracteres en el flujo IN de la respuesta del servidor. Esta cadena de N caracteres se descompone en líneas de texto para facilitar el seguimiento en pantalla. Entre estas líneas buscamos la línea del resultado:
utilizando, una vez más, una expresión regular. Una vez encontrado el resultado, se muestra.
El final del código del cliente es el siguiente:
' construcción de la expresión regular que permite recuperar el resultado
' en el flujo de la respuesta del servidor web
Dim modèle As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
Dim ModèleRésultat As New Regex(modèle)
Dim MatchRésultat As Match = Nothing
' se lee el resto de la respuesta del servidor web
Dim chrRéponse(longueur) As Char
[IN].Read(chrRéponse, 0, longueur)
Dim strRéponse As String = New [String](chrRéponse)
' se descompone la respuesta en líneas de texto
Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
' se recorren las líneas de texto en busca del resultado
Dim strRésultat As String = "?" ' résultat de la fonction
For i = 0 To lignes.Length - 1
' seguimiento
Console.Out.WriteLine(("<-- " + lignes(i)))
' comparación de la línea actual con el patrón
MatchRésultat = ModèleRésultat.Match(lignes(i))
' ¿Se ha encontrado?
If MatchRésultat.Success Then
' se anota el resultado
strRésultat = MatchRésultat.Groups(1).Value
End If
Next i
' se muestra el resultado
Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
End Sub
10.6. Un cliente SOAP
Aquí analizamos un segundo cliente que utilizará un diálogo cliente-servidor de tipo SOAP (Simple Object Access Protocol). Se nos presenta un ejemplo de diálogo para la función ajouter:


La solicitud del cliente es una solicitud POST. Por lo tanto, vamos a encontrar algunos de los mecanismos del cliente anterior. La principal diferencia es que, mientras que el cliente HTTP-POST enviaba los parámetros a y b en forma de
, el cliente SOAP los envía en un formato XML más complejo:
POST /operations/operations.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "st.istia.univ-angers.fr/ajouter"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>double</a>
<b>double</b>
</ajouter>
</soap:Body>
</soap:Envelope>
A cambio, recibe una respuesta XML, también más compleja que las respuestas vistas anteriormente:
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouterResponse xmlns="st.istia.univ-angers.fr">
<ajouterResult>double</ajouterResult>
</ajouterResponse>
</soap:Body>
</soap:Envelope>
Aunque la solicitud y la respuesta son más complejas, se trata del mismo mecanismo HTTP que para el cliente HTTP-POST. La escritura del cliente SOAP puede copiarse así de la del cliente HTTP-POST. A continuación se muestra un ejemplo de ejecución:
dos>clientsoap1 http://localhost/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 3 4
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--
--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:33 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 345
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>7</ajouterResult></ajouterResponse
></soap:Body></soap:Envelope>
[résultat=7]
Solo cambia el método executeFonction. El cliente SOAP envía los encabezados HTTP de su solicitud. Son simplemente un poco más complejos que los de HTTP-POST:
ajouter 3 4
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
El código que los genera:
' executeFonction
Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
' ejecuta la función (a,b) en el servicio web de URI uri
' los intercambios cliente-servidor se realizan a través de los flujos IN y OUT
' el resultado de la función se encuentra en la línea
' <double xmlns="st.istia.univ-angers.fr">double</double>
' enviada por el servidor
' construcción de la cadena de consulta SOAP
Dim requêteSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
requêteSOAP += "<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" + ControlChars.Lf
requêteSOAP += "<soap:Body>" + ControlChars.Lf
requêteSOAP += "<" + fonction + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
requêteSOAP += "<a>" + a + "</a>" + ControlChars.Lf
requêteSOAP += "<b>" + b + "</b>" + ControlChars.Lf
requêteSOAP += "</" + fonction + ">" + ControlChars.Lf
requêteSOAP += "</soap:Body>" + ControlChars.Lf
requêteSOAP += "</soap:Envelope>"
Dim nbCharsSOAP As Integer = requêteSOAP.Length
' construcción de la tabla de encabezados HTTP que se va a enviar
Dim entetes(6) As String
entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
entetes(1) = "Host: " & uri.Host & ":" & uri.Port
entetes(2) = "Content-Type: text/xml; charset=utf-8"
entetes(3) = "Content-Length: " & nbCharsSOAP
entetes(4) = "Connection: Keep-Alive"
entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
entetes(6) = ""
' se envían los encabezados HTTP al servidor
Dim i As Integer
For i = 0 To entetes.Length - 1
' envío al servidor
OUT.WriteLine(entetes(i))
' salida a pantalla
Console.Out.WriteLine(("--> " + entetes(i)))
Next i
Al recibir esta solicitud, el servidor envía su primera respuesta, que el cliente muestra:
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--
El código de lectura de esta primera respuesta es el siguiente:
' se lee la primera respuesta del servidor web HTTP/1.1 100
Dim ligne As String = Nothing
' una línea del flujo de lectura
ligne = [IN].ReadLine()
While ligne <> ""
'eco
Console.Out.WriteLine(("<-- " + ligne))
' línea siguiente
ligne = [IN].ReadLine()
End While 'while
'eco de la última línea
Console.Out.WriteLine(("<-- " + ligne))
El cliente enviará ahora sus parámetros en formato XML en lo que se denomina un sobre SOAP:
--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</soap:Body>
</soap:Envelope>
El código:
A continuación, el servidor enviará su respuesta definitiva:
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:33 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 345
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>7</ajouterResult></ajouterResponse
></soap:Body></soap:Envelope>
El cliente muestra en pantalla los encabezados HTTP recibidos mientras busca la línea Content-Length:
' construcción de la expresión regular que permite obtener el tamaño de la respuesta XML
' en el flujo de la respuesta del servidor web
Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
Dim RegexLength As New Regex(modèleLength) '
Dim MatchLength As Match = Nothing
Dim longueur As Integer = 0
' lectura de la segunda respuesta del servidor web tras el envío de la solicitud
' se almacena el valor de la línea Content-Length
ligne = [IN].ReadLine()
While ligne <> ""
' salida a pantalla
Console.Out.WriteLine(("<-- " + ligne))
' ¿Content-Length?
MatchLength = RegexLength.Match(ligne)
If MatchLength.Success Then
longueur = Integer.Parse(MatchLength.Groups(1).Value)
End If
' línea siguiente
ligne = [IN].ReadLine()
End While 'while
' salida de la última línea
Console.Out.WriteLine("<--")
Una vez conocido el tamaño N de la respuesta XML, el cliente lee N caracteres del flujo de la respuesta del servidor, descompone la cadena recuperada en líneas de texto para mostrarlas en pantalla y busca en ellas la etiqueta XML del resultado: <ajouterResult>7</ajouterResult> y muestra este último:
' construcción de la expresión regular que permite recuperar el resultado
' en el flujo de la respuesta del servidor web
Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
Dim ModèleRésultat As New Regex(modèle)
Dim MatchRésultat As Match = Nothing
' se lee el resto de la respuesta del servidor web
Dim chrRéponse(longueur) As Char
[IN].Read(chrRéponse, 0, longueur)
Dim strRéponse As String = New [String](chrRéponse)
' se descompone la respuesta en líneas de texto
Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
' se recorren las líneas de texto en busca del resultado
Dim strRésultat As String = "?" ' résultat de la fonction
For i = 0 To lignes.Length - 1
' seguimiento
Console.Out.WriteLine(("<-- " + lignes(i)))
' comparación de la línea actual con el patrón
MatchRésultat = ModèleRésultat.Match(lignes(i))
' ¿Se ha encontrado?
If MatchRésultat.Success Then
' se anota el resultado
strRésultat = MatchRésultat.Groups(1).Value
End If
'línea siguiente
Next i
' se muestra el resultado
Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
End Sub
10.7. Encapsulación de los intercambios cliente-servidor
Imaginemos que nuestro servicio web operations es utilizado por diversas aplicaciones. Sería interesante poner a disposición de estas una clase que actuara como interfaz entre la aplicación cliente y el servicio web y que ocultara la mayor parte de los intercambios de red, que para la mayoría de los desarrolladores no son triviales. De este modo, tendríamos el siguiente esquema:
![]() |
La aplicación cliente se dirigiría a la interfaz cliente-servidor para realizar sus solicitudes al servicio web. Esta se encargaría de todos los intercambios de red necesarios con el servidor y devolvería el resultado obtenido a la aplicación cliente. Esta ya no tendría que ocuparse de los intercambios con el servidor, lo que facilitaría enormemente su programación.
10.7.1. La clase de encapsulación
Tras lo visto en los párrafos anteriores, ahora conocemos bien los intercambios de red entre el cliente y el servidor. Incluso hemos visto tres métodos. Optamos por encapsular el método SOAP. La clase es la siguiente:
' espacios de nombres
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic
' clientSOAP del servicio web operations
Public Class clientSOAP
' variables de instancia
Private uri As uri = Nothing ' l'URI du service web
Private client As TcpClient = Nothing ' la liaison tcp du client avec le serveur
Private [IN] As StreamReader = Nothing ' le flux de lecture du client
Private OUT As StreamWriter = Nothing ' le flux d'écriture du client
' diccionario de funciones
Private dicoFonctions As New Hashtable
' lista de funciones
Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
' verboso
Private verbose As Boolean = False ' à vrai, affiche à l'écran les échanges client-serveur
' constructor
Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)
' se nota verboso
Me.verbose = verbose
' conexión al servidor
uri = New Uri(uriString)
client = New TcpClient(uri.Host, uri.Port)
' se crean los flujos de entrada-salida del cliente TCP
[IN] = New StreamReader(client.GetStream())
OUT = New StreamWriter(client.GetStream())
OUT.AutoFlush = True
' creación del diccionario de funciones del servicio web
Dim i As Integer
For i = 0 To fonctions.Length - 1
dicoFonctions.Add(fonctions(i), True)
Next i
End Sub
' cierre de la conexión al servidor
Public Sub Close()
' fin de la conexión cliente-servidor
[IN].Close()
OUT.Close()
client.Close()
End Sub
' executeFonction
Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String
' ejecuta la función (a,b) en el servicio web de URI uri
' los intercambios cliente-servidor se realizan a través de los flujos IN y OUT
' el resultado de la función se encuentra en la línea
' <double xmlns="st.istia.univ-angers.fr">double</double>
' enviada por el servidor
' ¿función válida?
fonction = fonction.Trim().ToLower()
If Not dicoFonctions.ContainsKey(fonction) Then
Return "[fonction [" + fonction + "] indisponible : (ajouter, soustraire,multiplier,diviser)]"
End If
' ¿Son válidos los argumentos a y b?
Dim doubleA As Double = 0
Try
doubleA = Double.Parse(a)
Catch
Return "[argument [" + a + "] incorrect (double)]"
End Try
Dim doubleB As Double = 0
Try
doubleB = Double.Parse(b)
Catch
Return "[argument [" + b + "] incorrect (double)]"
End Try
' ¿división por cero?
If fonction = "diviser" And doubleB = 0 Then
Return "[division par zéro]"
End If
' construcción de la cadena de consulta SOAP
Dim requêteSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
requêteSOAP += "<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" + ControlChars.Lf
requêteSOAP += "<soap:Body>" + ControlChars.Lf
requêteSOAP += "<" + fonction + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
requêteSOAP += "<a>" + a + "</a>" + ControlChars.Lf
requêteSOAP += "<b>" + b + "</b>" + ControlChars.Lf
requêteSOAP += "</" + fonction + ">" + ControlChars.Lf
requêteSOAP += "</soap:Body>" + ControlChars.Lf
requêteSOAP += "</soap:Envelope>"
Dim nbCharsSOAP As Integer = requêteSOAP.Length
' construcción de la tabla de encabezados HTTP que se va a enviar
Dim entetes(6) As String
entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
entetes(1) = "Host: " + uri.Host + ":" + uri.Port.ToString
entetes(2) = "Content-Type: text/xml; charset=utf-8"
entetes(3) = "Content-Length: " + nbCharsSOAP.ToString
entetes(4) = "Connection: Keep-Alive"
entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
entetes(6) = ""
' se envían los encabezados HTTP al servidor
Dim i As Integer
For i = 0 To entetes.Length - 1
' envío al servidor
OUT.WriteLine(entetes(i))
' salida a pantalla
If verbose Then
Console.Out.WriteLine(("--> " + entetes(i)))
End If
Next i
' se lee la primera respuesta del servidor web HTTP/1.1 100
Dim ligne As String = Nothing
' una línea del flujo de lectura
ligne = [IN].ReadLine()
While ligne <> ""
'eco
If verbose Then
Console.Out.WriteLine(("<-- " + ligne))
End If
' línea siguiente
ligne = [IN].ReadLine()
End While
'eco de la última línea
If verbose Then
Console.Out.WriteLine(("<-- " + ligne))
End If
' envío de los parámetros de la solicitud
OUT.Write(requêteSOAP)
' eco
If verbose Then
Console.Out.WriteLine(("--> " + requêteSOAP))
End If
' construcción de la expresión regular que permite obtener el tamaño de la respuesta XML
' en el flujo de la respuesta del servidor web
Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
Dim RegexLength As New Regex(modèleLength) '
Dim MatchLength As Match = Nothing
Dim longueur As Integer = 0
' lectura de la segunda respuesta del servidor web tras el envío de la solicitud
' se almacena el valor de la línea Content-Length
ligne = [IN].ReadLine()
While ligne <> ""
' salida a pantalla
If verbose Then
Console.Out.WriteLine(("<-- " + ligne))
End If
' ¿Content-Length?
MatchLength = RegexLength.Match(ligne)
If MatchLength.Success Then
longueur = Integer.Parse(MatchLength.Groups(1).Value)
End If
' línea siguiente
ligne = [IN].ReadLine()
End While
' salida de la última línea
If verbose Then
Console.Out.WriteLine("<--")
End If
' construcción de la expresión regular que permite recuperar el resultado
' en el flujo de la respuesta del servidor web
Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
Dim ModèleRésultat As New Regex(modèle)
Dim MatchRésultat As Match = Nothing
' se lee el resto de la respuesta del servidor web
Dim chrRéponse(longueur) As Char
[IN].Read(chrRéponse, 0, longueur)
Dim strRéponse As String = New [String](chrRéponse)
' se descompone la respuesta en líneas de texto
Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
' se recorren las líneas de texto en busca del resultado
Dim strRésultat As String = "?" ' résultat de la fonction
For i = 0 To lignes.Length - 1
' seguimiento
If verbose Then
Console.Out.WriteLine(("<-- " + lignes(i)))
End If ' comparaison ligne courante au modèle
MatchRésultat = ModèleRésultat.Match(lignes(i))
' ¿se ha encontrado?
If MatchRésultat.Success Then
' se anota el resultado
strRésultat = MatchRésultat.Groups(1).Value
End If
Next i
' se devuelve el resultado
Return strRésultat
End Function
End Class
No encontramos nada nuevo con respecto a lo que ya hemos visto. Simplemente hemos tomado el código del cliente SOAP que hemos estudiado y lo hemos reorganizado un poco para convertirlo en una clase. Esta clase tiene un constructor y dos métodos:
' creador
Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)
' executeFonction
Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String
' cierre de la conexión con el servidor
Public Sub Close()
y tiene los siguientes atributos:
' variables de instancia
Private uri As Uri = Nothing ' l'URI du service web
Private client As TcpClient = Nothing ' la liaison tcp du client avec le serveur
Private [IN] As StreamReader = Nothing ' le flux de lecture du client
Private OUT As StreamWriter = Nothing ' le flux d'écriture du client
' diccionario de funciones
Private dicoFonctions As New Hashtable
' lista de funciones
Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
' detallado
Private verbose As Boolean = False ' à vrai, affiche à l'écran les échanges client-serveur
Se pasan dos parámetros al constructor:
- el URI del servicio web al que debe conectarse
- un valor booleano verbose que, en realidad, solicita que los intercambios de red se muestren en pantalla; de lo contrario, no se mostrarán.
Durante la construcción, se crean los flujos IN de lectura de red, OUT de escritura de red, así como el diccionario de funciones gestionadas por el servicio. Una vez construido el objeto, se abre la conexión cliente-servidor y sus flujos IN y OUT quedan disponibles.
El método Close permite cerrar la conexión con el servidor.
El método ExecuteFonction es el que hemos escrito para el cliente SOAP estudiado, salvo por algunos detalles:
- 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
- El método ExecuteFonction, que antes devolvía un tipo void y mostraba el resultado de la función en pantalla, ahora devuelve este resultado y, por lo tanto, un tipo string.
Normalmente, un cliente utilizará la clase clientSOAP de la siguiente manera:
- creación de un objeto clientSOAP que establecerá la conexión con el servicio web
- uso repetido del método executeFonction
- cierre de la conexión con el servicio web mediante el método Close.
Analicemos un primer cliente.
10.7.2. Un cliente de consola
Retomamos aquí el cliente SOAP estudiado cuando la clase clientSOAP aún no existía y lo reestructuramos para que ahora utilice esta clase:
' espacios de nombres
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic
Public Module testClientSoap
' solicita el URI del servicio web de operaciones
' ejecuta de forma interactiva los comandos introducidos mediante el teclado
Public Sub Main(ByVal args() As String)
' sintaxis
Const syntaxe As String = "pg URI [verbose]"
' número de argumentos
If args.Length <> 1 And args.Length <> 2 Then
erreur(syntaxe, 1)
End If
' ¿detallado?
Dim verbose As Boolean = False
If args.Length = 2 Then
verbose = args(1).ToLower() = "verbose"
End If
' se conecta al servicio web
Dim client As clientSOAP = Nothing
Try
client = New clientSOAP(args(0), verbose)
Catch ex As Exception
' error de conexión
erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
End Try
' las solicitudes del usuario se introducen mediante el teclado
' en el formato función a b; terminan con el comando fin
Dim commande As String = Nothing ' commande tapée au clavier
Dim champs As String() = Nothing ' champs d'une ligne de commande
' solicita al usuario
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b" + ControlChars.Lf)
' gestión de errores
Dim erreurCommande As Boolean
Try
' bucle de introducción de comandos tecleados
While True
' al principio no hay error
erreurCommande = False
' lectura del comando
commande = Console.In.ReadLine().Trim().ToLower()
' ¿Terminado?
If commande Is Nothing Or commande = "fin" Then
Exit While
End If
' descomposición del comando en campos
champs = Regex.Split(commande, "\s+")
' se necesitan tres campos
If champs.Length <> 3 Then
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
' se registra el error
erreurCommande = True
End If
' se envía la solicitud al servicio web
If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
' siguiente solicitud
End While
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
' fin de la conexión cliente-servidor
Try
client.Close()
Catch
End Try
End Sub
' visualización de los errores
Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
' visualización del error
System.Console.Error.WriteLine(msg)
' parada con error
Environment.Exit(exitCode)
End Sub
End Module
El cliente es ahora considerablemente más sencillo y no contiene ninguna comunicación de red. El cliente admite dos parámetros:
- el URI del servicio web operations
- la palabra clave opcional verbose. Si está presente, los intercambios de red se mostrarán en pantalla.
Estos dos parámetros se utilizan para crear un objeto clientSOAP que se encargará de las comunicaciones con el servicio web.
' se conecta al servicio web
Dim client As clientSOAP = Nothing
Try
client = New clientSOAP(args(0), verbose)
Catch ex As Exception
' error de conexión
erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
End Try
Una vez establecida la conexión con el servicio web, el cliente puede enviar sus solicitudes. Estas se introducen mediante el teclado, se analizan y, a continuación, se envían al servidor mediante una llamada al método executeFonction del objeto clientSOAP.
' se realiza la solicitud al servicio web
If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
La clase clientSOAP se compila en un «ensamblado»:
dos>vbc /r:clientSOAP.dll testClientSOAP.vb
dos>dir
04/03/2004 08:46 6 913 clientSOAP.vb
04/03/2004 09:07 7 168 clientSOAP.dll
A continuación, la aplicación cliente testClientSoap se compila mediante:
dos>vbc /r:clientSOAP.dll /r:system.dll testClientSOAP.vb
dos>dir
04/03/2004 09:08 2 711 testClientSOAP.vb
04/03/2004 09:08 4 608 testClientSOAP.exe
A continuación se muestra un ejemplo de ejecución concisa:
dos>testclientsoap http://localhost/st/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 1 3
résultat=4
soustraire 6 7
résultat=-1
multiplier 4 5
résultat=20
diviser 1 2
résultat=0.5
x
syntaxe : [ajouter|soustraire|multiplier|diviser] a b
x 1 2
résultat=[fonction [x] indisponible : (ajouter, soustraire,multiplier,diviser)]
ajouter a b
résultat=[argument [a] incorrect (double)]
ajouter 1 b
résultat=[argument [b] incorrect (double)]
diviser 1 0
résultat=[division par zéro]
fin
Se puede seguir el intercambio de datos de red solicitando una ejecución «detallada»:
dos>testClientSOAP http://localhost/operations/operations.asmx verboso
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 4 8
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 08:15:25 GMT
<-- X-Powered-By: ASP.NET
<--
--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>4</a>
<b>8</b>
</ajouter>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 08:15:25 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 346
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>12</ajouterResult></ajouterResponse></soap:Body></soap:Envelope>
résultat=12
fin
Ahora vamos a crear un cliente gráfico.
10.7.3. Un cliente gráfico de Windows
Ahora vamos a consultar nuestro servicio web con un cliente gráfico que también utilizará la clase clientSOAP. La interfaz gráfica será la siguiente:
![]() |
Los controles son los siguientes:
n.º | tipo | nombre | función |
TextBox | txtURI | URI del servicio web de operaciones | |
Botón | btnOuvrir | abre la conexión con el servicio web | |
Botón | btnFermer | cierra la conexión con el servicio web | |
ComboBox | cmbFonctions | la lista de funciones (sumar, restar, multiplicar, dividir) | |
TextBox | txtA | los argumentos de las funciones | |
TextBox | txtB | el argumento b de las funciones | |
TextBox | txtRésultat | el resultado de la función (a, b) | |
Botón | btnCalculer | inicia el cálculo de la función(a,b) | |
TextBox | txtErreur | muestra un mensaje de estado de la conexión |
Existen algunas restricciones de funcionamiento:
- el botón btnOuvrir solo está activo si el campo txtURI no está vacío y no hay ninguna conexión ya abierta
- el botón btnFermer solo está activo cuando se ha abierto una conexión con el servicio web
- el botón btnCalculer solo está activo cuando hay una conexión abierta y los campos txtA y txtB no están vacíos
- los campos txtRésultat y txtErreur tienen el atributo ReadOnly establecido en verdadero
El cliente comienza abriendo la conexión con el servicio web mediante el botón [Ouvrir]:

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





A continuación se muestra el código de la aplicación. Hemos omitido el código del formulario, ya que no es relevante en este contexto.
'espacios de nombres
Imports System
Imports System.Windows.Forms
' la clase del formulario
Public Class FormClientSOAP
Inherits System.Windows.Forms.Form
' atributos de instancia
Dim client As clientSOAP ' client SOAP du service web operations
#Región «Código generado por el Diseñador de formularios de Windows»
Public Sub New()
MyBase.New()
'Esta llamada es requerida por el Diseñador de Windows Form.
InitializeComponent()
' otras inicializaciones
myInit()
End Sub
'El método sustituido Dispose del formulario para limpiar la lista de componentes.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
End Sub
...
Private Sub InitializeComponent()
....
End Sub
#Fin de la región
Private Sub myInit()
' inicialización del formulario
cmbFonctions.SelectedIndex = 0
btnOuvrir.Enabled = False
btnFermer.Enabled = True
btnCalculer.Enabled = False
End Sub
Private Sub txtURI_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtURI.TextChanged
' el contenido del campo de entrada ha cambiado: se establece el estado del botón «Abrir»
btnOuvrir.Enabled = txtURI.Text.Trim <> ""
End Sub
Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
' solicitud de apertura de una conexión con el servicio web
Try
' creación de un objeto de tipo [clientSOAP]
client = New clientSOAP(txtURI.Text.Trim, False)
' Estados de los botones
btnOuvrir.Enabled = False
btnFermer.Enabled = True
' el URI ya no se puede modificar
txtURI.ReadOnly = True
' estado del cliente
txtErreur.Text = "Liaison au service web ouverte"
Catch ex As Exception
' se ha producido un error; se muestra
txtErreur.Text = ex.Message
End Try
End Sub
Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
' cierre de la conexión al servicio web
client.Close()
' estados de los botones
btnOuvrir.Enabled = True
btnFermer.Enabled = False
' URI
txtURI.ReadOnly = False
' estado del cliente
txtErreur.Text = "Liaison au service web fermée"
End Sub
Private Sub btnCalculer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnCalculer.Click
' cálculo de una función f(a,b)
' se borra el resultado anterior
txtRésultat.Text = ""
Try
txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
Catch ex As Exception
' se ha producido un error de red
txtErreur.Text = ex.Message
' se cierra la conexión
btnFermer_Click(Nothing, Nothing)
End Try
End Sub
Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
' cambio del valor de A
btnCalculer.Enabled = txtA.Text.Trim <> "" And txtB.Text.Trim <> ""
End Sub
Private Sub txtB_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtB.TextChanged
' cambio del valor de B
txtA_TextChanged(Nothing, Nothing)
End Sub
' método principal
Public Shared Sub main()
Application.Run(New FormClientSOAP)
End Sub
End Class
Una vez más, la clase clientSOAP oculta todo el aspecto de red de la aplicación. La aplicación se ha construido de la siguiente manera:
- el ensamblado clientSOAP.dll, que contiene la clase clientSOAP, se ha colocado en la carpeta del proyecto
- la interfaz gráfica clientsoapgui.vb se ha creado con VS.NET y luego se ha compilado en una ventana de DOS:
dos>vbc /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll /r:clientSOAP.dll clientsoapgui.vb
dos>dir
04/03/2004 09:13 7 168 clientSOAP.dll
04/03/2004 16:44 9 866 clientsoapgui.vb
04/03/2004 16:44 11 264 clientsoapgui.exe
A continuación, se inició la interfaz gráfica mediante:
10.8. Un cliente proxy
Recordemos lo que acabamos de hacer. Hemos creado una clase intermedia que encapsula los intercambios de red entre un cliente y un servicio web según el esquema siguiente:
![]() |
La plataforma .NET lleva esta lógica un paso más allá. Una vez conocido el servicio web al que nos queremos conectar, podemos generar automáticamente la clase que nos servirá de intermediario para acceder a las funciones del servicio web y que ocultará toda la parte de red. A esta clase se le denomina proxy del servicio web para el que se ha generado.
¿Cómo generar la clase proxy de un servicio web? Un servicio web siempre va acompañado de un archivo de descripción en formato XML. Si el URI de nuestro servicio web operations es http://localhost/operations/operations.asmx, su archivo de descripción está disponible en http://localhost/operations/operations.asmx?wsdl, tal y como se muestra en la siguiente captura de pantalla:

Aquí tenemos un archivo XML que describe con precisión todas las funciones del servicio web, indicando para cada una de ellas el tipo y el número de parámetros, así como el tipo de resultado. A este archivo se le denomina archivo WSDL del servicio porque utiliza el lenguaje WSDL (Web Services Description Language). A partir de este archivo, se puede generar una clase proxy con la ayuda de la herramienta wsdl:
dos>wsdl http://localhost/operations/operations.asmx?wsdl /language=vb
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Écriture du fichier 'D:\data\devel\vbnet\poly\chap9\clientproxy\operations.vb'.
dos>dir
04/03/2004 17:17 6 663 operations.vb
La herramienta wsdl genera un archivo fuente VB.NET (opción /language=vb) con el nombre de la clase que implementa el servicio web, en este caso operations. Veamos una parte del código generado:
'------------------------------------------------------------------------------
' <autogenerado>
' Este código fue generado por una herramienta.
' Versión de tiempo de ejecución: 1.1.4322.573
'
' Los cambios en este archivo pueden provocar un comportamiento incorrecto y se perderán si
' se vuelve a generar el código.
' </autogenerated>
'------------------------------------------------------------------------------
Option Strict Off
Option Explicit On
Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Xml.Serialization
'
'Este código fuente ha sido té generado automáticamente généré por wsdl, Versión=1.1.4322.573.
'
'<remarks/>
<System.Diagnostics.DebuggerStepThroughAttribute(), _
System.ComponentModel.DesignerCategoryAttribute("code"), _
System.Web.Services.WebServiceBindingAttribute(Name:="operationsSoap", [Namespace]:="st.istia.univ-angers.fr")> _
Public Class operations
Inherits System.Web.Services.Protocols.SoapHttpClientProtocol
'<remarks/>
Public Sub New()
MyBase.New
Me.Url = "http://localhost/operations/operations.asmx"
End Sub
'<remarks/>
<System.Web.Services.Protocols.SoapDocumentMethodAttribute("st.istia.univ-angers.fr/ajouter", RequestNamespace:="st.istia.univ-angers.fr", ResponseNamespace:="st.istia.univ-angers.fr", Use:=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle:=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)> _
Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
Return CType(results(0),Double)
End Function
'<comentarios/>
Public Function Beginajouter(ByVal a As Double, ByVal b As Double, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult
Return Me.BeginInvoke("ajouter", New Object() {a, b}, callback, asyncState)
End Function
'<comentarios/>
Public Function Endajouter(ByVal asyncResult As System.IAsyncResult) As Double
Dim results() As Object = Me.EndInvoke(asyncResult)
Return CType(results(0),Double)
End Function
....
Este código resulta un poco complejo a primera vista. No es necesario comprender sus detalles para poder utilizarlo. Examinemos primero la declaración de la clase:
La clase lleva el nombre operations del servicio web para el que se ha creado. Deriva de la clase SoapHttpClientProtocol:

Nuestra clase proxy tiene un único constructor:
El constructor asigna al atributo url el valor URL del servicio web asociado al proxy. La clase operations anterior no define por sí misma el atributo url. Este se hereda de la clase de la que deriva el proxy: System.Web.Services.Protocols.SoapHttpClientProtocol. Veamos ahora lo que se refiere al método ajouter:
Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
Return CType(results(0),Double)
End Function
Se puede observar que tiene la misma firma que en el servicio web operations, donde se definía de la siguiente manera:
<WebMethod> _
Function ajouter(a As Double, b As Double) As Double
Return a + b
End Function 'ajouter
La forma en que esta clase interactúa con el servicio web no aparece aquí. Esta interacción está totalmente gestionada por la clase padre System.Web.Services.Protocols.SoapHttpClientProtocol. En el proxy solo se encuentra lo que lo diferencia de los demás proxies:
- el URL del servicio web asociado
- la definición de los métodos del servicio asociado.
Para utilizar los métodos del servicio web operations, el cliente solo necesita la clase proxy operations generada anteriormente. Compilemos esta clase en un archivo assembly:
Ahora escribamos un cliente de consola. Se invoca sin parámetros y ejecuta las solicitudes introducidas mediante el teclado:
dos>testclientproxy
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b
ajouter 4 5
résultat=9
soustraire 9 8
résultat=1
multiplier 10 4
résultat=40
diviser 6 7
résultat=0,857142857142857
toutfaire 10 20
résultats=[30,-10,200,0,5]
diviser 5 0
résultat=+Infini
fin
El código del cliente es el siguiente:
' espacios de nombres
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Public Module testClientProxy
' ejecuta de forma interactiva los comandos introducidos mediante el teclado
' y los envía al servicio web de operaciones
Public Sub Main()
' ya no hay argumentos: el URL del servicio web está codificado de forma fija en el proxy
' creación de un diccionario de funciones del servicio web
Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser", "toutfaire"}
Dim dicoFonctions As New Hashtable
Dim i As Integer
For i = 0 To fonctions.Length - 1
dicoFonctions.Add(fonctions(i), True)
Next i
' se crea un objeto proxy de operaciones
Dim myOperations As operations = Nothing
Try
myOperations = New operations
Catch ex As Exception
' error de conexión
erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
End Try
' las solicitudes del usuario se introducen mediante el teclado
' en la forma función a b; terminan con el comando fin
Dim commande As String = Nothing ' commande tapée au clavier
Dim champs As String() = Nothing ' champs d'une ligne de commande
' solicita al usuario
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)
' algunos datos locales
Dim erreurCommande As Boolean
Dim fonction As String
Dim a, b As Double
' bucle de introducción de comandos tecleados
While True
' al principio no hay error
erreurCommande = False
' lectura del comando
commande = Console.In.ReadLine().Trim().ToLower()
' ¿Terminado?
If commande Is Nothing Or commande = "fin" Then
Exit While
End If
' descomposición del comando en campos
champs = Regex.Split(commande, "\s+")
Try
' se necesitan tres campos
If champs.Length <> 3 Then
Throw New Exception
End If
' el campo 0 debe ser una función reconocida
fonction = champs(0)
If Not dicoFonctions.ContainsKey(fonction) Then
Throw New Exception
End If
' el campo 1 debe ser un número válido
a = Double.Parse(champs(1))
' el campo 2 debe ser un número válido
b = Double.Parse(champs(2))
Catch
' comando no válido
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
erreurCommande = True
End Try
' se envía la solicitud al servicio web
If Not erreurCommande Then
Try
Dim résultat As Double
Dim résultats() As Double
If fonction = "ajouter" Then
résultat = myOperations.ajouter(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "soustraire" Then
résultat = myOperations.soustraire(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "multiplier" Then
résultat = myOperations.multiplier(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "diviser" Then
résultat = myOperations.diviser(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "toutfaire" Then
résultats = myOperations.toutfaire(a, b)
Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
résultats(2).ToString + "," + résultats(3).ToString + "]"))
End If
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
End If
End While
End Sub
' visualización de errores
Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
' visualización del error
System.Console.Error.WriteLine(msg)
' parada con error
Environment.Exit(exitCode)
End Sub
End Module
Solo examinamos el código específico del uso de la clase proxy. En primer lugar, se crea un objeto proxy operations:
' se crea un objeto proxy de operaciones
Dim myOperations As operations = Nothing
Try
myOperations = New operations
Catch ex As Exception
' error de conexión
erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
End Try
Se escriben en el teclado las líneas de función a y b. A partir de esta información, se invocan los métodos adecuados del proxy:
' se realiza la solicitud al servicio web
If Not erreurCommande Then
Try
Dim résultat As Double
Dim résultats() As Double
If fonction = "ajouter" Then
résultat = myOperations.ajouter(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "soustraire" Then
résultat = myOperations.soustraire(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "multiplier" Then
résultat = myOperations.multiplier(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "diviser" Then
résultat = myOperations.diviser(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "toutfaire" Then
résultats = myOperations.toutfaire(a, b)
Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
résultats(2).ToString + "," + résultats(3).ToString + "]"))
End If
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
Aquí se procesa por primera vez la operación toutfaire, que realiza las cuatro operaciones. Hasta ahora se había ignorado porque envía una matriz de números encapsulada en un contenedor XML, más complicado de gestionar que las respuestas XML simples de las otras funciones, que solo devuelven un único resultado. Se observa que aquí, con la clase proxy, utilizar el método toutfaire no es más complicado que utilizar los demás métodos. La aplicación se ha compilado en una ventana de DOS de la siguiente manera:
dos>vbc /r:operations.dll /r:system.dll /r:system.web.services.dll testClientProxy.vb
dos>dir
04/03/2004 17:17 6 663 operations.vb
04/03/2004 17:24 7 680 operations.dll
04/03/2004 17:41 4 099 testClientProxy.vb
04/03/2004 17:41 5 632 testClientProxy.exe
10.9. Configurar un servicio web
Un servicio web puede necesitar información de configuración para inicializarse correctamente. Con el servidor IIS, esta información se puede colocar en un archivo llamado web.config y ubicado en la misma carpeta que el servicio web. Supongamos que queremos crear un servicio web que necesita dos datos para inicializarse: un nombre y una edad. Estos dos datos se pueden colocar en el archivo web.config con el siguiente formato:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Los parámetros de inicialización se colocan en un contenedor XML:
Se declarará un parámetro de inicialización con el nombre P y el valor V con la línea:
<add key="P" value="V"/>
¿Cómo recupera el servicio web esta información? Cuando IIS carga un servicio web, comprueba si hay un archivo web.config en la misma carpeta. Si lo hay, lo lee. El valor V de un parámetro P se obtiene mediante la instrucción:
donde ConfigurationSettings es una clase en el espacio de nombres System.Configuration.
Comprobemos esta técnica en el siguiente servicio web:
<%@ WebService language="VB" class=personne %>
Imports System.Web.Services
imports System.Configuration
<WebService([Namespace] := "st.istia.univ-angers.fr")> _
Public Class personne
Inherits WebService
' atributos
Private nom As String
Private age As Integer
' constructor
Public Sub New()
' inicialización de atributos
nom = ConfigurationSettings.AppSettings("nom")
age = Integer.Parse(ConfigurationSettings.AppSettings("age"))
End Sub
<WebMethod> _
Function id() As String
Return "[" + nom + "," + age.ToString + "]"
End Function
End Class
El servicio web personne tiene dos atributos, nom y age, que se inicializan en su constructor sin parámetros a partir de los valores leídos en el archivo de configuración web.config del servicio personne. Este archivo es el siguiente:
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
El servicio web tiene además un <WebMethod> id sin parámetros y que se limita a devolver los atributos nom y age. El servicio está registrado en el archivo fuente personne.asmx, que se encuentra junto con su archivo de configuración en la carpeta c:\inetpub\wwwroot\st\personne:
Asociemos una carpeta virtual IIS /config a la carpeta física anterior. Iniciemos IIS y, a continuación, desde un navegador, solicitemos la URL http://localhost/config/personne.asmx del servicio personne:

Sigamos el enlace del único método id:

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

Hemos recuperado correctamente la información contenida en el archivo web.config del servicio.
10.10. El servicio web IMPOTS
Retomamos la ya conocida aplicación IMPOTS. La última vez que trabajamos con ella, la convertimos en un servidor remoto al que se podía acceder a través de Internet. Ahora la convertimos en un servicio web.
10.10.1. El servicio web
Partimos de la clase impôt creada en el capítulo sobre bases de datos y que se construye a partir de la información contenida en una base de datos ODBC:
' opciones
Option Strict On
Option Explicit On
' espacios de nombres
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Public Class impôt
' los datos necesarios para el cálculo del impuesto
' proceden de una fuente externa
Private limites(), coeffR(), coeffN() As Decimal
' creador
Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
' se comprueba que las 3 tablas tengan el mismo tamaño
Dim OK As Boolean = LIMITES.Length = COEFFR.Length And LIMITES.Length = COEFFN.Length
If Not OK Then
Throw New Exception("Les 3 tableaux fournis n'ont pas la même taille(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
End If
' Todo correcto
Me.limites = LIMITES
Me.coeffR = COEFFR
Me.coeffN = COEFFN
End Sub
' constructor 2
Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
' inicializa los tres tableros límite, coeffR, coeffN a partir
' del contenido de la tabla Timpots de la base ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
' puede lanzar una excepción
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Dim impotsConn As OdbcConnection = Nothing ' la connexion
Dim sqlCommand As OdbcCommand = Nothing ' la commande SQL
' la consulta SELECT
Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
' tablas para recuperar los datos
Dim tLimites As New ArrayList
Dim tCoeffR As New ArrayList
Dim tCoeffN As New ArrayList
' se intenta acceder a la base de datos
impotsConn = New OdbcConnection(connectString)
impotsConn.Open()
' se crea un objeto de comando
sqlCommand = New OdbcCommand(selectCommand, impotsConn)
' se ejecuta la consulta
Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
' Análisis de la tabla recuperada
While myReader.Read()
' los datos de la línea actual se introducen en las tablas
tLimites.Add(myReader(colLimites))
tCoeffR.Add(myReader(colCoeffR))
tCoeffN.Add(myReader(colCoeffN))
End While
' liberación de recursos
myReader.Close()
impotsConn.Close()
' las tablas dinámicas se convierten en tablas estáticas
Me.limites = New Decimal(tLimites.Count) {}
Me.coeffR = New Decimal(tLimites.Count) {}
Me.coeffN = New Decimal(tLimites.Count) {}
Dim i As Integer
For i = 0 To tLimites.Count - 1
limites(i) = Decimal.Parse(tLimites(i).ToString())
coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
Next i
End Sub
' cálculo del impuesto
Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
' cálculo del número de participaciones
Dim nbParts As Decimal
If marié Then
nbParts = CDec(nbEnfants) / 2 + 2
Else
nbParts = CDec(nbEnfants) / 2 + 1
End If
If nbEnfants >= 3 Then
nbParts += 0.5D
End If
' cálculo de la renta imponible y del coeficiente familiar
Dim revenu As Decimal = 0.72D * salaire
Dim QF As Decimal = revenu / nbParts
' cálculo del impuesto
limites((limites.Length - 1)) = QF + 1
Dim i As Integer = 0
While QF > limites(i)
i += 1
End While
' Devolución del resultado
Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
End Function
End Class
En el servicio web, solo se puede utilizar un constructor sin parámetros. Por lo tanto, el constructor de la clase quedará así:
' constructor
Public Sub New()
' inicializa las tres tablas de límites, coeffR, coeffN a partir
' del contenido de la tabla Timpots de la base ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
' puede lanzar una excepción
': se recuperan los parámetros de configuración del servicio
Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")
': se consulta la base de datos
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Los cinco parámetros del constructor de la clase anterior se leen ahora en el archivo web.config del servicio. El código del archivo fuente impots.asmx es el siguiente. Recoge la mayor parte del código anterior. Nos hemos limitado a enmarcar las partes del código propias del servicio web:
<%@ WebService language="VB" class=impots %>
' creación de un servicio web de impuestos
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Imports System.Configuration
Imports System.Web.Services
<WebService([Namespace]:="st.istia.univ-angers.fr")> _
Public Class impôt
Inherits WebService
' los datos necesarios para el cálculo del impuesto
' provienen de una fuente externa
Private limites(), coeffR(), coeffN() As Decimal
Private OK As Boolean = False
Private errMessage As String = ""
' constructor
Public Sub New()
' inicializa las tres tablas de límites, coeffR, coeffN a partir
' del contenido de la tabla Timpots de la base ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
' puede lanzar una excepción
': se recuperan los parámetros de configuración del servicio
Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")
': se consulta la base de datos
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Dim impotsConn As OdbcConnection = Nothing ' la connexion
Dim sqlCommand As OdbcCommand = Nothing ' la commande SQL
Dim myReader As OdbcDataReader ' lecteur de données Odbc
' la consulta SELECT
Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
' tablas para recuperar los datos
Dim tLimites As New ArrayList
Dim tCoeffR As New ArrayList
Dim tCoeffN As New ArrayList
' se intenta acceder a la base de datos
Try
impotsConn = New OdbcConnection(connectString)
impotsConn.Open()
' se crea un objeto de comando
sqlCommand = New OdbcCommand(selectCommand, impotsConn)
' se ejecuta la consulta
myReader = sqlCommand.ExecuteReader()
' Explotación de la tabla recuperada
While myReader.Read()
' los datos de la línea actual se colocan en las tablas
tLimites.Add(myReader(colLimites))
tCoeffR.Add(myReader(colCoeffR))
tCoeffN.Add(myReader(colCoeffN))
End While
' Liberación de recursos
myReader.Close()
impotsConn.Close()
' las tablas dinámicas se colocan en tablas estáticas
Me.limites = New Decimal(tLimites.Count) {}
Me.coeffR = New Decimal(tLimites.Count) {}
Me.coeffN = New Decimal(tLimites.Count) {}
Dim i As Integer
For i = 0 To tLimites.Count - 1
limites(i) = Decimal.Parse(tLimites(i).ToString())
coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
Next i
' Todo listo
OK = True
errMessage = ""
Catch ex As Exception
' error
OK = False
errMessage += "[" + ex.Message + "]"
End Try
End Sub
' cálculo del impuesto
<WebMethod()> _
Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
' cálculo del número de participaciones
Dim nbParts As Decimal
If marié Then
nbParts = CDec(nbEnfants) / 2 + 2
Else
nbParts = CDec(nbEnfants) / 2 + 1
End If
If nbEnfants >= 3 Then
nbParts += 0.5D
End If
' cálculo de la renta imponible y del coeficiente familiar
Dim revenu As Decimal = 0.72D * salaire
Dim QF As Decimal = revenu / nbParts
' cálculo del impuesto
limites((limites.Length - 1)) = QF + 1
Dim i As Integer = 0
While QF > limites(i)
i += 1
End While
' Devolución del resultado
Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
End Function
' id
<WebMethod()> _
Function id() As String
' para comprobar si todo está bien OK
Return "[" + OK + "," + errMessage + "]"
End Function
End Class
Explicaremos las pocas modificaciones realizadas en la clase impots, aparte de las necesarias para convertirla en un servicio web:
- la lectura de la base de datos en el constructor puede fallar. Por ello, hemos añadido dos atributos a nuestra clase y un método:
- el booleano OK toma el valor vrai si se ha podido leer la base de datos, y faux en caso contrario
- la cadena errMessage contiene un mensaje de error si no se ha podido leer la base de datos.
- El método id sin parámetros permite obtener el valor de estos dos atributos.
- Para gestionar el posible error de acceso a la base de datos, la parte del código del constructor relacionada con este acceso se ha rodeado de un try-catch.
El archivo web.config de configuración del servicio es el siguiente:
<configuration>
<appSettings>
<add key="DSN" value="mysql-impots" />
<add key="TABLE" value="timpots" />
<add key="COL_LIMITES" value="limites" />
<add key="COL_COEFFR" value="coeffr" />
<add key="COL_COEFFN" value="coeffn" />
</appSettings>
</configuration>
Durante un primer intento de carga del servicio impots, el compilador indicó que no encontraba el espacio de nombres Microsoft.Data.Odbc utilizado en la directiva:
Tras consultar la documentación
- se añadió una directiva de compilación en web.config para indicar que se debía utilizar el ensamblador Microsoft.Data.odbc
- Se ha colocado una copia del archivo microsoft.data.odbc.dll en la carpeta bin del proyecto. El compilador de un servicio web explora sistemáticamente esta carpeta cuando busca un «assembly».
Parecen posibles otras soluciones, pero no se han analizado en profundidad aquí. Por lo tanto, el archivo de configuración ha pasado a ser:
<configuration>
<appSettings>
<add key="DSN" value="mysql-impots" />
<add key="TABLE" value="timpots" />
<add key="COL_LIMITES" value="limites" />
<add key="COL_COEFFR" value="coeffr" />
<add key="COL_COEFFN" value="coeffn" />
</appSettings>
<system.web>
<compilation>
<assemblies>
<add assembly="Microsoft.Data.Odbc" />
</assemblies>
</compilation>
</system.web>
</configuration>
Contenido de la carpeta impots\bin:
El servicio y su archivo de configuración se han colocado en impots:
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:

Si seguimos el enlace id:

Si se utiliza el botón Appeler:

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

Sigamos el enlace calculer:

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

El resultado es correcto.
10.10.2. Generar el proxy del servicio impots
Ahora que tenemos un servicio web impots operativo, podemos generar su clase proxy. Recordemos que esta será utilizada por las aplicaciones cliente para acceder al servicio web impots de forma transparente. En primer lugar, utilizamos la utilidad wsdl para generar el archivo fuente de la clase proxy y, a continuación, este se compila en una DLL.
dos>wsdl /language=vb http://localhost/impuestos/impots.asmx
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Écriture du fichier 'D:\data\serge\devel\vbnet\poly\chap9\impots\impots.vb'.
D:\data\serge\devel\vbnet\poly\chap9\impots>dir
09/03/2004 10:20 <REP> bin
09/03/2004 10:58 4 651 impots.asmx
09/03/2004 11:05 3 364 impots.vb
09/03/2004 10:19 431 web.config
dos>vbc /t:library /r:system.dll /r:system.web.services.dll /r:system.xml.dll impots.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4
pour Microsoft (R) .NET Framework version 1.1.4322.573
Copyright (C) Microsoft Corporation 1987-2002. Tous droits réservés.
dos>dir
09/03/2004 10:20 <REP> bin
09/03/2004 10:58 4 651 impots.asmx
09/03/2004 11:09 5 120 impots.dll
09/03/2004 11:05 3 364 impots.vb
09/03/2004 10:19 431 web.config
10.10.3. Usar el proxy con un cliente
En el capítulo sobre bases de datos creamos una aplicación de consola que permite calcular el impuesto:
dos>dir
27/02/2004 16:56 5 120 impots.dll
27/02/2004 17:12 3 586 impots.vb
27/02/2004 17:08 6 144 testimpots.exe
27/02/2004 17:18 3 328 testimpots.vb
dos>testimpots
pg DSNimpots tabImpots colLimites colCoeffR colCoeffN
dos>testimpots odbc-mysql-dbimpots impots limites coeffr coeffn
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F
El programa testimpots utilizaba entonces la clase impôt clásica, la contenida en el archivo impots.dll. El código del programa testimpots.vb era el siguiente:
Option Explicit On
Option Strict On
' espacios de nombres
Imports System
Imports Microsoft.VisualBasic
' página de prueba
Module testimpots
Sub Main(ByVal arguments() As String)
' programa interactivo de cálculo de impuestos
' el usuario introduce tres datos con el teclado: casado nbEnfants salario
' el programa muestra entonces el impuesto a pagar
Const syntaxe1 As String = "pg DSNimpots tabImpots colLimites colCoeffR colCoeffN"
Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"
' verificación de los parámetros del programa
If arguments.Length <> 5 Then
' mensaje de error
Console.Error.WriteLine(syntaxe1)
' fin
Environment.Exit(1)
End If 'if
' se recuperan los argumentos
Dim DSNimpots As String = arguments(0)
Dim tabImpots As String = arguments(1)
Dim colLimites As String = arguments(2)
Dim colCoeffR As String = arguments(3)
Dim colCoeffN As String = arguments(4)
' creación de un objeto de impuesto
Dim objImpôt As impôt = Nothing
Try
objImpôt = New impôt(DSNimpots, tabImpots, colLimites, colCoeffR, colCoeffN)
Catch ex As Exception
Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
Environment.Exit(2)
End Try
' bucle infinito
While True
' al principio no hay errores
Dim erreur As Boolean = False
' se solicitan los parámetros para el cálculo del impuesto
Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
Dim paramètres As String = Console.In.ReadLine().Trim()
' ¿Hay algo que hacer?
If paramètres Is Nothing Or paramètres = "" Then
Exit While
End If
' verificación del número de argumentos en la línea introducida
Dim args As String() = paramètres.Split(Nothing)
Dim nbParamètres As Integer = args.Length
If nbParamètres <> 3 Then
Console.Error.WriteLine(syntaxe2)
erreur = True
End If
Dim marié As String
Dim nbEnfants As Integer
Dim salaire As Integer
If Not erreur Then
' Comprobación de la validez de los parámetros
' casado
marié = args(0).ToLower()
If marié <> "o" And marié <> "n" Then
Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
erreur = True
End If
' nbEnfants
nbEnfants = 0
Try
nbEnfants = Integer.Parse(args(1))
If nbEnfants < 0 Then
Throw New Exception
End If
Catch
Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
erreur = True
End Try
' salario
salaire = 0
Try
salaire = Integer.Parse(args(2))
If salaire < 0 Then
Throw New Exception
End If
Catch
Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
erreur = True
End Try
End If
If Not erreur Then
' los parámetros son correctos: se calcula el impuesto
Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
End If
End While
End Sub
End Module
Retomamos el mismo programa para que ahora utilice el servicio web impots a través de la clase proxy impots creada anteriormente. Nos vemos obligados a modificar ligeramente el código:
- mientras que la clase impôt original tenía un constructor con cinco argumentos, la clase proxy impots tiene un constructor sin parámetros. Los cinco parámetros, como hemos visto, ahora están fijados en el archivo de configuración del servicio web.
- Por lo tanto, ya no es necesario pasar estos cinco parámetros como argumentos al programa de prueba
El nuevo código es el siguiente:
Imports System
Imports Microsoft.VisualBasic
' página de prueba
Module testimpots
Public Sub Main(ByVal arguments() As String)
' programa interactivo de cálculo de impuestos
' el usuario introduce tres datos con el teclado: casado nbEnfants salario
' el programa muestra entonces el impuesto a pagar
Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"
' creación de un objeto de impuestos
Dim objImpôt As impôt = Nothing
Try
objImpôt = New impôt
Catch ex As Exception
Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
Environment.Exit(2)
End Try
' bucle infinito
Dim erreur As Boolean
While True
' al principio no hay error
erreur = False
' se solicitan los parámetros para el cálculo del impuesto
Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
Dim paramètres As String = Console.In.ReadLine().Trim()
' ¿Hay algo que hacer?
If paramètres Is Nothing Or paramètres = "" Then
Exit While
End If
' verificación del número de argumentos en la línea introducida
Dim args As String() = paramètres.Split(Nothing)
Dim nbParamètres As Integer = args.Length
If nbParamètres <> 3 Then
Console.Error.WriteLine(syntaxe2)
erreur = True
End If
If Not erreur Then
' verificación de la validez de los parámetros
' casado
Dim marié As String = args(0).ToLower()
If marié <> "o" And marié <> "n" Then
Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
erreur = True
End If
' nbEnfants
Dim nbEnfants As Integer = 0
Try
nbEnfants = Integer.Parse(args(1))
If nbEnfants < 0 Then
Throw New Exception
End If
Catch
Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument nbEnfants incorrect : tapez un entier positif ou nul"))
erreur = True
End Try
' salario
Dim salaire As Integer = 0
Try
salaire = Integer.Parse(args(2))
If salaire < 0 Then
Throw New Exception
End If
Catch
Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument salaire incorrect : tapez un entier positif ou nul"))
erreur = True
End Try
' si los parámetros son correctos, se calcula el impuesto
If Not erreur Then Console.Out.WriteLine(("impôt=" + objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
End If
End While
End Sub
End Module
Tenemos el proxy impots.dll y el código fuente testimpots en la misma carpeta.
dos>dir
09/03/2004 11:28 <REP> bin
09/03/2004 11:09 5 120 impots.dll
09/03/2004 11:34 3 396 testimpots.vb
09/03/2004 10:19 431 web.config
Compilamos el código fuente testimpots.vb:
dos>vbc /r:impots.dll /r:microsoft.visualbasic.dll /r:system.web.services.dll /r:system.dll testimpots.vb
dos>dir
09/03/2004 11:28 <REP> bin
09/03/2004 11:09 5 120 impots.dll
09/03/2004 11:05 3 364 impots.vb
09/03/2004 11:35 5 632 testimpots.exe
09/03/2004 11:34 3 396 testimpots.vb
09/03/2004 10:19 431 web.config
y luego lo ejecutamos:
dos>testimpots
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F
Obtenemos el resultado esperado.





