Skip to content

10. Serviços Web

10.1. Introdução

No capítulo anterior, apresentámos várias aplicações cliente-servidor TCP/IP. Uma vez que os clientes e o servidor trocam linhas de texto, estas podem ser escritas em qualquer linguagem. O cliente precisa apenas de conhecer o protocolo de comunicação esperado pelo servidor. Os serviços Web são aplicações de servidor TCP/IP com as seguintes características:

  • São hospedados por servidores web e, por isso, o protocolo de comunicação cliente-servidor é o HTTP (HyperText Transfer Protocol), um protocolo que funciona sobre TCP/IP.
  • O serviço Web possui um protocolo de comunicação padrão, independentemente do serviço prestado. Um serviço Web oferece vários serviços S1, S2, .., Sn. Cada um deles espera parâmetros fornecidos pelo cliente e devolve um resultado ao cliente. Para cada serviço, o cliente precisa de saber:
    • o nome exato do serviço Si
    • a lista de parâmetros a fornecer e os seus tipos
    • o tipo de resultado devolvido pelo serviço

Uma vez conhecidos estes elementos, a interação cliente-servidor segue o mesmo formato, independentemente do serviço Web que está a ser consultado. O código do cliente é, assim, padronizado.

  • Por razões de segurança relacionadas com ataques provenientes da Internet, muitas organizações mantêm redes privadas e abrem apenas determinadas portas nos seus servidores para a Internet: principalmente a porta 80 para o serviço Web. Todas as outras portas estão bloqueadas. Consequentemente, as aplicações cliente-servidor, como as apresentadas no capítulo anterior, são construídas dentro da rede privada (intranet) e geralmente não são acessíveis a partir do exterior. Alojamento de um serviço num servidor Web torna-o acessível a toda a comunidade da Internet.
  • O serviço web pode ser modelado como um objeto remoto. Os serviços oferecidos tornam-se então métodos desse objeto. Um cliente pode aceder a este objeto remoto como se fosse local. Isto oculta toda a camada de comunicação de rede e permite o desenvolvimento de um cliente independente dessa camada. Se a camada mudar, o cliente não precisa de ser modificado. Esta é uma enorme vantagem e provavelmente o principal benefício dos serviços web.
  • Tal como nas aplicações cliente-servidor TCP/IP apresentadas no capítulo anterior, o cliente e o servidor podem ser escritos em qualquer linguagem. Eles trocam linhas de texto. Estas consistem em duas partes:
    • os cabeçalhos exigidos pelo protocolo HTTP
    • o corpo da mensagem. Para uma resposta do servidor ao cliente, o corpo está no formato XML (eXtensible Markup Language). Para um pedido do cliente ao servidor, o corpo da mensagem pode assumir várias formas, incluindo XML. O pedido XML do cliente pode utilizar um formato específico chamado SOAP (Simple Object Access Protocol). Neste caso, a resposta do servidor também segue o formato SOAP.

10.2. Navegadores e XML

Os serviços Web enviam XML aos seus clientes. Os navegadores podem reagir de forma diferente ao receber este fluxo de XML. O Internet Explorer possui uma folha de estilo predefinida que lhe permite apresentar o XML. O Netscape Communicator, no entanto, não possui esta folha de estilo e não apresenta o código XML recebido. É necessário visualizar o código-fonte da página recebida para aceder ao XML. Aqui está um exemplo. Para o seguinte código XML:

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

O Internet Explorer irá apresentar a seguinte página:

Image

enquanto o Netscape Navigator exibirá:

Image

Se visualizarmos o código-fonte da página recebida pelo Netscape, obtemos:

Image

O Netscape recebeu, de facto, o mesmo conteúdo que o Internet Explorer, mas apresentou-o de forma diferente. A partir daqui, utilizaremos o Internet Explorer para as capturas de ecrã.

10.3. Um primeiro serviço web

Vamos explorar os serviços web através de um exemplo muito simples disponível em três versões.

10.3.1. Versão 1

Para esta primeira versão, utilizaremos o VS.NET, que tem a vantagem de poder gerar um esqueleto de serviço web que fica imediatamente operacional. Assim que compreendermos esta arquitetura, poderemos começar a trabalhar de forma independente. Esse será o foco das versões seguintes.

Usando o VS.NET, vamos criar um novo projeto através da opção [Ficheiro/Novo/Projeto]:

Image

Observe os seguintes pontos:

  • o tipo de projeto é Visual Basic (painel esquerdo)
  • o modelo do projeto é Serviço Web ASP.NET (painel direito)
  • a localização é flexível. Aqui, o serviço Web será hospedado por um servidor Web IIS local. A sua URL será, portanto, http://localhost/[path], onde [path] deve ser definido. Aqui, escolhemos o caminho http://localhost/polyvbnet/demo. O VS.NET criará então uma pasta para este projeto. Onde? O servidor IIS tem um diretório raiz para a árvore de documentos Web que serve. Vamos chamar a esta raiz <IISroot>. Ela corresponde à URL http://localhost. Podemos deduzir que a URL http://localhost/polyvbnet/demo estará associada à pasta <IISroot>/polyvbnet/demo. <IISroot> é normalmente a pasta \inetpub\wwwroot na unidade onde o IIS foi instalado. No nosso exemplo, trata-se da unidade E. A pasta criada pelo VS.NET é, portanto, a pasta e:\inetpub\wwwroot\polyvbnet\demo:

Image

Como sempre, há uma abundância de pastas criadas. Nem sempre são úteis. Explicaremos apenas as que precisamos num determinado momento. O VS.NET criou um projeto:

Image

Encontramos alguns dos ficheiros presentes na pasta física do projeto. O mais interessante para nós é o ficheiro com a extensão .asmx. Esta é a extensão para serviços web. Um serviço web é gerido pelo VS.NET como uma aplicação Windows, ou seja, uma aplicação que possui uma interface gráfica de utilizador e código para a gerir. É por isso que temos uma janela de design:

Image

Um serviço web normalmente não possui uma interface gráfica de utilizador. Representa um objeto que pode ser chamado remotamente. Possui métodos, e as aplicações chamam esses métodos. Vamos, portanto, considerá-lo como um objeto clássico com a característica única de poder ser instanciado remotamente através da rede. Por isso, não utilizaremos a janela de design fornecida pelo VS.NET. Em vez disso, vamos concentrar-nos no código do serviço utilizando a opção View/Code:

Image

Há vários pontos que merecem destaque:

  • O ficheiro chama-se Service1.asmx.vb, e não Service1.asmx. Voltaremos ao conteúdo do ficheiro Service1.asmx um pouco mais tarde.
  • Vemos uma janela de código semelhante à que tínhamos ao criar aplicações para Windows com o VS.NET

O código gerado pelo VS.NET é o seguinte:


Imports System.Web.Services
 
<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
    Inherits System.Web.Services.WebService
 
#Region " Code généré par le Concepteur des services Web "
 
    Public Sub New()
        MyBase.New()
 
        'This call is required by the Web Services Designer.
        InitializeComponent()
 
        'Add your initialization code after the InitializeComponent() call
 
    End Sub
 
    'Required by the Web Services Designer
    Private components As System.ComponentModel.IContainer
 
    'REMARQUE: the following procedure is required by the Web Services Designer
    'It can be modified using the Web Services Designer.  
    'Do not modify it using the code editor.
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
    End Sub
 
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        'CODEGEN: this procedure is required by the Web Services Designer
        'Do not modify it using the code editor.
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub
 
#End Region
 
    ' EXEMPLE DE SERVICE WEB
    ' The HelloWorld() service example returns the string Hello World.
    ' To generate, leave the following lines uncommented, then save and generate the project.
    ' To test this Web service, make sure that the .asmx file is the start page
    ' and press F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World
    ' End Function
 
End Class

Primeiro, repare que temos aqui uma classe, a classe Service1, que deriva da classe WebService:

Public Class Service1
    Inherits System.Web.Services.WebService

Isto leva-nos a importar o namespace System.Web.Services:


Imports System.Web.Services

A declaração da classe é precedida por um atributo de compilação:


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

O atributo System.Web.Services.WebService() indica que a classe seguinte é um serviço web. Este atributo aceita vários parâmetros, incluindo um chamado NameSpace. É utilizado para colocar o serviço web num namespace. De facto, é possível imaginar que existam vários serviços web denominados «weather» no mundo. Precisamos de uma forma de os diferenciar. O espaço de nomes torna isso possível. Um poderia chamar-se [namespace1].weather e outro [namespace2].weather. Este é um conceito análogo aos espaços de nomes de classes. O VS.NET gerou código automaticamente e colocou-o numa região do código-fonte:


#Region " Code généré par le Concepteur des services Web "

Se olharmos para este código, é o mesmo código que o designer gerou quando criámos aplicações Windows. Este é um código que podemos simplesmente eliminar se não tivermos uma interface gráfica de utilizador, o que será o caso dos serviços Web.

A aula termina com um exemplo de como um serviço Web pode ser:


#End Region
 
    ' EXEMPLE DE SERVICE WEB
    ' L'exemple de service HelloWorld() retourne la chaîne Hello World.
    ' Pour générer, ne commentez pas les lignes suivantes, puis enregistrez et générez le projet.
    ' Pour tester ce service Web, assurez-vous que le fichier .asmx est la page de démarrage
    ' et appuyez sur F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' End Function

Com base no que acabou de ser dito, limpamos o código para que fique da seguinte forma:


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

Agora temos uma ideia mais clara.

  • Um serviço web é uma classe derivada da classe WebService
  • A classe é qualificada pelo atributo <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. Colocamos, portanto, o nosso serviço no namespace st.istia.univ-angers.fr.
  • Os métodos da classe são qualificados por um atributo <WebMethod()>, indicando que se trata de um método que pode ser chamado remotamente através da rede

A classe que fornece o nosso serviço web chama-se, portanto, Bonjour e possui um único método, também chamado Bonjour, que devolve uma string. Estamos prontos para um teste inicial.

  • Inicie o servidor web IIS, caso ainda não o tenha feito
  • Utilize a opção Debug/Run Without Debugging. VS.NET

O VS.NET irá então compilar toda a aplicação, iniciar um navegador (geralmente o Internet Explorer, se estiver disponível) e apresentar o URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

Porquê o URL http://localhost/polyvbnet/demo/Service1.asmx? Porque era o único ficheiro .asmx no projeto:

Image

Se houvesse vários ficheiros .asmx, teríamos de especificar qual deles deveria ser executado primeiro. Isto é feito clicando com o botão direito do rato no ficheiro .asmx relevante e selecionando a opção [Definir como página inicial].

Image

Talvez lhe interesse saber o que o ficheiro service1.asmx contém. Na verdade, com o VS.NET, trabalhámos no ficheiro service1.asmx.vb e não no ficheiro service1.asmx. Este ficheiro encontra-se na pasta do projeto:

Image

Vamos abri-lo com um editor de texto (Bloco de Notas ou outro). Obtemos o seguinte conteúdo:

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

O ficheiro contém uma diretiva simples para o servidor IIS indicando:

  • que se trata de um serviço web (palavra-chave WebService)
  • que a linguagem da classe deste serviço é Visual Basic (Language="vb")
  • que o código-fonte desta classe se encontra no ficheiro Service1.asmx.vb (Codebehind="Service1.asmx.vb")
  • que a classe que implementa o serviço se chama demo.Bonjour (Class="demo.Bonjour"). Note-se que o VS.NET colocou a classe Bonjour no namespace demo, que é também o nome do projeto.

Voltemos à página acessada no URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

Quem escreveu o código HTML da página acima? Não fomos nós — sabemos disso. Foi o IIS, que apresenta os serviços web de forma padrão. Esta página oferece-nos dois links. Vamos seguir o primeiro [Descrição do Serviço]:

Image

Ups... isso é um XML bastante obscuro. Repare, no entanto, no URL

http://localhost/polyvbnet/demo/Service1.asmx?WSDL. Abra um navegador e digite este URL diretamente. Obterá o mesmo resultado que antes. Por isso, lembre-se de que o URL http://serviceweb?WSDL dá acesso à descrição XML do serviço web. Voltemos à página inicial e cliquemos no link [Hello]. Lembre-se de que Hello é um método do serviço web. Se houvesse vários métodos, todos eles estariam listados aqui. Obtemos a seguinte nova página:

Image

Truncámos intencionalmente a página resultante para manter a nossa demonstração concisa. Repare novamente na URL:

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

Se digitarmos esta URL diretamente num navegador, obteremos o mesmo resultado que acima. Somos solicitados a utilizar o botão [Call]. Vamos fazê-lo. Aparece uma nova página:

Image

Trata-se novamente de XML. Contém duas informações que estavam presentes no nosso serviço web:

  • o namespace st.istia.univ-angers.fr do nosso serviço

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

        Return "bonjour !"

O que aprendemos?

  • como escrever um serviço web S
  • como chamá-lo

Vamos agora ver como escrever um serviço web sem utilizar o VS.NET.

10.3.2. Versão 2

No exemplo anterior, o VS.NET fez muitas coisas por si próprio. Será possível criar um serviço Web sem esta ferramenta? A resposta é sim, e vamos mostrar-lhe como agora. Utilizando um editor de texto, vamos criar o seguinte serviço 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

A classe chama-se Bonjour2 e tem um método chamado getBonjour. Está localizada no ficheiro demo2.vb, que por sua vez se encontra na estrutura de diretórios do servidor IIS, na pasta E:\Inetpub\wwwroot\polyvbnet\demo2. É uma classe VB.NET padrão que, por isso, pode ser compilada:

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 o assembly demo2.dll numa pasta bin (este nome é obrigatório):

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

Vamos agora criar o ficheiro demo2.asmx. Este é o ficheiro que será chamado pelos clientes web. O seu conteúdo é o seguinte:

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

Já nos deparámos com esta diretiva. Ela indica que:

  • a classe do serviço Web chama-se Bonjour2 e está localizada no assembly demo2.dll. O IIS irá procurar este assembly em vários locais, incluindo a pasta bin do serviço Web. É por isso que colocámos o assembly demo2.dll nesse local.

Agora podemos realizar vários testes. Certificamo-nos de que o IIS está a funcionar e solicitamos o URL http://localhost/polyvbnet/demo2/demo2.asmx num navegador:

Image

Em seguida, a URL http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Em seguida, a URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, onde getBonjour é o nome do único método no nosso serviço web:

Image

Utilizamos o botão [Call] acima:

Image

Conseguimos obter o resultado da chamada ao método getBonjour do serviço web. Agora sabemos como criar um serviço web sem o Visual Studio .NET. A partir daqui, iremos ignorar os detalhes de como o serviço web é criado e concentrar-nos exclusivamente nos ficheiros fundamentais.

10.3.3. Versão 3

As duas versões anteriores do serviço web [Hello] utilizavam dois ficheiros:

  • um ficheiro .asmx, o ponto de entrada do serviço web
  • um ficheiro .vb, o código-fonte do serviço web

Aqui, mostramos que um único ficheiro .asmx é suficiente. O código para o serviço demo3.asmx é o seguinte:

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

Imports System.Web.Services

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

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

Podemos ver que o código-fonte do serviço está agora diretamente no ficheiro-fonte demo3.asmx. A diretiva

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

já não faz referência a uma classe num assembly externo, mas sim a uma classe localizada no mesmo ficheiro de código-fonte. Vamos colocar este ficheiro na pasta <IISroot>\polyvbnet\demo3:

Image

Vamos iniciar o IIS e aceder ao URL http://localhost/polyvbnet/demo3/demo3.asmx:

Image

Notamos uma diferença significativa em relação à versão anterior: não tivemos de compilar o código VB do serviço. O IIS realizou essa compilação por conta própria, utilizando o compilador VB.NET instalado na mesma máquina. Em seguida, apresentou a página. Se houver um erro de compilação, o IIS irá reportá-lo:

Image

10.3.4. Versão 4

Aqui, focamo-nos na configuração do servidor IIS. Até agora, sempre colocámos os nossos serviços web no diretório raiz <IISroot> do servidor IIS, neste caso [e:\inetpub\wwwroot]. Demonstramos aqui que podemos colocar o serviço web em qualquer local. Isto é feito utilizando diretórios virtuais do IIS. Vamos colocar o nosso serviço no seguinte diretório:

Image

A pasta [D:\data\devel\vbnet\poly\chap9\demo3] não se encontra na árvore de diretórios do servidor IIS. Temos de indicar esta localização ao IIS criando uma pasta virtual no IIS. Vamos abrir o IIS e selecionar a opção [Avançado] abaixo:

Image

É apresentada uma lista de diretórios virtuais. Não nos deteremos nesta lista. Criamos um novo diretório virtual utilizando o botão [Adicionar] acima:

Image

Usando o botão [Procurar], selecionamos a pasta física que contém o serviço web, neste caso a pasta [D:\data\devel\vbnet\poly\chap9\demo3]. Atribuímos a esta pasta um nome lógico (virtual): [virdemo3]. Isto significa que os documentos dentro da pasta física [D:\data\devel\vbnet\poly\chap9\demo3] ficarão acessíveis na rede através do URL [http://<machine>/virdemo3]. A caixa de diálogo acima contém outras definições que deixamos como estão. Clicamos em OK. A nova pasta virtual aparece na lista de pastas virtuais no IIS:

Image

Agora, abrimos um navegador e acedemos à URL [http://localhost/virdemo3/demo3.asmx]. Obtemos o mesmo resultado que antes:

Image

10.3.5. Conclusão

Demonstrámos várias formas de criar um serviço web. Daqui em diante, utilizaremos o método da versão 3 para criar o serviço e o método 4 para a sua implementação. Desta forma, não precisaremos do VS.NET. No entanto, vale a pena referir as vantagens de utilizar o VS.NET pela assistência à depuração que proporciona. Existem ferramentas gratuitas disponíveis para o desenvolvimento de aplicações web, nomeadamente o produto WebMatrix patrocinado pela Microsoft, que pode ser encontrado na URL [http://www.asp.net/webmatrix]. É uma excelente ferramenta para dar os primeiros passos na programação web sem qualquer investimento inicial.

10.4. Um serviço web para operações

Considere um serviço web que oferece cinco funções:

  1. add(a,b), que retorna a+b
  2. subtract(a,b), que retorna a-b
  3. multiply(a,b), que retorna a*b
  4. divide(a,b), que retorna a/b
  5. doAll(a,b), que retorna a matriz [a+b, a-b, a*b, a/b]

O código VB.NET para este serviço é o seguinte:


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

Estamos a repetir aqui algumas explicações que já foram dadas, mas que vale a pena revisitar ou aprofundar. A classe de operações assemelha-se a uma classe VB.NET, com alguns pontos a ter em conta:

  • os métodos são precedidos por um atributo <WebMethod()> que indica ao compilador quais métodos devem ser «publicados», ou seja, disponibilizados ao cliente. Um método não precedido por este atributo seria invisível para clientes remotos. Este poderia ser um método interno utilizado por outros métodos, mas não destinado à publicação.
  • A classe deriva da classe WebService definida no namespace System.Web.Services. Esta herança nem sempre é obrigatória. Neste exemplo, em particular, poderíamos dispensá-la.
  • A própria classe é precedida por um atributo <WebService(Namespace="st.istia.univ-angers.fr")> destinado a fornecer um namespace para o serviço web. Um fornecedor de classes atribui um namespace às suas classes para lhes dar um nome único e, assim, evitar conflitos com classes de outros fornecedores que possam ter o mesmo nome. O mesmo se aplica aos serviços web. Cada serviço web deve ser identificável por um nome único, neste caso st.istia.univ-angers.fr.
  • Não definimos um construtor. Por conseguinte, o construtor da classe pai será utilizado implicitamente.

O código-fonte acima não se destina diretamente ao compilador VB.NET, mas ao servidor web IIS. Deve ter a extensão .asmx e ser guardado na estrutura de diretórios do servidor web. Aqui, guardamo-lo como operations.asmx na pasta <IISroot>\polyvbnet\operations:

Image

Associamos o diretório virtual do IIS [operations] a este diretório físico:

Vamos aceder ao serviço utilizando um navegador. O URL a solicitar é [http://localhost/operations/operations.asmx]:

Image

Recebemos um documento web com um link para cada um dos métodos definidos no serviço web de operações. Vamos seguir o link «Adicionar»:

Image

A página que aparece convida-nos a testar o método add, fornecendo os dois argumentos a e b que este requer. Recordemos a definição do método *add*:

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

Note que a página utilizou os nomes dos argumentos a e b da definição do método. Clique no botão Call e a seguinte resposta aparece numa janela separada do navegador:

Image

Se selecionar [Exibir/Fonte] acima, obtém o seguinte código:

Image

Vamos repetir o processo para o método [toutfaire]:

Image

Obtemos a seguinte página:

Image

Vamos utilizar o botão [Ligar] acima:

Image

Em todos os casos, a resposta do servidor tem o seguinte formato:

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • a resposta está no formato XML
  • A linha 1 é padrão e está sempre presente na resposta
  • As linhas seguintes dependem do tipo de resultado (double, ArrayOfDouble), do número de resultados e do namespace do serviço web (st.istia.univ-angers.fr, neste caso).

Existem vários métodos para consultar um serviço web e obter a sua resposta. Voltemos à URL do serviço:

Image

e seguir o link [Add]. Na página que aparece, são apresentados dois métodos para consultar a função [Add] do serviço web:

Image

Image

Image

Estes dois métodos para aceder às funções de um serviço web chamam-se, respetivamente: HTTP-POST e SOAP. Vamos agora analisá-los um a um.

Nota: Nas primeiras versões do VS.NET, existia um terceiro método chamado HTTP-GET. À data deste documento (março de 2004), este método parece já não estar disponível. Isto significa que o serviço web gerado pelo VS.NET não aceita pedidos GET. Isto não significa que não seja possível escrever serviços web que aceitem pedidos GET, nomeadamente utilizando outras ferramentas que não o VS.NET ou simplesmente manualmente.

10.5. Um cliente HTTP-POST

Seguiremos o método proposto pelo serviço web:

Image

Vamos analisar o que está escrito. Primeiro, o cliente web deve enviar os seguintes cabeçalhos HTTP:

POST /operations/operations.asmx/add HTTP/1.1
O cliente web faz uma solicitação POST para a URL /operations/operations.asmx/add de acordo com o protocolo HTTP versão 1.1
HOST: localhost
Especificamos a máquina de destino para a solicitação. Aqui, localhost. Este cabeçalho tornou-se obrigatório a partir da versão 1.1 do protocolo HTTP
Content-Type: application/x-www-form-urlencoded
Isto especifica que, após os cabeçalhos HTTP, serão enviados parâmetros adicionais no formato urlencoded. Este formato substitui determinados caracteres pelos seus códigos hexadecimais.
Content-Length: 7
Esta é a contagem de caracteres da cadeia de parâmetros que será enviada após os cabeçalhos HTTP.

Os cabeçalhos HTTP são seguidos por uma linha em branco e, em seguida, pela cadeia de caracteres do parâmetro POST com [Content-Length] caracteres no formato a=XX&b=YY, em que XX e YY são as cadeias «codificadas por URL» dos valores dos parâmetros a e b. Sabemos o suficiente para reproduzir o acima exposto com o nosso cliente TCP genérico já utilizado no capítulo sobre programação TCP/IP:

  • Iniciamos o IIS
  • o serviço está disponível na URL [http://localhost/operations/operations.asmx]
  • utilizamos o cliente TCP genérico numa janela do 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]

Primeiro, note que adicionámos o cabeçalho [Connection: close] para instruir o servidor a fechar a ligação após enviar a resposta. Isto é necessário neste caso. Se não especificarmos isto, por predefinição, o servidor manterá a ligação aberta. No entanto, a sua resposta é uma sequência de linhas de texto, a última das quais não é terminada por um carácter de fim de linha. Acontece que o nosso cliente TCP genérico lê linhas de texto terminadas por um caractere de fim de linha utilizando o método ReadLine. Se o servidor não fechar a ligação após enviar a última linha, o cliente fica bloqueado porque está à espera de um caractere de fim de linha que nunca chega. Se o servidor fechar a ligação, o método ReadLine do cliente é concluído e o cliente não fica bloqueado.

Imediatamente após receber a linha vazia que sinaliza o fim dos cabeçalhos HTTP, o servidor IIS envia uma resposta inicial:

<-- 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 resposta, composta exclusivamente por cabeçalhos HTTP, indica ao cliente que pode enviar os 7 caracteres que disse querer enviar. O que fazemos:

a=2&b=3

Note que o nosso cliente TCP envia mais de 7 caracteres aqui, uma vez que os envia com um marcador de fim de linha (WriteLine). Isto não interfere com o servidor, que apenas irá reter os primeiros 7 caracteres dos recebidos, e porque a ligação é então encerrada (Connection: close). Estes caracteres adicionais teriam sido problemáticos se a ligação tivesse permanecido aberta, uma vez que teriam sido interpretados como provenientes do comando seguinte do cliente. Assim que os parâmetros são recebidos, o servidor envia a sua resposta:

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

Agora temos os elementos necessários para escrever um programa cliente para o nosso serviço web. Será um cliente de consola chamado httpPost2 e utilizado da seguinte forma:


dos>httpPost2 http://localhost/operations/operations.asmx
 
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
 
ajouter 6 7
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[résultat=13]
 
soustraire 8 9
--> POST /operations/operations.asmx/soustraire HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=8&b=9
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">-1</double>
[résultat=-1]
 
fin
 
dos>

O cliente é chamado passando-lhe o URL do serviço web:

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

Em seguida, o cliente lê os comandos digitados no teclado e executa-os. Estes têm o seguinte formato:

fonction a b

onde função é a função do serviço web que está a ser chamada (somar, subtrair, multiplicar, dividir) e a e b são os valores sobre os quais esta função irá operar. Por exemplo:

ajouter 6 7

A partir daí, o cliente enviará a solicitação HTTP necessária ao servidor web e receberá uma resposta. As trocas cliente-servidor são exibidas no ecrã para o ajudar a compreender melhor o processo:

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]

A troca de mensagens apresentada acima é idêntica à que vimos com o cliente TCP genérico, com uma diferença: o cabeçalho HTTP **Connection: Keep-Alive instrui o servidor a não fechar a ligação. A ligação permanece, portanto, aberta para a próxima operação do cliente, pelo que este não precisa de se reconectar ao servidor. No entanto, isto requer que o cliente utilize um método diferente de ReadLine() para ler a resposta do servidor, uma vez que sabemos que a resposta consiste numa sequência de linhas, a última das quais não é terminada por um caractere de nova linha. Assim que toda a resposta do servidor tiver sido recebida, o cliente analisa-a para encontrar o resultado da operação solicitada e exibi-lo:

[résultat=13]

Vamos examinar o código do nosso cliente:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web
 
' web operations client
Public Module clientPOST
 
    Public Sub Main(ByVal args() As String)
        ' syntax
        Const syntaxe As String = "pg URI"
        Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
 
        ' number of arguments
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' note the URI required
        Dim URIstring As String = args(0)
 
        ' connect to the server
        Dim uri As Uri = Nothing        ' the URI of the web service
        Dim client As TcpClient = Nothing        ' the client's tcp link with the server
        Dim [IN] As StreamReader = Nothing        ' the customer's reading flow
        Dim OUT As StreamWriter = Nothing        ' the customer's writing flow
        Try
            ' server connection
            uri = New Uri(URIstring)
            client = New TcpClient(uri.Host, uri.Port)
            ' create customer input/output flows TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
        Catch ex As Exception
            ' URI incorrect or other problem
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try
 
        ' creation of a dictionary of web service functions
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
 
        ' user requests are typed on the keyboard
        ' as function a b
        ' they are terminated with the command fin
        Dim commande As String = Nothing        ' keyboard command
        Dim champs As String() = Nothing        ' command line fields
        Dim fonction As String = Nothing        ' name of a web service function
        Dim a, b As String        ' web service function arguments
 
        ' invites the user
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")
 
        ' error management
        Dim erreurCommande As Boolean
        Try
            ' keyboard command input loop
            While True
                ' no error at start
                erreurCommande = False
                ' read command
                commande = Console.In.ReadLine().Trim().ToLower()
                ' finished?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' breaking down the order into fields
                champs = Regex.Split(commande, "\s+")
                Try
                    ' three fields are required
                    If champs.Length <> 3 Then
                        Throw New Exception
                    End If
                    ' field 0 must be a recognized function
                    fonction = champs(0)
                    If Not dicoFonctions.ContainsKey(fonction) Then
                        Throw New Exception
                    End If
                    ' field 1 must be a valid number
                    a = champs(1)
                    Double.Parse(a)
                    ' field 2 must be a valid number
                    b = champs(2)
                    Double.Parse(b)
                Catch
                    ' invalid order
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    erreurCommande = True
                End Try
                ' request the web service
                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
        ' end of client-server link
        Try
            [IN].Close()
            OUT.Close()
            client.Close()
        Catch
        End Try
    End Sub
...........
    ' error display
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Module

Já vimos estes elementos várias vezes anteriormente e não requerem quaisquer comentários especiais. Vamos agora examinar o código do método executeFonction, onde se encontram os novos elementos:


    ' executeFonction
    Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
        ' executes function(a,b) on the URI uri web service
        ' client-server exchanges take place via IN and OUT flows
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server
 
        ' query chain construction
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length
 
        ' construction of header table HTTP to be sent
        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) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i
 
        ' we read the 1st web server response HTTP/1.1 100
        Dim ligne As String = Nothing
        ' a line in the read stream
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' next line
            ligne = [IN].ReadLine()
        End While
        'last line echo
        Console.Out.WriteLine(("<-- " + ligne))
 
        ' send request parameters
        OUT.Write(requête)
        ' echo
        Console.Out.WriteLine(("--> " + requête))
 
        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        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
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length ?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While
        ' last line echo
        Console.Out.WriteLine("<--")
 
        ' build the regular expression to retrieve the result
        ' in the web server response stream
        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
 
        ' we read the rest of the web server response
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)
 
        ' the answer is broken down into lines of text
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
 
        ' scroll through the lines of text looking for the result
        Dim strRésultat As String = "?"        ' function result
        For i = 0 To lignes.Length - 1
            ' follow-up
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' compare current line to model
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' have we found?
            If MatchRésultat.Success Then
                ' we note the result
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' the result is displayed
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

Primeiro, o cliente HTTP-POST envia o seu pedido no formato POST:


        ' query chain construction
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length
 
        ' construction of header table HTTP to be sent
        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) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

No cabeçalho

--> Content-Length: 7

temos de especificar o tamanho dos parâmetros que serão enviados pelo cliente por trás dos cabeçalhos HTTP:

--> a=6&b=7

Para o fazer, utilize o seguinte código:


        ' query chain construction
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

O método HttpUtility.UrlEncode(string string) converte determinados caracteres na string em %n1n2, onde n1n2 é o código ASCII do caractere convertido. Os caracteres visados por esta conversão são todos aqueles que têm um significado específico numa solicitação POST (espaço, =, &, etc.). Aqui, o método HttpUtility.UrlEncode é normalmente desnecessário, uma vez que a e b são números que não contêm nenhum desses caracteres especiais. É utilizado aqui apenas como exemplo. Requer o namespace System.Web. Assim que o cliente tiver enviado os seus cabeçalhos 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
-->

O servidor responde com o cabeçalho 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
<--

O código simplesmente lê e apresenta esta primeira resposta no ecrã:


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

Depois de ler esta resposta inicial, o cliente deve enviar os seus parâmetros:

--> a=6&b=7

Faz isso com o seguinte código:


        ' envoi paramètres de la requête
        OUT.Write(requête)
        ' echo
        Console.Out.WriteLine(("--> " + requête))

O servidor enviará então a sua resposta. Esta resposta é composta por duas partes:

  1. Cabeçalhos HTTP seguidos de uma linha em branco
  2. a resposta em 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>

Primeiro, o cliente lê os cabeçalhos HTTP para encontrar a linha Content-Length e recuperar o tamanho da resposta XML (aqui, 90). Isto é recuperado utilizando uma expressão regular. Poderíamos ter feito isto de forma diferente e provavelmente mais eficiente.


        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        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
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length ?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While
        ' last line echo
        Console.Out.WriteLine("<--")

Assim que tivermos o comprimento N da resposta XML, basta ler N caracteres do fluxo IN da resposta do servidor. Esta cadeia de N caracteres é dividida em linhas de texto para efeitos de monitorização no ecrã. Entre estas linhas, procuramos a linha que contém o resultado:

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

utilizando novamente uma expressão regular. Assim que o resultado é encontrado, é apresentado.

[résultat=13]

O final do código do cliente é o seguinte:


        ' build the regular expression to retrieve the result
        ' in the web server response stream
        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
 
        ' we read the rest of the web server response
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)
 
        ' the answer is broken down into lines of text
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
 
        ' scroll through the lines of text looking for the result
        Dim strRésultat As String = "?"        ' function result
        For i = 0 To lignes.Length - 1
            ' follow-up
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' compare current line to model
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' have we found?
            If MatchRésultat.Success Then
                ' we note the result
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' the result is displayed
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.6. Um cliente SOAP

Aqui, analisamos um segundo cliente que utilizará um diálogo cliente-servidor SOAP (Simple Object Access Protocol). Apresenta-se um exemplo desse diálogo para a função add:

Image

Image

O pedido do cliente é um pedido POST. Veremos, portanto, alguns dos mesmos mecanismos que no cliente anterior. A principal diferença é que, enquanto o cliente HTTP-POST enviou os parâmetros a e b no formulário

    a=A&b=B

o cliente SOAP os envia num formato XML mais complexo:

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>

Recebe em troca uma resposta XML que é também mais complexa do que as respostas 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>

Embora o pedido e a resposta sejam mais complexos, trata-se, de facto, do mesmo mecanismo HTTP que o do cliente HTTP-POST. O código do cliente SOAP pode, portanto, ser modelado a partir do código do cliente HTTP-POST. Aqui está um exemplo de execução:

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]

Apenas o método executeFonction é alterado. O cliente SOAP envia os cabeçalhos HTTP para o seu pedido. Estes são simplesmente um pouco mais complexos do que os do 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"
-->

O código que os gera:


    ' executeFonction
    Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
        ' executes function(a,b) on the URI uri web service
        ' client-server exchanges take place via IN and OUT flows
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server
        ' construction of query string 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
 
        ' construction of header table HTTP to be sent
        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) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

Ao receber este pedido, o servidor envia a sua primeira resposta, que o cliente apresenta:

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

O código para ler esta primeira resposta é o seguinte:


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

O cliente irá agora enviar os seus parâmetros em formato XML no que se denomina um envelope 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>

O código:


        ' envoi paramètres de la requête
        OUT.Write(requêteSOAP)
        ' echo
        Console.Out.WriteLine(("--> " + requêteSOAP))

O servidor enviará então a sua resposta final:

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

O cliente exibe os cabeçalhos HTTP recebidos no ecrã enquanto procura a linha Content-Length:


        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        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
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length ?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While        'while
        ' last line echo
        Console.Out.WriteLine("<--")

Assim que o tamanho N da resposta XML for conhecido, o cliente lê N caracteres do fluxo de resposta do servidor, divide a cadeia de caracteres recuperada em linhas de texto para as apresentar no ecrã e procura a tag XML do resultado: <ajouterResult>7</ajouterResult> e apresenta-a:


        ' build the regular expression to retrieve the result
        ' in the web server response stream
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing
 
        ' we read the rest of the web server response
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)
 
        ' the answer is broken down into lines of text
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
 
        ' scroll through the lines of text looking for the result
        Dim strRésultat As String = "?"        ' function result
        For i = 0 To lignes.Length - 1
            ' follow-up
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' compare current line to model
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' have we found?
            If MatchRésultat.Success Then
                ' we note the result
                strRésultat = MatchRésultat.Groups(1).Value
            End If
            'next line
        Next i
        ' the result is displayed
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.7. Encapsulamento da comunicação cliente-servidor

Imaginemos que as operações do nosso serviço web são utilizadas por várias aplicações. Seria útil fornecer a estas aplicações uma classe que atuasse como uma interface entre a aplicação cliente e o serviço web, ocultando a maior parte da comunicação de rede, o que não é trivial para a maioria dos programadores. Isto resultaria na seguinte arquitetura:

A aplicação cliente dirigir-se-ia à interface cliente-servidor para efetuar os seus pedidos ao serviço web. A interface trataria de toda a comunicação de rede necessária com o servidor e devolveria o resultado à aplicação cliente. A aplicação cliente deixaria de ter de lidar com a comunicação com o servidor, o que simplificaria consideravelmente o seu desenvolvimento.

10.7.1. A Classe de Encapsulamento

Com base no que abordámos nas secções anteriores, temos agora uma boa compreensão da comunicação de rede entre o cliente e o servidor. Analisámos até três métodos. Optámos por encapsular o método SOAP. A classe é a seguinte:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic
 
' clientSOAP of the Web operations service
Public Class clientSOAP
 
    ' instance variables
    Private uri As uri = Nothing    ' the URI of the web service
    Private client As TcpClient = Nothing    ' the client's tcp link with the server
    Private [IN] As StreamReader = Nothing    ' the customer's reading flow
    Private OUT As StreamWriter = Nothing    ' the customer's writing flow
    ' function dictionary
    Private dicoFonctions As New Hashtable
    ' function list
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' verbose
    Private verbose As Boolean = False    ' to true, displays client-server exchanges on screen
 
    ' manufacturer
    Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)
 
        ' we note verbose
        Me.verbose = verbose
 
        ' server connection
        uri = New Uri(uriString)
        client = New TcpClient(uri.Host, uri.Port)
 
        ' create customer input/output flows TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True
 
        ' create a dictionary of web service functions
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
    End Sub
 
    ' close server connection
    Public Sub Close()
        ' end of client-server link
        [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
        ' executes function(a,b) on the URI uri web service
        ' client-server exchanges take place via IN and OUT flows
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server
 
        ' valid function?
        fonction = fonction.Trim().ToLower()
        If Not dicoFonctions.ContainsKey(fonction) Then
            Return "[fonction [" + fonction + "] indisponible : (ajouter, soustraire,multiplier,diviser)]"
        End If
 
        ' valid arguments a and 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
 
        ' division by zero?
        If fonction = "diviser" And doubleB = 0 Then
            Return "[division par zéro]"
        End If
 
        ' construction of query string 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
 
        ' construction of header table HTTP to be sent
        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) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            If verbose Then
                Console.Out.WriteLine(("--> " + entetes(i)))
            End If
        Next i
 
        ' we read the 1st web server response HTTP/1.1 100
        Dim ligne As String = Nothing
        ' a line in the read stream
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While
        'last line echo
        If verbose Then
            Console.Out.WriteLine(("<-- " + ligne))
        End If
        ' send request parameters
        OUT.Write(requêteSOAP)
        ' echo
        If verbose Then
            Console.Out.WriteLine(("--> " + requêteSOAP))
        End If
 
        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        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
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            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
            ' next line
            ligne = [IN].ReadLine()
        End While
 
        ' last line echo
        If verbose Then
            Console.Out.WriteLine("<--")
        End If
 
        ' build the regular expression to retrieve the result
        ' in the web server response stream
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing
 
        ' we read the rest of the web server response
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)
 
        ' the answer is broken down into lines of text
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
 
        ' scroll through the lines of text looking for the result
        Dim strRésultat As String = "?"        ' function result
        For i = 0 To lignes.Length - 1
            ' follow-up
            If verbose Then
                Console.Out.WriteLine(("<-- " + lignes(i)))
            End If            ' compare current line to model
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' have we found?
            If MatchRésultat.Success Then
                ' we note the result
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
 
        ' return the result
        Return strRésultat
    End Function
End Class

Não há nada de novo aqui em comparação com o que já vimos. Simplesmente pegámos no código do cliente SOAP que estudámos e reorganizámo-lo ligeiramente para o transformar numa classe. Esta classe tem um construtor e dois métodos:


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

    ' close server connection
    Public Sub Close()

e possui os seguintes atributos:


    ' variables d'instance
    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
    ' dictionnaire des fonctions
    Private dicoFonctions As New Hashtable
    ' liste des fonctions
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' verbose
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

Passamos dois parâmetros ao construtor:

  1. o URI do serviço web ao qual deve ligar-se
  2. um parâmetro booleano «verbose» que, quando verdadeiro, solicita que as trocas de rede sejam exibidas no ecrã; caso contrário, não serão exibidas.

Durante a construção, são criados o fluxo IN para leitura de rede, o fluxo OUT para escrita de rede e o dicionário de funções geridas pelo serviço. Assim que o objeto é construído, a ligação cliente-servidor é aberta e os seus fluxos IN e OUT estão prontos a ser utilizados.

O método Close encerra a ligação ao servidor.

O método ExecuteFonction é aquele que escrevemos para o cliente SOAP que estudámos, com algumas pequenas diferenças:

  1. Os parâmetros `uri`, `IN` e `OUT`, que anteriormente eram passados como parâmetros para o método, já não precisam de ser passados, uma vez que são agora atributos de instância acessíveis a todos os métodos da instância
  2. O método ExecuteFonction, que anteriormente retornava um tipo void e exibia o resultado da função no ecrã, agora retorna esse resultado — e, portanto, um tipo string.

Normalmente, um cliente utilizará a classe clientSOAP da seguinte forma:

  1. criando um objeto clientSOAP que estabelecerá a ligação ao serviço web
  2. chamando repetidamente o método executeFonction
  3. fechar a ligação ao serviço web utilizando o método Close.

Vamos analisar um primeiro cliente.

10.7.2. Um cliente de consola

Aqui, revisamos o cliente SOAP que estudámos quando a classe clientSOAP ainda não existia e redesenhamo-lo para que agora utilize esta classe:


' namespaces
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic
 
Public Module testClientSoap
 
    ' requests the URI of the operations web service
    ' interactively executes keyboard commands
    Public Sub Main(ByVal args() As String)
        ' syntax
        Const syntaxe As String = "pg URI [verbose]"
 
        ' number of arguments
        If args.Length <> 1 And args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' verbose?
        Dim verbose As Boolean = False
        If args.Length = 2 Then
            verbose = args(1).ToLower() = "verbose"
        End If
        ' connect to the web service 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' connection error
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try
 
        ' user requests are typed on the keyboard
        ' in the form function a b - they end with the command fin
        Dim commande As String = Nothing        ' keyboard command
        Dim champs As String() = Nothing        ' command line fields
 
        ' invites the user
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b" + ControlChars.Lf)
 
        ' error management
        Dim erreurCommande As Boolean
        Try
            ' keyboard command input loop
            While True
                ' initially no error
                erreurCommande = False
                ' read command
                commande = Console.In.ReadLine().Trim().ToLower()
                ' finished?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' breaking down the order into fields
                champs = Regex.Split(commande, "\s+")
                ' three fields are required
                If champs.Length <> 3 Then
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    ' we note the error
                    erreurCommande = True
                End If
                ' make a request to the web service
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
                ' following request
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' end of client-server link
        Try
            client.Close()
        Catch
        End Try
    End Sub
 
    ' error display
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Module

O cliente está agora consideravelmente mais simples e não contém comunicação de rede. O cliente aceita dois parâmetros:

  1. o URI das operações do serviço web
  2. a palavra-chave opcional «verbose». Se estiver presente, as trocas de dados de rede serão exibidas no ecrã.

Estes dois parâmetros são utilizados para construir um objeto clientSOAP que irá gerir a comunicação com o serviço web.


        ' connect to the web service 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' connection error
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

Assim que a ligação ao serviço web for estabelecida, o cliente pode enviar os seus pedidos. Estes são digitados no teclado, analisados e, em seguida, enviados para o servidor através da chamada ao método executeFonction do objeto clientSOAP.


                ' on fait la demande au service web
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))

A classe clientSOAP é compilada num «assembly»:

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

A aplicação cliente testClientSoap é então compilada utilizando:

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

Aqui está um exemplo de uma execução não detalhada:

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

Pode monitorizar o tráfego de rede solicitando uma execução «verbosa»:

dos>testClientSOAP http://localhost/operations/operations.asmx verbose
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

Agora vamos criar um cliente gráfico.

10.7.3. Um cliente gráfico para Windows

Vamos agora consultar o nosso serviço web utilizando um cliente gráfico que também utilizará a classe clientSOAP. A interface gráfica será a seguinte:

Os controlos são os seguintes:

N.º
Tipo
nome
função
1
Caixa de Texto
txtURI
o URI das operações do serviço web
2
Botão
btnOpen
abre a ligação ao serviço web
3
Botão
btnClose
fecha a ligação ao serviço web
4
ComboBox
cmbFunctions
a lista de funções (soma, subtração, multiplicação, divisão)
5
TextBox
txtA
o argumento das funções
6
Caixa de Texto
txtB
argumento b das funções
7
Caixa de Texto
txtResult
o resultado da função(a,b)
8
Botão
btnCalculate
inicia o cálculo da função(a,b)
9
Caixa de Texto
txtError
exibe uma mensagem sobre o estado da ligação

Existem algumas restrições operacionais:

  • O botão btnOpen só está ativo se o campo txtURI não estiver vazio e se ainda não houver uma ligação aberta
  • O botão btnClose só fica ativo quando uma ligação ao serviço web tiver sido estabelecida
  • O botão btnCalculate só fica ativo quando existe uma ligação aberta e os campos txtA e txtB não estão vazios
  • Os campos txtResult e txtError têm o atributo ReadOnly definido como true

O cliente começa por abrir a ligação ao serviço web utilizando o botão [Open]:

Image

Em seguida, o utilizador pode selecionar uma função e valores para a e b:

Image

Image

Image

Image

Image

Segue-se o código da aplicação. Omitimos o código do formulário, que não é relevante neste contexto.


'namespaces
Imports System
Imports System.Windows.Forms
 
' the form class
Public Class FormClientSOAP
    Inherits System.Windows.Forms.Form
 
    ' instance attributes
    Dim client As clientSOAP    ' client SOAP of the web operations service
 
#Region " Code généré par le Concepteur Windows Form "
 
    Public Sub New()
        MyBase.New()
        'This call is required by the Windows Form Designer.
        InitializeComponent()
        ' other initializations
        myInit()
    End Sub
 
    'The substituted method Disposes of the form to clean up the list of components.
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
    End Sub
 
...
 
    Private Sub InitializeComponent()
....
    End Sub
 
#End Region
 
 
    Private Sub myInit()
        ' init form
        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
        ' the content of the input field has changed - set the state of the open button
        btnOuvrir.Enabled = txtURI.Text.Trim <> ""
    End Sub
 
    Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
        ' request to open a connection with the web service
        Try
            ' creation of an object of type [clientSOAP]
            client = New clientSOAP(txtURI.Text.Trim, False)
            ' button status
            btnOuvrir.Enabled = False
            btnFermer.Enabled = True
            ' the URI can no longer be modified
            txtURI.ReadOnly = True
            ' customer status
            txtErreur.Text = "Liaison au service web ouverte"
        Catch ex As Exception
            ' there has been an error - it is displayed
            txtErreur.Text = ex.Message
        End Try
    End Sub
 
    Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
        ' closing the web service connection
        client.Close()
        ' button states
        btnOuvrir.Enabled = True
        btnFermer.Enabled = False
        ' URI
        txtURI.ReadOnly = False
        ' customer status
        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
        ' calculating a function f(a,b)
        ' delete the previous result
        txtRésultat.Text = ""
        Try
            txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
        Catch ex As Exception
            ' there has been a network error
            txtErreur.Text = ex.Message
            ' we close the link
            btnFermer_Click(Nothing, Nothing)
        End Try
    End Sub
 
    Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
        ' change in the value of 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
        ' change in the value of B
        txtA_TextChanged(Nothing, Nothing)
    End Sub
 
    ' main method
    Public Shared Sub main()
        Application.Run(New FormClientSOAP)
    End Sub
End Class

Mais uma vez, a classe clientSOAP oculta todos os aspetos relacionados com a rede da aplicação. A aplicação foi construída da seguinte forma:

  • O assembly clientSOAP.dll, que contém a classe clientSOAP, foi colocado na pasta do projeto
  • A GUI clientsoapgui.vb foi criada com o VS.NET e, em seguida, compilada numa janela do 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 interface gráfica foi então iniciada por:

dos>clientsoapgui

10.8. Um cliente proxy

Vamos recapitular o que acabámos de fazer. Criámos uma classe intermédia que encapsula as trocas de rede entre um cliente e um serviço web, de acordo com o diagrama abaixo:

A plataforma .NET leva esta lógica um passo mais além. Assim que sabemos qual o serviço Web a aceder, podemos gerar automaticamente a classe que atuará como intermediária para aceder às funções do serviço Web e ocultar toda a camada de rede. Esta classe é designada por proxy do serviço Web para o qual foi gerada.

Como se gera uma classe proxy de serviço Web? Um serviço Web é sempre acompanhado por um ficheiro de descrição em formato XML. Se o URI das operações do nosso serviço Web for http://localhost/operations/operations.asmx, o seu ficheiro de descrição está disponível no URL http://localhost/operations/operations.asmx?wsdl, conforme mostrado na captura de ecrã seguinte:

Image

Este é um ficheiro XML que descreve com precisão todas as funções do serviço web, incluindo, para cada uma delas, o tipo e o número de parâmetros, bem como o tipo do resultado. Este ficheiro é designado por ficheiro WSDL do serviço, uma vez que utiliza a WSDL (Web Services Description Language). A partir deste ficheiro, é possível gerar uma classe proxy utilizando a ferramenta 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

A ferramenta wsdl gera um ficheiro fonte VB.NET (opção /language=vb) com o nome da classe que implementa o serviço web, neste caso operations. Vamos examinar parte do código gerado:

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

Option Strict Off
Option Explicit On

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

'
'This source code has been té automatically généré by wsdl, Version=1.1.4322.573.
'

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

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

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

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

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

    '<remarks/>
    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 pode parecer um pouco complexo à primeira vista. Não precisamos de compreender os detalhes para o podermos utilizar. Vamos primeiro examinar a declaração da classe:

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

A classe tem o nome das operações do serviço Web para o qual foi criada. Deriva da classe SoapHttpClientProtocol:

Image

A nossa classe proxy tem um único construtor:

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

O construtor atribui a URL do serviço web associado ao proxy ao atributo url. A classe de operações acima não define o próprio atributo url. Este é herdado da classe da qual o proxy deriva: System.Web.Services.Protocols.SoapHttpClientProtocol. Vamos agora examinar o que se refere ao método add:

    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

Podemos ver que tem a mesma assinatura que no serviço Web de operações, onde foi definido da seguinte forma:

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

A forma como esta classe comunica com o serviço web não é mostrada aqui. Esta comunicação é inteiramente gerida pela classe pai System.Web.Services.Protocols.SoapHttpClientProtocol. O proxy contém apenas o que o distingue de outros proxies:

  • o URL do serviço web associado
  • a definição dos métodos do serviço associado.

Para utilizar os métodos do serviço web de operações, um cliente necessita apenas da classe proxy de operações gerada anteriormente. Vamos compilar esta classe num ficheiro de assembly:

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

Agora vamos escrever um cliente de consola. É chamado sem parâmetros e executa os pedidos digitados no 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

O código do cliente é o seguinte:


' namespaces
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
 
Public Module testClientProxy
 
    ' interactively executes keyboard commands
    ' and sends them to the web operations service
    Public Sub Main()
        ' there are no more arguments - the web service's URL is hard-coded in the proxy
 
        ' creation of a dictionary of web service functions
        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
 
        ' create a proxy operations object 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' connection error
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try
 
        ' user requests are typed on the keyboard
        ' in the form function a b - they end with the command fin
        Dim commande As String = Nothing        ' keyboard command
        Dim champs As String() = Nothing        ' command line fields
 
        ' invites the user
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)
 
        ' some local data
        Dim erreurCommande As Boolean
        Dim fonction As String
        Dim a, b As Double
        ' keyboard command input loop
        While True
            ' initially no error
            erreurCommande = False
            ' read command
            commande = Console.In.ReadLine().Trim().ToLower()
            ' finished?
            If commande Is Nothing Or commande = "fin" Then
                Exit While
            End If
            ' breaking down the order into fields
            champs = Regex.Split(commande, "\s+")
            Try
                ' three fields are required
                If champs.Length <> 3 Then
                    Throw New Exception
                End If
                ' field 0 must be a recognized function
                fonction = champs(0)
                If Not dicoFonctions.ContainsKey(fonction) Then
                    Throw New Exception
                End If
                ' field 1 must be a valid number
                a = Double.Parse(champs(1))
                ' field 2 must be a valid number
                b = Double.Parse(champs(2))
            Catch
                ' invalid order
                Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                erreurCommande = True
            End Try
            ' make a request to the web service
            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
 
    ' error display
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Module

Estamos apenas a analisar o código específico para a utilização da classe proxy. Primeiro, é criado um objeto de operações proxy:


        ' create a proxy operations object 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' connection error
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

As linhas a e b são digitadas no teclado. Com base nesta informação, são chamados os métodos de proxy apropriados:


            ' make a request to the web service
            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

Aqui, estamos a lidar pela primeira vez com a operação «tudo-em-um» que executa as quatro operações. Esta tinha sido ignorada até agora porque devolve uma matriz de números encapsulada num invólucro XML, que é mais complicado de manusear do que as respostas XML simples das outras funções, que devolvem apenas um único resultado. Aqui, com a classe proxy, vemos que utilizar o método «tudo-em-um» não é mais complicado do que utilizar os outros métodos. A aplicação foi compilada numa janela do DOS da seguinte forma:

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 um serviço Web

Um serviço Web pode necessitar de informações de configuração para ser inicializado corretamente. Com o IIS, estas informações podem ser colocadas num ficheiro chamado web.config, localizado na mesma pasta que o serviço Web. Suponhamos que queremos criar um serviço Web que requer duas informações para ser inicializado: um nome e uma idade. Estas duas informações podem ser colocadas no ficheiro web.config no seguinte formato:

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

As definições de inicialização são colocadas num contentor XML:

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

Um parâmetro de inicialização denominado P com o valor V será declarado com a linha:


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

Como é que o serviço web recupera esta informação? Quando o IIS carrega um serviço web, verifica se existe um ficheiro web.config na mesma pasta. Se existir, lê-o. O valor V de um parâmetro P é obtido utilizando a instrução:

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

onde ConfigurationSettings é uma classe no namespace System.Configuration.

Vamos testar esta técnica no seguinte serviço 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
 
   ' attributes
   Private nom As String
   Private age As Integer
 
   ' manufacturer
   Public Sub New()
      ' init attributes
      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 

O serviço web Person tem dois atributos, nome e idade, que são inicializados no seu construtor sem parâmetros utilizando valores lidos do ficheiro de configuração web.config do serviço Person. Este ficheiro é o seguinte:


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

O serviço web também possui um <WebMethod> sem parâmetros que simplesmente devolve os atributos nome e idade. O serviço está registado no ficheiro fonte personne.asmx, que se encontra, juntamente com o seu ficheiro de configuração, na pasta c:\inetpub\wwwroot\st\personne:

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

Vamos associar uma pasta virtual do IIS chamada /config à pasta física acima. Inicie o IIS e, em seguida, utilize um navegador para aceder ao URL http://localhost/config/personne.asmx para o serviço person:

Image

Siga o link para o método de identificação única:

Image

O método id não tem parâmetros. Vamos usar o botão Call:

Image

Recuperámos com sucesso as informações armazenadas no ficheiro web.config do serviço.

10.10. O serviço Web de cálculo de impostos

Vamos voltar à aplicação IMPOTS, que já nos é familiar. Da última vez que trabalhámos com ela, transformámo-la num servidor remoto que podia ser acedido através da Internet. Agora, vamos transformá-la num serviço Web.

10.10.1. O serviço Web

Começaremos com a classe impôt criada no capítulo sobre bases de dados, que é construída a partir de informações contidas numa base de dados ODBC:


' options
Option Strict On
Option Explicit On 
 
' namespaces
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
 
Public Class impôt
    ' data required for tax calculation
    ' come from an external source
    Private limites(), coeffR(), coeffN() As Decimal
 
    ' manufacturer
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' we check that the 3 tablaeux are the same size
        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
        ' it's good
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub
 
    ' builder 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception
        Dim connectString As String = "DSN=" + DSNimpots + ";"        ' base connection chain
        Dim impotsConn As OdbcConnection = Nothing        ' the connection
        Dim sqlCommand As OdbcCommand = Nothing        ' the SQL command
        ' the SELECT query
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' tables to retrieve data
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList
 
        ' attempt to access the database
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' create a command object
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' execute the query
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' Using the recovered table
        While myReader.Read()
            ' the data of the current line are put in the tables
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' freeing up resources
        myReader.Close()
        impotsConn.Close()
 
        ' dynamic tables are placed in static tables
        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
 
    ' tAX CALCULATION
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' calculating the number of shares
        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
        ' calculation of taxable income & family quota
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' tAX CALCULATION
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' return result
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

No serviço web, apenas pode ser utilizado um construtor sem parâmetros. Por conseguinte, o construtor da classe ficará da seguinte forma:


    ' manufacturer
    Public Sub New()
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception
 
        ' retrieve the service configuration parameters
        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")
 
        ' database operation
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' base connection chain

Os cinco parâmetros do construtor da classe anterior são agora lidos a partir do ficheiro web.config do serviço. O código no ficheiro fonte impots.asmx é o seguinte. Inclui a maior parte do código anterior. Simplesmente envolvemos as partes do código específicas do serviço web:


<%@ WebService language="VB" class=impots %>
 
' creation of a tax web service
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Imports System.Configuration
Imports System.Web.Services
 
<WebService([Namespace]:="st.istia.univ-angers.fr")> _
Public Class impôt
    Inherits WebService
 
    ' data required for tax calculation
    ' come from an external source
    Private limites(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""
 
 
    ' manufacturer
    Public Sub New()
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception
 
        ' retrieve the service configuration parameters
        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")
 
        ' database operation
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' base connection chain
        Dim impotsConn As OdbcConnection = Nothing     ' the connection
        Dim sqlCommand As OdbcCommand = Nothing     ' the SQL command
        Dim myReader As OdbcDataReader     ' odbc data reader
 
        ' the SELECT query
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
 
        ' tables to retrieve data
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList
 
        ' attempt to access the database
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' create a command object
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' execute the query
            myReader = sqlCommand.ExecuteReader()
            ' Using the recovered table
            While myReader.Read()
                ' the data of the current line are put in the tables
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' freeing up resources
            myReader.Close()
            impotsConn.Close()
 
            ' dynamic tables are placed in static tables
            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
            ' it's good
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' error
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub
 
    ' tAX CALCULATION
    <WebMethod()> _
    Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' calculating the number of shares
        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
        ' calculation of taxable income & family quota
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' tAX CALCULATION
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' return result
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
 
    ' id
    <WebMethod()> _
    Function id() As String
        ' to see if everything is OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

Vamos explicar as poucas alterações feitas na classe impots, para além das necessárias para a transformar num serviço web:

  • A leitura da base de dados no construtor pode falhar. Por isso, adicionámos dois atributos à nossa classe e um método:
    • o booleano OK é verdadeiro se o banco de dados puder ser lido, falso caso contrário
    • A string `errMessage` contém uma mensagem de erro caso a base de dados não tenha sido lida.
    • O método id, sem parâmetros, recupera os valores destes dois atributos.
  • Para lidar com quaisquer erros potenciais de acesso à base de dados, a parte do código do construtor relacionada com este acesso foi colocada num bloco try-catch.

O ficheiro web.config para a configuração do serviço é o seguinte:


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

Ao tentar carregar o serviço impots pela primeira vez, o compilador indicou que não conseguiu encontrar o namespace Microsoft.Data.Odbc utilizado na diretiva:

Imports Microsoft.Data.Odbc

Após consultar a documentação

  • foi adicionada uma diretiva de compilação ao ficheiro web.config para especificar que o assembly Microsoft.Data.odbc deve ser utilizado
  • foi colocada uma cópia do ficheiro microsoft.data.odbc.dll na pasta bin do projeto. Esta pasta é sistematicamente pesquisada pelo compilador do serviço web quando procura um «assembly».

Outras soluções parecem possíveis, mas não foram exploradas aqui. O ficheiro de configuração ficou, portanto, assim:


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

O conteúdo da pasta impots\bin:

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

O serviço e o seu ficheiro de configuração foram colocados em impots:

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

A pasta física do serviço web foi mapeada para a pasta virtual /impots no IIS. A página do serviço fica então da seguinte forma:

Image

Se seguir o link id:

Image

Se utilizar o botão «Ligar»:

Image

O resultado anterior apresenta os valores dos atributos OK (true) e errMessage (""). Neste exemplo, a base de dados foi carregada com sucesso. Nem sempre foi assim, razão pela qual adicionámos o método id para aceder à mensagem de erro. O erro consistiu no facto de o nome DSN da base de dados ter sido definido como um DSN de utilizador, quando deveria ter sido definido como um DSN de sistema. Esta distinção é feita no Gestor de Origens ODBC de 32 bits:

Vamos voltar à página de serviços:

Image

Vamos clicar no link «Calcular»:

Image

Definimos os parâmetros da chamada e executamos a chamada:

Image

O resultado está correto.

10.10.2. Gerar o proxy para o serviço impots

Agora que temos um serviço Web impots em funcionamento, podemos gerar a sua classe proxy. Lembre-se de que esta será utilizada pelas aplicações cliente para aceder ao serviço Web impots de forma transparente. Primeiro, utilizamos o utilitário wsdl para gerar o ficheiro de código-fonte da classe proxy, que é depois compilado numa DLL.

dos>wsdl /language=vb http://localhost/impots/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. Utilização do proxy com um cliente

No capítulo sobre bases de dados, criámos uma aplicação de consola para calcular impostos:

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

O programa testimpots utilizou então a classe fiscal padrão contida no ficheiro impots.dll. O código do programa testimpots.vb era o seguinte:


Option Explicit On 
Option Strict On
 
' namespaces
Imports System
Imports Microsoft.VisualBasic
 
' test pg
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' interactive tax calculator
        ' the user enters three data points on the keyboard: married nbEnfants salary
        ' the program then displays the tax payable
        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"
 
        ' checking program parameters
        If arguments.Length <> 5 Then
            ' error msg
            Console.Error.WriteLine(syntaxe1)
            ' end
            Environment.Exit(1)
        End If        'if
        ' retrieve the arguments
        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)
 
        ' tax object creation
        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
 
        ' infinite loop
        While True
            ' initially no errors
            Dim erreur As Boolean = False
 
            ' tax calculation parameters are requested
            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()
 
            ' anything to do?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
 
            ' check the number of arguments in the input line
            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
                ' checking the validity of parameters
                ' married
                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
                ' salary
                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
                ' parameters are correct - tax is calculated
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

Vamos utilizar o mesmo programa para agora fazer com que ele utilize o serviço web impots através da classe proxy impots criada anteriormente. Temos de modificar ligeiramente o código:

  • enquanto a classe tax original tinha um construtor com cinco argumentos, a classe proxy tax tem um construtor sem parâmetros. Como vimos, os cinco parâmetros estão agora definidos no ficheiro de configuração do serviço web.
  • Por conseguinte, já não há necessidade de passar estes cinco parâmetros como argumentos para o programa de teste

O novo código é o seguinte:


Imports System
Imports Microsoft.VisualBasic
 
' test pg
Module testimpots
 
    Public Sub Main(ByVal arguments() As String)
        ' interactive tax calculator
        ' the user enters three data points on the keyboard: married nbEnfants salary
        ' the program then displays the tax payable
        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"
 
        ' tax object creation
        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
 
        ' infinite loop
        Dim erreur As Boolean
        While True
            ' initially no error
            erreur = False
            ' tax calculation parameters are requested
            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()
            ' anything to do?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' check the number of arguments in the input line
            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
                ' checking parameter validity
                ' married
                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
                ' salary
                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
                ' if the parameters are correct - the tax is calculated
                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

Temos o proxy impots.dll e o código-fonte do testimpots na mesma pasta.

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 o ficheiro fonte 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

e, em seguida, execute-o:

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

Obtemos o resultado esperado.