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

mientras que Netscape Navigator mostrará:

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

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

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

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 que están en la carpeta física del proyecto. Lo más interesante para nosotros es el archivo con la extensión asmx. Esa es la extensión de los servicios web. Un servicio web es administrado por VS.NET como una aplicación de Windows, c.a.d: una aplicación que cuenta con una interfaz gráfica y código para administrarla. Por eso, tenemos una ventana de diseño:

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

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

¿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. Esto se hace haciendo clic con el botón derecho del ratón sobre el archivo .asmx en cuestión y seleccionando la opción [Définir comme page de démarrage].

Quizás nos interese saber qué contiene el archivo service1.asmx. De hecho, con VS.NET hemos trabajado en el archivo service1.asmx.vb y no en el archivo service1.asmx. Este archivo se encuentra en la carpeta del proyecto:

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 también es el nombre del proyecto.
Volvamos a la página a la que se accede mediante la URL http://localhost/polyvbnet/demo/Service1.asmx:

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

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

Hemos acortado deliberadamente la página obtenida para no sobrecargar nuestra demostración. Observemos nuevamente la URL obtenida:
Si escribimos directamente esta URL en un navegador, obtendremos lo mismo que antes. Se nos invita a usar el botón [Appeler]. Hagámoslo. Obtenemos una nueva página:

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

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

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

Efectivamente, obtenemos el resultado de la llamada al método getBonjour del servicio web. Ahora sabemos cómo construir un servicio web sin vs.net. A partir de ahora, dejaremos de lado la forma en que está construido el servicio web para centrarnos únicamente en los archivos fundamentales.
10.3.3. Versión 3
Las dos versiones anteriores del servicio web [Bonjour] utilizaban dos archivos:
- un archivo .asmx, punto de entrada del servicio web
- un archivo .vb, código fuente del servicio web
Aquí demostramos que basta con un solo archivo .asmx. El código del servicio demo3.asmx es el siguiente:
Observamos que el código fuente del servicio ahora se encuentra 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 archivo en la carpeta <IISroot>\polyvbnet\demo3:

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

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

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

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

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

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

Ahora abrimos un navegador y accedemos a 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. En adelante, utilizaremos el método de la versión 3 para la creación del servicio y el método 4 para su localización. De esta manera, no necesitaremos VS.NET. Sin embargo, cabe destacar la utilidad de utilizar VS.NET por la ayuda que brinda en la depuración. Existen herramientas gratuitas para desarrollar aplicaciones web, en particular el producto WebMatrix patrocinado por Microsoft, que se puede encontrar en URL [http://www.asp.net/webmatrix]. Es una herramienta excelente para iniciarse en la programación web sin necesidad de inversión previa.
10.4. Un servicio web de operaciones
Consideremos un servicio web que ofrece cinco funciones:
- sumar(a,b), que devuelve 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
Aquí retomamos algunas explicaciones que ya se han dado, pero que vale la pena recordar o completar. La clase operations se parece a una clase VB.NET, aunque hay algunos puntos a destacar:
- los métodos van precedidos por un atributo <WebMethod()> que indica al compilador cuáles son los métodos que deben «publicarse» (c.a.d) y ponerse a disposición del cliente. Un método que no esté precedido por este atributo sería invisible para los clientes remotos. Podría tratarse de un método interno utilizado por otros métodos, pero que no está destinado a ser publicado.
- La clase deriva de la clase WebService definida en el espacio de nombres System.Web.Services. Esta herencia no siempre es obligatoria. En este ejemplo, por ejemplo, se podría prescindir de ella.
- La clase misma va precedida de un atributo <WebService(Namespace="st.istia.univ-angers.fr")> destinado a asignar un espacio de nombres al servicio web. Un proveedor de clases asigna un espacio de nombres a sus clases para darles un nombre único y así evitar conflictos con clases de otros proveedores que podrían tener el mismo nombre. En el caso de los servicios web, ocurre lo mismo. Cada servicio web debe poder identificarse mediante un nombre único, en este caso st.istia.univ-angers.fr.
- No hemos definido un constructor. Por lo tanto, se utilizará implícitamente el constructor de la clase padre.
El código fuente anterior no está destinado directamente al compilador VB.NET, sino al servidor web IIS. Debe llevar el sufijo .asmx y guardarse en el árbol de directorios del servidor web. Aquí lo guardamos con el nombre operations.asmx en la carpeta <IISroot>\polyvbnet\operations:

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

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 que aparece nos invita a probar el método ajouter proporcionándole los dos argumentos a y b que necesita. Recordemos la definición del método ajouter:
Cabe señalar que la página ha tomado los nombres de los argumentos a y b utilizados en la definición del método. Al hacer clic en el botón Appeler, se obtiene la siguiente respuesta en una ventana separada del navegador:

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

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

Obtenemos la siguiente página:

Usemos el botón [Appeler] que aparece 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 presentan 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. Ahora los analizaremos uno por uno.
Nota: en las primeras versiones de VS.NET, existía un tercer método llamado HTTP-GET. A la fecha de redacción de este documento (marzo de 2004), este método ya no parece estar disponible. Esto significa que el servicio web generado por VS.NET no acepta solicitudes GET. Esto no significa que no se puedan crear servicios web que acepten solicitudes GET, especialmente con otras herramientas distintas a VS.NET o simplemente de forma manual.
10.5. Un cliente HTTP-POST
Seguimos el método propuesto por el servicio web:

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


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

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

Aquí tenemos un archivo XML que describe con precisión todas las funciones del servicio web, indicando para cada una de ellas el tipo y el número de parámetros, así como el tipo de resultado. A este archivo se le denomina el archivo WSDL del servicio porque utiliza el lenguaje WSDL (Web Services Description Language). A partir de este archivo, se puede generar una clase proxy con la ayuda de la herramienta wsdl:
dos>wsdl http://localhost/operations/operations.asmx?wsdl /language=vb
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Écriture du fichier 'D:\data\devel\vbnet\poly\chap9\clientproxy\operations.vb'.
dos>dir
04/03/2004 17:17 6 663 operations.vb
La herramienta wsdl genera un archivo fuente VB.NET (opción /language=vb) con el nombre de la clase que implementa el servicio web, en este caso operations. Veamos una parte del código generado:
'------------------------------------------------------------------------------
' <autogenerado>
' Este código fue generado por una herramienta.
' Versión de tiempo de ejecución: 1.1.4322.573
'
' Los cambios en este archivo pueden provocar un comportamiento incorrecto y se perderán si
' se vuelve a generar el código.
' </autogenerado>
'------------------------------------------------------------------------------
Option Strict Off
Option Explicit On
Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Xml.Serialization
'
'Este código fuente fue generado automáticamente por wsdl, Versión=1.1.4322.573.
'
'<remarks/>
<System.Diagnostics.DebuggerStepThroughAttribute(), _
System.ComponentModel.DesignerCategoryAttribute("code"), _
System.Web.Services.WebServiceBindingAttribute(Name:="operationsSoap", [Namespace]:="st.istia.univ-angers.fr")> _
Public Class operations
Inherits System.Web.Services.Protocols.SoapHttpClientProtocol
'<remarks/>
Public Sub New()
MyBase.New
Me.Url = "http://localhost/operations/operations.asmx"
End Sub
'<observaciones/>
<System.Web.Services.Protocols.SoapDocumentMethodAttribute("st.istia.univ-angers.fr/ajouter", RequestNamespace:="st.istia.univ-angers.fr", ResponseNamespace:="st.istia.univ-angers.fr", Use:=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle:=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)> _
Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
Return CType(results(0),Double)
End Function
'<observaciones/>
Public Function Beginajouter(ByVal a As Double, ByVal b As Double, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult
Return Me.BeginInvoke("ajouter", New Object() {a, b}, callback, asyncState)
End Function
'<comentarios/>
Public Function Endajouter(ByVal asyncResult As System.IAsyncResult) As Double
Dim results() As Object = Me.EndInvoke(asyncResult)
Return CType(results(0),Double)
End Function
....
Este código parece un poco complejo a primera vista. No necesitamos entender los detalles para poder usarlo. Veamos primero la declaración de la clase:
La clase lleva el nombre operations del servicio web para el que fue creada. 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 mencionada anteriormente no define por sí misma el atributo url. Este se hereda de la clase de la que deriva el proxy: System.Web.Services.Protocols.SoapHttpClientProtocol. Veamos ahora lo que se refiere al método ajouter:
Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
Return CType(results(0),Double)
End Function
Se puede observar que tiene la misma firma que en el servicio web operations, donde se definía de la siguiente manera:
<WebMethod> _
Function ajouter(a As Double, b As Double) As Double
Return a + b
End Function 'ajouter
La forma en que esta clase se comunica con el servicio web no se muestra aquí. Esta comunicación es manejada íntegramente por la clase padre System.Web.Services.Protocols.SoapHttpClientProtocol. En el proxy solo se encuentra lo que lo diferencia de los demás proxies:
- el URL del servicio web asociado
- la definición de los métodos del servicio asociado.
Para utilizar los métodos del servicio web operations, un cliente solo necesita la clase proxy operations generada anteriormente. Compilemos esta clase en un archivo assembly:
Ahora escribamos un cliente de consola. Se invoca sin parámetros y ejecuta las solicitudes que se ingresan desde el teclado:
dos>testclientproxy
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b
ajouter 4 5
résultat=9
soustraire 9 8
résultat=1
multiplier 10 4
résultat=40
diviser 6 7
résultat=0,857142857142857
toutfaire 10 20
résultats=[30,-10,200,0,5]
diviser 5 0
résultat=+Infini
fin
El código del cliente es el siguiente:
' espacios de nombres
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Public Module testClientProxy
' ejecuta de manera interactiva los comandos ingresados desde el teclado
' y los envía al servicio web de operaciones
Public Sub Main()
' ya no hay argumentos; la URL del servicio web está codificada de forma fija en el proxy
' creación de un diccionario de funciones del servicio web
Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser", "toutfaire"}
Dim dicoFonctions As New Hashtable
Dim i As Integer
For i = 0 To fonctions.Length - 1
dicoFonctions.Add(fonctions(i), True)
Next i
' se crea un objeto proxy de operaciones
Dim myOperations As operations = Nothing
Try
myOperations = New operations
Catch ex As Exception
' error de conexión
erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
End Try
' las solicitudes del usuario se ingresan con el teclado
' en el formato «función a b»; terminan con el comando «fin»
Dim commande As String = Nothing ' commande tapée au clavier
Dim champs As String() = Nothing ' champs d'une ligne de commande
' se le pide al usuario
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)
' algunos datos locales
Dim erreurCommande As Boolean
Dim fonction As String
Dim a, b As Double
' bucle de entrada de los comandos escritos con el teclado
While True
' al principio no hay error
erreurCommande = False
' lectura del comando
commande = Console.In.ReadLine().Trim().ToLower()
' ¿Terminado?
If commande Is Nothing Or commande = "fin" Then
Exit While
End If
' descomposición del comando en campos
champs = Regex.Split(commande, "\s+")
Try
' se necesitan tres campos
If champs.Length <> 3 Then
Throw New Exception
End If
' el campo 0 debe ser una función reconocida
fonction = champs(0)
If Not dicoFonctions.ContainsKey(fonction) Then
Throw New Exception
End If
' el campo 1 debe ser un número válido
a = Double.Parse(champs(1))
' el campo 2 debe ser un número válido
b = Double.Parse(champs(2))
Catch
' comando inválido
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
erreurCommande = True
End Try
' se envía la solicitud al servicio web
If Not erreurCommande Then
Try
Dim résultat As Double
Dim résultats() As Double
If fonction = "ajouter" Then
résultat = myOperations.ajouter(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "soustraire" Then
résultat = myOperations.soustraire(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "multiplier" Then
résultat = myOperations.multiplier(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "diviser" Then
résultat = myOperations.diviser(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "toutfaire" Then
résultats = myOperations.toutfaire(a, b)
Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
résultats(2).ToString + "," + résultats(3).ToString + "]"))
End If
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
End If
End While
End Sub
' visualización de errores
Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
' visualización de error
System.Console.Error.WriteLine(msg)
' interrupción con error
Environment.Exit(exitCode)
End Sub
End Module
Solo analizamos el código específico del uso de la clase proxy. En primer lugar, se crea un objeto proxy operations:
' se crea un objeto proxy de operaciones
Dim myOperations As operations = Nothing
Try
myOperations = New operations
Catch ex As Exception
' error de conexión
erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
End Try
Se ingresan las líneas de función a y b mediante el teclado. A partir de esta información, se invocan los métodos apropiados del proxy:
' se realiza la solicitud al servicio web
If Not erreurCommande Then
Try
Dim résultat As Double
Dim résultats() As Double
If fonction = "ajouter" Then
résultat = myOperations.ajouter(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "soustraire" Then
résultat = myOperations.soustraire(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "multiplier" Then
résultat = myOperations.multiplier(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "diviser" Then
résultat = myOperations.diviser(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "toutfaire" Then
résultats = myOperations.toutfaire(a, b)
Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
résultats(2).ToString + "," + résultats(3).ToString + "]"))
End If
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
Aquí se procesa por primera vez la operación toutfaire, que realiza las cuatro operaciones. Hasta ahora se había ignorado porque envía una matriz de números encapsulada en un contenedor XML, más complicado de manejar que las respuestas simples XML de las otras funciones, que solo entregan un único resultado. Se observa que aquí, con la clase proxy, utilizar el método toutfaire no es más complicado que utilizar los otros métodos. La aplicación se compiló en una ventana de DOS de la siguiente manera:
dos>vbc /r:operations.dll /r:system.dll /r:system.web.services.dll testClientProxy.vb
dos>dir
04/03/2004 17:17 6 663 operations.vb
04/03/2004 17:24 7 680 operations.dll
04/03/2004 17:41 4 099 testClientProxy.vb
04/03/2004 17:41 5 632 testClientProxy.exe
10.9. Configurar un servicio web
Un servicio web puede necesitar información de configuración para inicializarse correctamente. Con el servidor IIS, esta información se puede colocar en un archivo llamado web.config, ubicado en la misma carpeta que el servicio web. Supongamos que queremos crear un servicio web que necesita dos datos para inicializarse: un nombre y una edad. Estos dos datos pueden colocarse en el archivo web.config de la siguiente manera:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Los parámetros de inicialización se encuentran en un paquete XML:
Se declarará un parámetro de inicialización con el nombre P y el valor V mediante la línea:
<add key="P" value="V"/>
¿Cómo recupera el servicio web esta información? Cuando IIS carga un servicio web, verifica si en la misma carpeta hay un archivo web.config. Si es así, lo lee. El valor V de un parámetro P se obtiene mediante la instrucción:
donde ConfigurationSettings es una clase en el espacio de nombres System.Configuration.
Probemos esta técnica en el siguiente servicio web:
<%@ WebService language="VB" class=personne %>
Imports System.Web.Services
imports System.Configuration
<WebService([Namespace] := "st.istia.univ-angers.fr")> _
Public Class personne
Inherits WebService
' atributos
Private nom As String
Private age As Integer
' constructor
Public Sub New()
' inicialización de atributos
nom = ConfigurationSettings.AppSettings("nom")
age = Integer.Parse(ConfigurationSettings.AppSettings("age"))
End Sub
<WebMethod> _
Function id() As String
Return "[" + nom + "," + age.ToString + "]"
End Function
End Class
El servicio web personne tiene dos atributos, nom y age, que se inicializan en su constructor sin parámetros a partir de los valores leídos en el archivo de configuración web.config del servicio personne. Este archivo es el siguiente:
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Además, el servicio web tiene un identificador <WebMethod> sin parámetros que simplemente devuelve los atributos nom y age. El servicio está registrado en el archivo fuente personne.asmx, que se encuentra junto con su archivo de configuración en la carpeta c:\inetpub\wwwroot\st\personne:
Asociemos una carpeta virtual IIS/config a la carpeta física anterior. Ejecutemos IIS y, luego, desde un navegador, accedamos a la URL http://localhost/config/personne.asmx del servicio personne:

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

El método id no tiene parámetros. Usemos 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, la cual se construye a partir de la información contenida en una base de datos ODBC:
' opciones
Option Strict On
Option Explicit On
' espacios de nombres
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Public Class impôt
' los datos necesarios para el cálculo del impuesto
' provienen de una fuente externa
Private limites(), coeffR(), coeffN() As Decimal
' creador
Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
' se verifica que las tres tablas tengan el mismo tamaño
Dim OK As Boolean = LIMITES.Length = COEFFR.Length And LIMITES.Length = COEFFN.Length
If Not OK Then
Throw New Exception("Les 3 tableaux fournis n'ont pas la même taille(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
End If
' está bien
Me.limites = LIMITES
Me.coeffR = COEFFR
Me.coeffN = COEFFN
End Sub
' constructor 2
Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
' inicializa los tres tableros de límites, coeffR, coeffN a partir
' del contenido de la tabla Timpots de la base de datos ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
' puede generar una excepción
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Dim impotsConn As OdbcConnection = Nothing ' la connexion
Dim sqlCommand As OdbcCommand = Nothing ' la commande SQL
': la consulta SELECT
Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
': tablas para recuperar los datos
Dim tLimites As New ArrayList
Dim tCoeffR As New ArrayList
Dim tCoeffN As New ArrayList
' se intenta acceder a la base de datos
impotsConn = New OdbcConnection(connectString)
impotsConn.Open()
' se crea un objeto de comando
sqlCommand = New OdbcCommand(selectCommand, impotsConn)
' se ejecuta la consulta
Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
' Análisis de la tabla recuperada
While myReader.Read()
' los datos de la fila actual se colocan en las tablas
tLimites.Add(myReader(colLimites))
tCoeffR.Add(myReader(colCoeffR))
tCoeffN.Add(myReader(colCoeffN))
End While
' Se liberan los recursos
myReader.Close()
impotsConn.Close()
' Las tablas dinámicas se transfieren a tablas estáticas
Me.limites = New Decimal(tLimites.Count) {}
Me.coeffR = New Decimal(tLimites.Count) {}
Me.coeffN = New Decimal(tLimites.Count) {}
Dim i As Integer
For i = 0 To tLimites.Count - 1
limites(i) = Decimal.Parse(tLimites(i).ToString())
coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
Next i
End Sub
' Cálculo del impuesto
Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
' cálculo del número de participaciones
Dim nbParts As Decimal
If marié Then
nbParts = CDec(nbEnfants) / 2 + 2
Else
nbParts = CDec(nbEnfants) / 2 + 1
End If
If nbEnfants >= 3 Then
nbParts += 0.5D
End If
' cálculo de la renta imponible y del coeficiente familiar
Dim revenu As Decimal = 0.72D * salaire
Dim QF As Decimal = revenu / nbParts
' cálculo del impuesto
limites((limites.Length - 1)) = QF + 1
Dim i As Integer = 0
While QF > limites(i)
i += 1
End While
' resultado
Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
End Function
End Class
En el servicio web, solo se puede utilizar un constructor sin parámetros. Por lo tanto, el constructor de la clase quedará así:
' constructor
Public Sub New()
' inicializa las tres tablas de límites, coeffR, coeffN a partir
' del contenido de la tabla Timpots de la base de datos ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
' puede generar una excepción
': se recuperan los parámetros de configuración del servicio
Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")
': se procesa la base de datos
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Los cinco parámetros del constructor de la clase anterior ahora se leen del archivo web.config del servicio. El código del archivo fuente impots.asmx es el siguiente. Incluye la mayor parte del código anterior. Nos hemos limitado a resaltar las partes del código específicas del servicio web:
<%@ WebService language="VB" class=impots %>
' creación de un servicio web de impuestos
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Imports System.Configuration
Imports System.Web.Services
<WebService([Namespace]:="st.istia.univ-angers.fr")> _
Public Class impôt
Inherits WebService
' los datos necesarios para el cálculo del impuesto
' provienen de una fuente externa
Private limites(), coeffR(), coeffN() As Decimal
Private OK As Boolean = False
Private errMessage As String = ""
' constructor
Public Sub New()
' inicializa las tres tablas de límites, coeffR, coeffN a partir
' del contenido de la tabla Timpots de la base de datos ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN son las tres columnas de esta tabla
' puede generar una excepción
': se recuperan los parámetros de configuración del servicio
Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")
': se procesa la base de datos
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Dim impotsConn As OdbcConnection = Nothing ' la connexion
Dim sqlCommand As OdbcCommand = Nothing ' la commande SQL
Dim myReader As OdbcDataReader ' lecteur de données Odbc
': la consulta SELECT
Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
' tablas para recuperar los datos
Dim tLimites As New ArrayList
Dim tCoeffR As New ArrayList
Dim tCoeffN As New ArrayList
' se intenta acceder a la base de datos
Try
impotsConn = New OdbcConnection(connectString)
impotsConn.Open()
' se crea un objeto de comando
sqlCommand = New OdbcCommand(selectCommand, impotsConn)
' se ejecuta la consulta
myReader = sqlCommand.ExecuteReader()
' Análisis de la tabla recuperada
While myReader.Read()
' Los datos de la fila actual se colocan en las tablas
tLimites.Add(myReader(colLimites))
tCoeffR.Add(myReader(colCoeffR))
tCoeffN.Add(myReader(colCoeffN))
End While
' Se liberan los recursos
myReader.Close()
impotsConn.Close()
' Las tablas dinámicas se transfieren a tablas estáticas
Me.limites = New Decimal(tLimites.Count) {}
Me.coeffR = New Decimal(tLimites.Count) {}
Me.coeffN = New Decimal(tLimites.Count) {}
Dim i As Integer
For i = 0 To tLimites.Count - 1
limites(i) = Decimal.Parse(tLimites(i).ToString())
coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
Next i
' Todo listo
OK = True
errMessage = ""
Catch ex As Exception
' error
OK = False
errMessage += "[" + ex.Message + "]"
End Try
End Sub
' cálculo del impuesto
<WebMethod()> _
Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
' cálculo del número de participaciones
Dim nbParts As Decimal
If marié Then
nbParts = CDec(nbEnfants) / 2 + 2
Else
nbParts = CDec(nbEnfants) / 2 + 1
End If
If nbEnfants >= 3 Then
nbParts += 0.5D
End If
' cálculo de la renta imponible y del coeficiente familiar
Dim revenu As Decimal = 0.72D * salaire
Dim QF As Decimal = revenu / nbParts
' cálculo del impuesto
limites((limites.Length - 1)) = QF + 1
Dim i As Integer = 0
While QF > limites(i)
i += 1
End While
' devuelve el resultado
Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
End Function
' ID
<WebMethod()> _
Function id() As String
' para verificar si todo está bien OK
Return "[" + OK + "," + errMessage + "]"
End Function
End Class
Explicaremos las pocas modificaciones realizadas en la clase impots, además de las necesarias para convertirla en un servicio web:
- la lectura de la base de datos en el constructor puede fallar. Por eso, hemos agregado dos atributos a nuestra clase y un método:
- El valor booleano OK es vrai si se pudo leer la base de datos, y faux en caso contrario
- la cadena errMessage contiene un mensaje de error si no se pudo leer la base de datos.
- El método id sin parámetros permite obtener el valor de estos dos atributos.
- Para manejar un posible error de acceso a la base de datos, la parte del código del constructor relacionada con este acceso se ha rodeado de un try-catch.
El archivo de configuración del servicio web.config es el siguiente:
<configuration>
<appSettings>
<add key="DSN" value="mysql-impots" />
<add key="TABLE" value="timpots" />
<add key="COL_LIMITES" value="limites" />
<add key="COL_COEFFR" value="coeffr" />
<add key="COL_COEFFN" value="coeffn" />
</appSettings>
</configuration>
Al intentar cargar por primera vez el servicio impots, el compilador informó que no pudo encontrar el espacio de nombres Microsoft.Data.Odbc utilizado en la directiva:
Tras consultar la documentación
- se agregó una directiva de compilación en web.config para indicar que se debía utilizar el ensamblador Microsoft.Data.odbc
- se colocó una copia del archivo microsoft.data.odbc.dll en la carpeta bin del proyecto. El compilador de un servicio web explora sistemáticamente esta carpeta cuando busca un «ensamblado».
Parecen existir otras soluciones, pero no se han analizado a fondo en este caso. Por lo tanto, el archivo de configuración quedó así:
<configuration>
<appSettings>
<add key="DSN" value="mysql-impots" />
<add key="TABLE" value="timpots" />
<add key="COL_LIMITES" value="limites" />
<add key="COL_COEFFR" value="coeffr" />
<add key="COL_COEFFN" value="coeffn" />
</appSettings>
<system.web>
<compilation>
<assemblies>
<add assembly="Microsoft.Data.Odbc" />
</assemblies>
</compilation>
</system.web>
</configuration>
El contenido de la carpeta impots\bin:
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 cargó correctamente. No siempre ha sido así y por eso hemos agregado el método id para tener acceso al mensaje de error. El error radicaba en que el nombre DSN de la base de datos se había definido como usuario DSN, cuando debía definirse como sistema DSN. Esta distinción se realiza en el administrador de fuentes ODBC de 32 bits:
![]() |
Volvamos a la página del servicio:

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





