Desenvolvimento - Delphi

Desvendando Web Services (Soap/XML)

Este artigo tem por objetivo dar uma visão geral e completa sobre uma das melhores novidades do Delphi 6, o suporte a Web Services.

por Fernando Vasconcelos



Visão geral

Uma nova geração de tecnologia de desenvolvimento nos é apresentada com o Web Services. Com ela podemos criar aplicações modulares e independentes que são distribuídas facilmente em qualquer estrutura de redes TCP/IP, pois esse foi um dos princípios fundamentais de sua implementação.

Um grande ponto positivo desta tecnologia é que a criação de servidores e clientes independem da linguagem de programação e do sistema operacional que são implementados. Atualmente o suporte da Borland se restringe ao Windows, mas já foi anunciado para um futuro próximo o suporte no Kylix, o que abriria um grande leque de possibilidades no que diz respeito à implementação de aplicações distribuídas multi-plataformas.

Os servidores podem descrever seus próprios serviços através da WSDL (Web Service Definition Language). Dessa forma, os clientes podem facilmente obter informações sobre os servidores que usarão, tais como: estrutura, métodos e parâmetros exigidos. Isso se torna essencialmente útil quando se está codificando servidores que serão usados por terceiros ou implementando clientes que usam serviços de outras empresas.

No caso particular do Delphi podemos usar um wizard que importa essas informações e cria automaticamente as units com as definições dos serviços ofertados pelo servidor.

SOAP/XML

A comunicação entre clientes e servidores é feita através do SOAP (Simple Object Access Protocol). Esse protocolo é definido em XML, sendo assim, as chamadas a procedures remotas (RPC) são codificadas em XML. Para transporte das mensagens é usado o HTTP, que além de tornar o SOAP um protocolo leve, elimina inúmeros problemas de outras tecnologias com proxys, como CORBA, DCOM e etc...

Como foi citado acima, não temos a preocupação de contornar o esquema de segurança para realizar a comunicação entre clientes e servidores. Como o SOAP usa o HTTP como camada de transporte ele opera na porta 80, que na extrema maioria dos casos está liberada pelo proxy/firewall. Além do mais, conceitualmente não existe diferença entre uma requisição a um método e a uma página HTML.

Dentre as vantagens dessa tecnologia, ainda podemos ressaltar a não necessidade de instalação de software adicional para o suporte à tecnologia como acontece com o CORBA e com o DCOM (no windows95).

Mais informações sobre SOAP podem ser encontradas no site da especificação oficial em http://www.w3.org/TR/SOAP/%3c/a%3e.%3c

Implementando servidores com suporte a Web Services

Daremos início , agora, à parte prática deste artigo, então para começar devemos criar um novo projeto de Web Services. No meu caso escolhi CGI stand-alone como tipo da aplicação e ela rodará diretamente no IIS5, mas estejam livres para escolherem o que mais lhe convierem. Feito isso seremos apresentados a seguinte tela:

WebModule1

Figura 1: WebModule1

Ao examinarmos o projeto gerado, percebemos facilmente que se trata de uma Web Server Application. No Web Module criado, encontramos os componentes fundamentais para implementação do servidor, são eles:

THTTPSoapDispatcher: Este componente atua como despachante recebendo as mensagens entrantes e as encaminha para o objeto especificado em sua propriedade Dispatcher para que seja decodificada. Ele registra-se automaticamente junto ao Web Module como um auto-dispatching object, fazendo assim com que não seja necessário a criação de Actions para direcionar as requisições para o THTTPSoapDispatcher, este passa então a receber todas as solicitações automaticamente.

THTTPSoapPascalInvoker: Este componente recebe a mensagem vinda do THTTPSoapDispatcher e a interpreta cuidando que seja disparado o método correspondente a solicitação. Ele ainda codifica o retorno do método para o padrão SOAP/XML.

TWSDLHTMLPublish: Este componente é responsável por publicar todas informações registradas pelo Web Service em WSDL que faz com que qualquer pessoa possa adquiri-las para implementar o cliente desse serviço, mesmo que seja em outra ferramenta que não o Delphi.

O próximo passo na implementação de um Web Services é a definição das interfaces que serão publicadas para uso por seus clientes. Aquelas devem herdar de IInvokable.

Interfaces Invokable

Antes de passarmos a definição, vamos ver alguma coisa a respeito delas. Por que precisamos derivar nossas interfaces de IInvokable? A resposta é simples, a arquitetura trazida pelo Delphi requer que as interfaces de definição sejam compiladas com informação de run-time, e é isso que a que a IInvokable garante. A única diferença entre IUnknow e IInvokable é que esta é compilada com a diretiva {$M}, fazendo gerar RTTI para ela e todas suas descendentes.

Dessa forma, tecnicamente chegamos a conclusão de que podemos derivar nossas interfaces diretamente de IUnknow, desde que incluamos a diretiva {$M} na unit de declaração da mesma. Embora isso seja possível e funcione, não é recomendável tal conduta, porque podemos gerar problemas futuros com novas versões da arquitetura. Se numa versão posterior for definido métodos essenciais na IInvokable teremos problemas de compatibilidade ou anomalias no funcionamento do código.

Agora que vimos o conceito vamos passar para a parte prática. Vamos começar definindo uma interface para as operações aritméticas básicas:

Listagem 1: Interface para operações aritimeticas básicas

unit IMathIntf;
interface 
type
IMath = interface(IInvokable)
["{E4F918D6-429C-45D4-9D28-D3E64DDB65E3}"]
function Soma(X, Y : Double): Double; stdcall;
function Dife(X, Y : Double): Double; stdcall;
function Mult(X, Y : Double): Double; stdcall;
function Divi(X, Y : Double): Double; stdcall;
end;
implementation
uses InvokeRegistry;
initialization
InvRegistry.RegisterInterface(TypeInfo(IMath));
end.

Nessa unit encontramos uma declaração de interface normal, e esta será usada pelos clientes para solicitar serviços junto ao servidor. Uma importante observação a ser feita sobre a unit acima é o código localizado na seção initialization:

Listagem 2: Registrando a interface IMath

InvRegistry.RegisterInterface(TypeInfo(IMath));

Essa é mais uma característica da programação de Web Services. Sempre que declararmos interfaces, classes de implementação ou classes de exceções (vistas mais adiante) teremos que registrá-las. Isso é feito através de um objeto global disponibilizado pela unit InvokeRegistry, que deve ser declarada na seção uses da unit. Através desse objeto, chamamos métodos específicos para cada tipo de declaração, no caso de interface usamos o RegisterInterface. Isso é necessário para o mecanismo de vinculação, feito pelo componente THTTPSOAPPascalInvoker, entre solicitações SOAP e a chamada ao método correto.

Pronto, agora que já temos a definição do serviço que desejamos prover vamos agora implementá-la.

Implementando Interfaces Invokable (TInvokableClass)

Quando vamos codificar as interfaces de nossos Web Services devemos criar classes descendentes da TInvokable. Mas por que isso? São basicamente três os motivos que nos levam a seguir esta convenção:

  1. Embora na implementação atual não tenha nada que realmente empeça que derivemos nossa classes de outra qualquer como TObject ou TInterfacedObject, assim como no caso da interface IUnknow/IInvokable, pode ser que em futuras distribuições sejam criados métodos essenciais para o funcionamento da arquitetura na Tinvokable, o que levaria nosso código a ter sérios problemas de compatibilidade e funcionamento.
  2. Dispensa a necessidade de criar uma factory procedure, pois o invocation registry sabe como instanciar essas classes e suas descendentes, do contrário teríamos que registrá-las juntamente com uma factory procedure para assegurar seu funcionamento.
  3. Dispensa a necessidade de implementar um esquema de liberação de memória. A classe TInvokable implementa uma contagem de referências, e quando esta chega a zero ela se libera automaticamente.

Mas se depois de tudo isso ainda quisermos usar outra ancestral para nossas classes de implementação nós podemos. Vejamos abaixo o código da unit de implementação.

Listagem 3: Unit de implementação da classe TMath

unit IMathImpl;
interface
Uses InvokeRegistry, IMathIntf;
type
//TMath = class(TInterfacedObject, IMath)
TMath = class(TInvokableclass, IMath)
public
function Soma(X: Double; Y: Double): Double; stdcall;
function Dife(X: Double; Y: Double): Double; stdcall;
function Mult(X: Double; Y: Double): Double; stdcall;
function Divi(X: Double; Y: Double): Double; stdcall;
end;
implementation
{ TMath }
function TMath.Soma(X, Y: Double): Double;
begin
Result := X + Y;
end;
function TMath.Dife(X, Y: Double): Double;
begin
Result := X - Y;
end;
function TMath.Mult(X, Y: Double): Double;
begin
Result := X * Y;
end;
function TMath.Divi(X, Y: Double): Double;
begin
Result := X / Y;
end;
{// Factory Procedure
procedure CreateMath(out Obj : TObject);
begin
//Pode ser implementado aqui o conceito de singleton.
Result := TMath.Create;
end;
}
initialization
InvRegistry.RegisterInvokableClass(TMath);
//InvRegistry.RegisterInvokableClass(TMath, CreateMath);
end.

Repare nas linhas de código comentadas, visto que elas representam o básico necessário para o funcionamento da arquitetura, se optarmos por não ter a Tinvokable como ancestral de nossas classes. Ressalto que essa conduta deve ser evitada. Novamente percebemos a necessidade de um código de registro, dessa vez o da classe, que é feito como descrito abaixo.

Listagem 4: Codigo de registro da Classe IMath

InvRegistry.RegisterInvokableClass(TypeInfo(IMath));

Feito tudo isso até aqui, já podemos considerar que temos um servidor com suporte a Web Services completo e funcional. Já até poderíamos passar para a implementação de um cliente para usá-lo, mas ao invés disso vamos continuar a incrementá-lo com mais alguns conceitos interessantes antes de passarmos ao desenvolvimento do cliente.

Até agora só usamos um tipo de dado em nossa interface, o double. Embora essa escolha tenha se dado devido ao serviço escolhido para o exemplo, o trabalho com todos os outros tipos básicos(primitivos) funcionam da mesma maneira. Mas e se nós precisarmos retornar para o cliente um tipo complexo como uma classe? Isso é o que vamos ver a seguir.

Tipos complexos em Interfaces Invokable (TRemotable)

Sempre que precisarmos retornar ou receber tipos complexos tais como records, sets ou classes em nossos Web Services, devemos mapeá-los para classes descendentes de TRemotable. Na compilação destas, são incluídas informações de RTTI, usadas para converter os dados em SOAP stream.

Então, para exemplificarmos o uso desse recurso vamos definir outra interface para o nosso Web Service, que usará e retornará um tipo complexo por nós definido. Vamos analisar o código abaixo:

Listagem 5: Interface IGeometryIntf

unit IGeometryIntf;
interface
uses InvokeRegistry;
type
TLosango = class(TRemotable)
private
FX1, FX2, FY1, FY2 : Integer;
published
property X1 : Integer read FX1 write FX1;
property X2 : Integer read FX2 write FX2;
property Y1 : Integer read FY1 write FY1;
property Y2 : Integer read FY2 write FY2;
end;
TPoint = class(TRemotable)
private
FX, FY : Integer;
published
property X : Integer read FX write FX;
property Y : Integer read FY write FY;
end;
IGeometry = interface(IInvokable)
["{926590CF-4B48-4AB2-9079-24183DD8D34F}"]
function DiagonalMaior(const Los : TLosango) : Integer; stdcall;
function Losango(const Centro : TPoint; DiaU, DiaL : Integer) : TLosango; stdcall;
end;
implementation
initialization
InvRegistry.RegisterInterface(TypeInfo(IGeometry));
RemTypeRegistry.RegisterXSClass(TPoint);
RemTypeRegistry.RegisterXSClass(TLosango);

end.

Declaramos duas remotables classes TLosango e TPoint que como seus nomes sugerem, representam respectivamente um losango e um ponto nos eixos de coordenadas. Também declaramos uma interface IGeometry contendo duas funções:

  • A primeira recebe um parâmetro TLosango e calcula a diagonal maior de um losango, demonstrando a passagem de um tipo complexo como parâmetro de uma função.
  • A segunda recebe os valores do centro, diagonal maior e menor de um losango, com isso ela retorna um objeto TLosango contendo as coordenadas calculadas de acordo com os parâmetros passados.

Devemos observar na seção initialization dessa unit uma diferença entre o registro de uma interface e de uma remotable class.

Listagem 6: Seção initialization da unit IGeometryIntf

initialization
InvRegistry.RegisterInterface(TypeInfo(IGeometry));
RemTypeRegistry.RegisterXSClass(TPoint);
RemTypeRegistry.RegisterXSClass(TLosango);
end.

Como podemos observar até aqui, no caso das interfaces e invokables classes nós usamos os métodos RegisterInterface e RegisterInvokableClass do objeto InvRegistry, e no caso das remotables classes e exceções personalizadas usamos o método RegisterXSClass do objeto RemTypeRegistry, todos eles definidos na unit InvokeRegistry.

Então vamos implementar agora a definição analisada acima:

Listagem 7: Unit IGeometryImpl

unit IGeometryImpl;
interface
uses InvokeRegistry, IgeometryIntf, Math;
type
TGeometry = class(TInvokableClass, IGeometry)
public
function DiagonalMaior(const Los: TLosango): Integer; stdcall;
function Losango(const Centro: TPoint; DiaU: Integer; DiaL: Integer): TLosango; stdcall;
end;
implementation
{ TGeometry }
function TGeometry.DiagonalMaior(const Los: TLosango): Integer;
begin
Result := Max(Los.X2 - Los.X1, Los.Y2 - Los.Y1);
end;
function TGeometry.Losango(const Centro: TPoint; DiaU,
DiaL: Integer): TLosango;
begin
Result := TLosango.Create;
Result.X1 := Centro.X - (DiaU div 2);
Result.X2 := Centro.X + (DiaU div 2);
Result.Y1 := Centro.Y - (DiaL div 2);
Result.Y2 := Centro.Y + (DiaL div 2);
end;
initialization
InvRegistry.RegisterInvokableClass(TGeometry);
end.

Analisando essa última implementação, os mais atentos poderiam fazer a seguinte pergunta: E essa instância de TLosango criada no método Losango, não é preciso desalocar a memória associada a ela? Não, todas as descendentes de TRemotable são liberadas automaticamente logo depois que a codificação do pacote de retorno é concluída, portanto não precisamos nos preocupar com isso.

Bom, antes de passarmos a implementação do cliente vamos dar uma rápida olhada no manuseio de exceções.

Exceções personalizadas em Web Services

Todo programa bem implementado deve ter uma boa estrutura de exceções para poder assegurar uma robustez desejável. No caso dos Web Services isso não deve ser diferente.

Quando uma exceção ocorre no escopo da chamada de um método, o servidor automaticamente codifica as informações sobre ela num SOAP fault packet e as envia como retorno do método solicitado. Dessa forma, a aplicação cliente gera a exceção.

Se não definirmos um tratamento de exceções personalizadas, a aplicação cliente gera uma exceção comum(Exception) com a mensagem trazida no SOAP fault packet. Embora em alguns casos isso possa ser suficiente, nós temos como transmitir qualquer informação desejada de uma exceção. Para isso se tornar possível, basta que a gente defina nossas classes de exceção descendentes de ERemotableExceptio.

Dessa forma podemos enviar ao cliente o valor de todas as propriedades published definidas na classe, assim ele pode levantar uma exceção equivalente à ocorrida no servidor. E o melhor é que se o cliente e o servidor compartilharem da mesma unit que define, implementa e registra suas classes de exceção, automaticamente o cliente levanta a exceção correta com todos seus valores de propriedades preenchidos quando receber um SOAP fault packet.

Registrar as classes de exceção? Isso mesmo, como já podemos perceber tudo com que trabalhamos em Web Services deve ser registrado. O registro das classes de exceção se dá de forma idêntica a das descendentes de TRemotable que foi vista anteriormente.

Para exemplificar vamos criar agora uma exceção que será levantada quando o método IGeometry.Losango receber no parâmetro Centro uma coordenada não compreendida no primeiro quadrante. Então depois que definirmos a exceção vamos ter de alterar esse método para implementar tal comportamento. Vejamos a seguir o código de definição da exceção:

Listagem 7: Unit IGeometryImpl

unit EInvalidCentroU;
interface
uses InvokeRegistry;
type
EInvalidCentro = class(ERemotableException)
private
FX, FY : Integer;
public
constructor Create(const X, Y : Integer); 
published
property X : Integer read FX write FX;
property Y : Integer read FY write FY;
end;
implementation
{ EInvalidCentro }
constructor EInvalidCentro.Create(const X, Y: Integer);
begin
inherited Create("O centro deve estar no primeiro quadrante.");
FX := X;
FY := Y;
end;
initialization
RemTypeRegistry.RegisterXSClass(EInvalidCentro);
end.

Ufa! O caminho foi longo, mas chegamos lá. Com isso concluímos a codificação do nosso servidor, vamos passar agora para a implementação do cliente.

Implementando clientes para Web Services

Para iniciar vamos criar uma nova aplicação comum e deixá-la com a interface como abaixo:

Configuração dos componentes na tela

Figura 2: Configuração dos componentes na tela

Agora que já construímos a interface, vamos ver como requisitar os serviços remotos do Web Services. Como já destaquei anteriormente, um cliente independe da implementação do servidor, dessa forma, veremos como obter informações sobre um servidor que já está rodando e disponível.

Todo servidor Web Services deve publicar informações sobre si mesmo no padrão WSDL, no Delphi isso é feito automaticamente pela simples inclusão e configuração do componente TWSDLHTMLPublish no projeto do servidor. Essas informações estão acessíveis para nós, normalmente, através de uma action com path info "/wsdl". Nesse caso estamos rodando o servidor localmente sobre o IIS5.0, então podemos acessar essas informações solicitando a seguinte URL :

http://localhost/cgi-bin/WebServices.exe/wsdl Isso nos trará a seguinte tela:

WebService Listing

Figura 3: WebService Listing

Podemos encontrar então todas as interfaces implementadas pelo servidor bem como um link para a descrição WSDL de cada interface. Para vermos essa descrição vamos clicar no link de IMath. Fazendo isso seremos apresentados a seguinte tela:

Descrição WSDL da Interface IMath

Figura 4: Descrição WSDL da Interface IMath

OBS: Este comportamento acima descrito pode variar de acordo com a implementação do servidor.

Através dessas informações obtidas já podemos então analisá-las para começar a codificação das units de interface com o servidor. Quer dizer que temos que ler estas informações e codificá-las manualmente? Depende, se você estiver implementando o cliente em Delphi está livre dessa tarefa, podemos usar o Web Services Importer para fazer esse trabalho para nós:

Adcionando um Web Services Import

Figura 5: Adicionando um Web Services Importer

Web Services Import

Figura 6: Web Services Import

Devemos repetir esse procedimento para cada interface que queremos importar, no caso da IMath devemos preencher o campo acima com a seguinte URL:http://localhost/cgi-bin/WebServices.exe/wsdl/IMath

>

Depois de clicarmos em Generate veremos que ele transformou aquela descrição WSDL em units do Delphi muito semelhantes (senão igual) àquelas que foram definidas no servidor. Isso torna claro que se o servidor e o cliente forem ser implementados em Delphi podemos ignorar esse procedimento e simplesmente compartilhar as units de definição entre os dois projetos.

Nesse ponto, já temos um projeto com a interface gráfica e todas as units de importação prontas, passemos agora então a requisição dos métodos remotos.

THTTPRIO

O componente THTTPRIO é o que usamos para obter uma referência válida de uma interface registrada. Quando fazemos um type cast desse objeto para uma interface específica ele gera uma tabela de métodos em memória que é usada quando é feita uma chamada a um método específico.

Antes de começarmos a usar esse componente, devemos configurá-lo, isso pode ser feito de duas maneiras diferentes:

  1. Se o servidor foi escrito em Delphi precisamos apenas configurar a propriedade URL, cujo valor é gerado no registro da invokable interface.
  2. Independentemente da linguagem usada na implementação do servidor, podemos configurar as propriedades WSDLLocation, Service e Port. Quando configuramos WSDLLocation, deve ser usado o mesmo valor passado para o Web Services Importer (http://localhost/cgi-bin/WebServices.exe/wsdl/IMath), fica disponível valores para as propriedades Service e Port que devem ser selecionados no object inspector.
Propriedades do objeto HRMath

Figura 7: Propriedades do objeto HRMath

Eu particularmente prefiro esse método 2, por ser um método mais genérico.

Depois do componente configurado vamos passar a implementação do botão de somar para ilustrar o uso dele. Vejamos o código do botão somar:

Listagem 8: Botão Somar

procedure TfrmMain.SpeedButton1Click(Sender: TObject);
Var
Im : IMath;
A, B : Double;
begin
Im := HRMath as IMath;
A := StrToFloat(EdtX.Text);
B := StrToFloat(EdtY.Text);
LblResult.Caption := FloatToStr(Im.Soma(A, B));
end;

Podemos perceber que o uso dele é bem simples. Basta declararmos uma variável do tipo da interface desejada e atribuirmos a ela o type cast do componente. Depois de feito isso, podemos usá-la normalmente como se fosse um objeto. Lembre-se também que não é necessária a liberação de memória correspondente a referência da interface, ela é liberada automaticamente quando a variável sai de escopo. Esse procedimento deve ser repetido para todos os métodos que desejamos requisitar.

Para uma melhor compreensão do assunto abordado, recomendo que seja analisado os fontes dos projetos criados ao longo deste artigo.

TSoapConnection

O componente TSoapConnection pode ser usado por uma aplicação cliente que deseja conectar-se a uma mult-tiered database application, implementada como um Web Services. Ele nos dá a possibilidade de estabelecer a conexão entre servidor e cliente e obter a IAppServer do servidor, implementado num RemoteDataModule, a partir da qual temos acesso aos providers existentes nele.

Esse componente traz algumas vantagens em relação aos outros existentes que desempenham funções parecidas. Dentre essas vantagens, destacamos o uso do HTTP para transporte das informações, bem como o suporte a SSL. Podemos ter segurança na transmissão de dados e ainda evitamos problemas com proxys.

Consulte o help online para informações sobre pré-requisitos para o uso deste componente. O componente TSoapConnection desempenha mais ou menos o mesmo papel dos componentes TDCOMConnection, TSocketConnection, TWebConnection, TCORBAConnection.

Conclusão

Como podemos perceber a discussão sobre Web Services é extensa e muito interessante. Uma das coisas que mais impulsiona a disseminação dessa tecnologia é a simplicidade notada para implementar aplicações distribuídas quando comparada a outras tecnologias existentes no mercado.

Termino este artigo desejando que as todas as expectativas daqueles que iniciaram a sua leitura tenham sido satisfeitas. Agradecerei a todos que venham a opiná-lo ou comentá-lo.

Fernando Vasconcelos

Fernando Vasconcelos - Detém os títulos de Borland Delphi 5 Certified Developer (emitido pela Borland Latin America), Borland Delphi 5.0 / Borland Delphi 3.0 / OO Concepts / Programming Concepts / RDBMS Concepts / SQL Ansi / HTML 4.0 / Internet Concepts (emitidos pelo BrainBrench [TranscriptID=2997381]).
Atualmente ministra cursos de Delphi Avançado e Delphi para Internet bem como atua na equipe de desenvolvimento de uma solução distribuída de CRM com CTI (Computer Integration Telephony).