Desenvolvimento - WCF/WPF

WCF – WS-Discovery

Ao referenciar um serviço WCF em uma aplicação cliente, um proxy é gerado para abstrair toda a complexidade necessária para efetuar a comunicação entre o cliente e o serviço.

por Israel Aéce



Ao referenciar um serviço WCF em uma aplicação cliente, um proxy é gerado para abstrair toda a complexidade necessária para efetuar a comunicação entre o cliente e o serviço. Ao efetuar essa referência, além da classe que representa o proxy, o arquivo de configuração da aplicação cliente também é alterado, efetuando todas as configurações necessárias para que o WCF possa efetuar a requisição ao serviço remoto.

Entre essas configurações que são realizadas, uma delas é a criação do endpoint do lado do cliente, com o endereço, binding e o contrato necessário para efetuar as requisições. Ao fazer isso, o endereço ficará fixado no arquivo de configuração e não teremos problemas até que o endereço mude de local. Se, por algum motivo, o serviço não estiver mais disponível naquele endereço (endpoint) que foi inicialmente publicado, todos as aplicações deixarão de funcionar, até que alguém altere o endereço manualmente, apontando para o novo endereço.

Para solucionar este problema, a Microsoft estará incorporando ao WCF 4.0 a implementação do protocolo WS-Discovery. Como o próprio nome diz, WS-Discovery trata-se de um padrão criado pela OASIS, que define um mecanismo para o descobrimento de serviços de uma determinada rede, removendo a necessidade dos consumidores conhecerem o endereço do serviço até que a aplicação execute, e além disso, os serviços podem mudar constantemente de endereço, que os eventuais consumidores não deixarão de funcionar. A finalidade deste artigo é abordar como podemos proceder para criar, publicar e consumir serviços utilizando o WS-Discovery. Vale lembrar que este artigo será baseado na versão Beta do Visual Studio .NET 2010 e do .NET Framework 4.0, ou seja, poderá haver alguma mudança até a versão final.

Antes de analisar as classes que são necessárias para fazer com que o descobrimento de serviços funcione, precisamos primeiramente conhecer um pouco mais sobre o procotolo WS-Discovery em si, ou seja, analisar como as notificações são enviadas aos clientes e como esses clientes podem interrogar o serviço. Segundo as especificações deste protocolo, as mensagens utilizadas por ele para descobrimento do serviço são formatadas em padrão SOAP, enviadas através do protocolo UDP. Assim como vários outros padrões WS-*, o WS-Discovery utiliza várias mensagens para notificar e/ou descobrir um serviço na rede. Para entender o fluxo, vamos analisar a imagem abaixo, extraída da especificação do WS-Discovery:

Antes de compreender a imagem acima, é importante estar familiarizado com dois termos: multicasting e unicasting. Ambos são considerados routing schemes, e definem para quem enviar a mensagem. No formato multicast, a mensagem (seja ela qual for) será enviada de forma simultânea para um grupo de destinatários dentro da rede. Já no formato unicast, as informações são enviadas para um único destinatário da rede. Ainda existem outros formatos, como o broacast e anycast, mas não serão utilizados aqui.

(1) Ao entrar na rede, o serviço envia uma mensagem conhecida como “hello”, no formato multicast, e os clientes que fazem parte do mesmo grupo, podem detectar que o serviço está online, evitando que o cliente fique repetidamente consultando para ver se o serviço está ou não online, reduzindo a quantidade de informações trafegadas na rede. Em analogia à programação assíncrona, seria mais ou menos como receber um “callback” ao invés de utilizar o pooling. (2) Os clientes também podem enviar uma mensagem conhecida como “probe” no formato multicast para a rede, em busca de um serviço de um determinado tipo. (3) Serviços que atendam aquele critério, retornam uma mensagem conhecida como “probe match”, no formato unicast, para o respectivo cliente. O cliente pode querer procurar um serviço através de seu nome. (4) Neste caso, o cliente deve enviar uma mensagem conhecida como “resolve message” através do formato multicast, e caso um serviço seja encontrado, (5) ele responderá através de uma mensagem conhecida como “resolve match”. (6) Finalmente, ao deixar a rede, o serviço envia uma mensagem conhecida como “bye” no formato multicast, para notificar que ele está saindo.

Depois de uma pequena introdução ao protocolo WS-Discovery, vamos analisar os tipos que temos a disposição a partir do WCF 4.0, que permitem ao serviço e ao cliente efetuar o descobrimento. Esses tipos estão debaixo do namespace System.ServiceModel.Discovery, que por sua vez está definido dentro do Assembly System.ServiceModel.Discovery.dll.

Em princípio, o contrato e a classe que representa o serviço não sofrerão qualquer alteração. As mudanças começam na configuração do serviço, onde você deverá criar um endpoint para possibilitar o descobrimento do serviço. Esse endpoint permitirá ao serviço monitorar as requisições (as mesmas mostradas na imagem acima) que este protocolo envia ou recebe para garantir o seu funcionamento. Para configurá-lo, você pode optar pela programação declarativa ou imperativa. No modo imperativo, basta você passar uma instância da classe UdpDiscoveryEndpoint para o método AddServiceEndpoint da classe ServiceHost, enquanto pelo modo declarativo, você pode recorrer ao atributo kind do elemento endpoint, especificando o valor “udpDiscoveryEndpoint”, que determina um standard endpointpreconfigurado.

Além da criação de um endpoint para descobrimento, ainda precisamos adicionar um behavior em nível de serviço, que é representado pela classe ServiceDiscoveryBehavior (ou pelo elemento serviceDiscovery em modo declarativo), que indica ao serviço que ele possa ser descoberto. Esse behavior ainda fornece uma coleção chamada de AnnouncementEndpoints, que permite aos consumidores do serviço a receberem notificações de quando o serviço estiver online ou offline. Basicamente, dentro desta seção temos também os standard endpoints, que encapsulam toda a comunicação para fazer o protocolo WS-Discovery funcionar, sem interferir na comunicação com o serviço em si. O trecho de código abaixo exemplifica a configuração do protocolo WS-Discovery em um serviço, utilizando o modelo declarativo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="Service.Servico" behaviorConfiguration="bhv">
<host>
<baseAddresses>
<add baseAddress="http://localhost:8383/Srv/"/>
</baseAddresses>
</host>
<endpoint address="" binding="basicHttpBinding" contract="Service.IContrato" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
<endpoint name="udpDiscovery" kind="udpDiscoveryEndpoint" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="bhv">
<serviceMetadata />
<serviceDiscovery>
<announcementEndpoints>
<endpoint name="udpAnnouncement" kind="udpAnnouncementEndpoint" />
</announcementEndpoints>
</serviceDiscovery>

</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>

Consumindo o Serviço

Como falado acima, geralmente efetuamos a referência do serviço no cliente, onde a IDE ou o svcutil.exe faz todo o trabalho para a geração do proxy e do arquivo de configuração correspondente. Você pode continuar utilizando estas técnicas para construir os artefatos necessários para efetuar a comunicação, mas ao invés de definir o endereço de forma estática no arquivo de configuração, vamos fazer uso do protocolo WS-Discovery. O WS-Discovery é útil quando o serviço pode mudar frequentemente de endereço, e fixá-lo fará com que uma exceção seja disparada quando o mesmo não estiver mais respondendo naquele endereço, obrigando o administrador ou desenvolvedor alterar para o novo endereço de forma manual.

Como já falamos acima, utilizando o WS-Discovery não há mais a necessidade de conhecer o endereço, pois compete a este protocolo procurar pelos serviços que estão rodando naquela rede. Antes de invocar efetivamente a operação que o serviço disponibiliza, há alguns passos extras que devem ser realizados para que o cliente consiga determinar o endereço onde o serviço está rodando. Felizmente o WCF fornece uma classe chamada DiscoveryClient, que dado um endpoint de descobrimento e um critério de pesquisa, dispara as mensagens “probe” ou “resolve” para tentar encontrar o serviço em questão. O endpoint utilizado pelo cliente segue as mesmas características do endpoint utilizado pelo serviço, ou seja, fazendo uso do padrão UDP.

O critério a ser pesquisado deve ser criado a partir da classe FindCriteria, que em seu construtor recebe um parâmetro que especifica o contrato do serviço a ser pesquisado na rede. É importante dizer que neste momento é necessário que o cliente conheça, de alguma forma, o contrato que os possíveis serviços que estão rodando na rede implementem. Esse contrato pode ter sido fornecido out-of-band (por exemplo via e-mail), através do compartilhamento de assemblies que especificam os contratos e tipos que são utilizados por estes serviços, ou até mesmo via IDE ou através do utilitário svcutil.exe. A instância desta classe é passada como parâmetro para o método Find da classe DiscoveryClient, que por sua vez, retorna uma instância da classe FindResponse, contendo o resultado da pesquisa. O código abaixo ilustra essa primeira etapa do descobrimento:

using (DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint()))
{
FindResponse fr = dc.Find(new FindCriteria(typeof(IContrato)));
}

É importante dizer que o método Find é sobrecarregado, ou seja, há uma segunda versão dele que não recebe parâmetros, retornando todos os serviços ativos na rede, independentemente de quais contratos ele venha a implementar. Além disso, a classe DiscoveryClient ainda fornece um método chamado FindAsync, que como o próprio nome diz, possibilita a busca por serviços de forma assíncrona, pois caso este processo demore, você estará livre para continuar trabalhando em outras áreas do sistema, efetuando outras tarefas. Utilizando a forma assíncrona, você pode se vincular aos eventos FindProgressChanged e FindCompleted, para ser notificado a respeito do progresso da busca e quando ela for finalizada, respectivamente.

A classe FindResponse, que representa o resultado da pesquisa, possui apenas uma única propriedade chamada Endpoints. Essa propriedade retorna uma coleção, onde cada elemento é do tipo EndpointDiscoveryMetadata. Cada um destes elementos representam os serviços que estão ativos na rede e que implementam o contrato que você especificou no critério de busca, disponibilizando informações através de suas propriedades, de cada serviço descoberto, como por exemplo, o endereço (Address) e os contratos que o serviço implementa (ContractTypeNames).

A partir deste momento passamos a utilizar as classes já conhecidas, e que existem desde a primeira versão do WCF. Como falado acima, temos duas opções: a primeira delas consiste em criar o proxy através da IDE ou do utilitário svcutil.exe. O proxy criado possui um overload do contrutor que recebe uma instância da classe EndpointAddress, que representa o endereço de acesso ao serviço. Como antes o endereço estava fixado no arquivo de configuração, não era necessário informar explicitamente, já que com a configuração padrão, ele envia a requisição para aquele endereço preconfigurado. Já a segunda alternativa, geralmente utilizada quando estamos compartilhando o contrato através de assemblies, podemos recorrer a classe ChannelFactory<TChannel>, que dado uma instância da classe EndpointAddress e o binding em seu construtor, também estabelece a comunicação com o serviço em questão.

A propriedade Address, exposta pela classe EndpointDiscoveryMetadata, retorna uma instância da classe EndpointAddress contendo o endereço do serviço descoberto. Como já podemos assimilar, essa instância é passada para o proxy ou para a classe ChannelFactory<TChannel>. Para exemplificar, estarei utilizando a segunda opção, onde utilizarei a classe ChannelFactory<TChannel> para estabelecer a conexão e executar a operação no serviço descoberto pela classe DiscoveryClient:

using (DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint()))
{
FindResponse fr = dc.Find(new FindCriteria(typeof(IContrato)));

if (fr.Endpoints.Count > 0)
{
EndpointAddress address = fr.Endpoints[0].Address;

using (ChannelFactory<IContrato> factory =
new ChannelFactory<IContrato>(new BasicHttpBinding(), address))
{
IContrato client = factory.CreateChannel();
Console.WriteLine(client.Ping(DateTime.Now.ToString()));
}
}
else
{
Console.WriteLine("Nenhum serviço encontrado.");
}
}

Se estivermos utilizando o proxy que foi gerado automaticamente através do WSDL, então somente devemos substituir o ChannelFactory<TChannel> pela instância do proxy, informando o endereço do serviço que foi descoberto. É importante notar também que o arquivo App.Config não possui nenhuma configuração do serviço.

A principal vantagem do descobrimento é conseguir identificar um serviço que muda frequentemente de endereço. Como vimos acima, ao encontrar o serviço, criamos a instância da classe ChannelFactory<TChannel> com o respectivo endereço e informamos o binding BasicHttpBinding. Como estamos em um projeto de testes, onde tudo é facilmente controlado, conseguimos definir o mesmo tipo de binding, já que sabemos qual deles foi utilizado para expor o serviço. Mas em um mundo real, o serviço pode, por algum motivo, também alterar o binding que está sendo exposto pelo serviço, e por mais que você saiba o endereço até o mesmo, se o binding não corresponder ao mesmo utilizado pelo serviço, a comunicação não será possível.

O binding é uma informação que é exposta através do documento WSDL. Caso o serviço disponibilize este documento através de um endereço (isso não é obrigatório para o WS-Discovery funcionar), você pode utilizar o método estático Resolve da classe MetadataResolver (existente desde a primeira versão do WCF), para efetuar o download dos metadados de um determinado contrato, onde também teremos uma coleção com os endpoints disponíveis, e cada elemento desta coleção é representado pela classe ServiceEndpoint. Essa classe expõe uma propriedade chamada Binding, que como o próprio nome diz, retorna a instância de um binding que corresponde ao binding que está sendo utilizado pelo endpoint remoto.

É importante dizer que o método Resolve necessita do endereço até o endpoint que expõe o documento WSDL, e sendo assim, o critério de busca passa a ser pelo endereço do WSDL e não mais pelo endereço do serviço diretamente. O código cliente está totalmente dinâmico, ou seja, tanto o endereço como o binding são descobertos durante a execução. Aplicando essas mudanças, o código de exemplo que vimos acima passa a ficar da seguinte maneira:

using (DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint()))
{
FindResponse fr = dc.Find(FindCriteria.CreateMexEndpointCriteria(typeof(IContrato)));
if (fr.Endpoints.Count > 0)
{
EndpointAddress mex = fr.Endpoints[0].Address;

ServiceEndpointCollection sec =
MetadataResolver.Resolve(typeof(IContrato), mex);

if (sec.Count > 0)
{
ServiceEndpoint se = sec[0];

using (ChannelFactory<IContrato> factory =
new ChannelFactory<IContrato>(se.Binding, se.Address))
{
IContrato client = factory.CreateChannel();
Console.WriteLine(client.Ping(DateTime.Now.ToString()));
}
}
}
else
{
Console.WriteLine("Serviço não encontrado.");
}
}

Para finalizar, note que utilizamos uma instância da classe FindCriteria que foi retornada através do método estático CreateMexEndpointCriteria, pois o foco agora é procurar pelo documento WSDL, para conseguir extrair também o respectivo binding que está sendo exposto pelos endpoints.

Notificação através de Announcements

Announcements
é uma funcionalidade que é disponibilizada juntamente com o WS-Discovery que possibilita o cliente ser notificado quando o serviço estiver online ou offline. Isso permitirá você interceptar essas notificações e tomar alguma decisão para determinar se deve ou não invocar o serviço. Podemos enxergar os announcements a partir das mensagens “hello” e “bye” da imagem acima, enviando-as a partir do formato multicast, ou seja, somente os clientes interessados irão receber essas mensagens.

Para que isso funcione, primeiramente você precisa configurar o serviço para que ele notifique os possíveis clientes quando entrar no ar ou quando ele for deixar a rede (quando fechar o host). Para efetuar essa primeira configuração, você precisa adicionar um endpoint de announcement (também UDP) no elemento announcementEndpoints, do behavior serviceDiscovery. O código do arquivo de configuração que vimos acima, já está com esse endpoint devidamente configurado.

Já do lado do cliente, há algumas classes que precisamos utilizar para que o mesmo seja notificado. É importante dizer que não será o proxy que irá monitorar essas mensagens, mas sim um serviço chamado AnnouncementService. Essa classe irá “ouvir” as mensagens de “hello” e “bye”, enviadas pelo serviço, e notificará a aplicação cliente através dos eventos OnlineAnnouncementReceived e OfflineAnnouncementReceived. Como disse anteriormente, essa classe será um serviço hospedado pelo próprio cliente (self-hosted), ou seja, devemos configurar os eventos e passar a instância dela para um ServiceHost. A única configuração necessária no host é a adição de um standard endpoint, que é representado pela instância da classe AnnouncementEndpoint. O código a seguir mostra o quanto é simples fazer a aplicação cliente receber essas mensagens:

AnnouncementService announSrv = new AnnouncementService();
announSrv.OfflineAnnouncementReceived += (sender, e) => Console.WriteLine("Serviço Offline.");
announSrv.OnlineAnnouncementReceived += (sender, e) => Console.WriteLine("Serviço Online.");

using (ServiceHost announHost = new ServiceHost(announSrv))
{
announHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
announHost.Open();

Console.ReadLine();
}

Extensions

A classe EndpointDiscoveryMetadata ainda fornece uma propriedade chamada Extensions, que nos permite passar informações extras do serviço para o cliente, que estarão acessíveis durante o processo de descobrimento. Para que você consiga adicionar essas informações, você deverá recorrer à um behavior de endpoint, chamado EndpointDiscoveryBehavior. Essa classe fornece uma propriedade chamada Extensions, que expõe uma coleção de elementos do tipo XElement, representando as informações customizadas que desejamos enviar ao cliente.

Primeiramente é necessário criar a instância da classe EndpointDiscoveryBehavior e configurar as extensões através da propriedade Extensions. Depois disso, basta adicioná-la na coleção de behaviors de um endpoint específico, acessível a partir do host (ServiceHost). A escolha de qual endpoint adicionar, dependerá de qual critério utilizará para busca, e no caso do exemplo acima, estamos procurando pelo endpoint que expõe o WSDL. O código abaixo ilustra a criação da classe EndpointDiscoveryBehavior e como adicioná-la ao endpoint:

using (ServiceHost host = new ServiceHost(typeof(Servico), new Uri[] { }))
{
EndpointDiscoveryBehavior edb = new EndpointDiscoveryBehavior();
edb.Extensions.Add(new XElement("InfoExtra", "ViaDiscovery"));
host.Description.Endpoints[1].Behaviors.Add(edb);

host.Open();
Console.ReadLine();
}

Já do lado do cliente, tudo o que precisamos fazer (caso o endpoint seja encontrado), é também recorrer a propriedade Extensions, assim como é mostrado abaixo:

using (DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint()))
{
FindResponse fr = dc.Find(FindCriteria.CreateMexEndpointCriteria(typeof(IContrato)));

if (fr.Endpoints.Count > 0)
Console.WriteLine(fr.Endpoints[0].Extensions[0].Value);
else
Console.WriteLine("Serviço não encontrado.");
}

Conclusão: Vimos neste artigo como funciona a utilização do protocolo WS-Discovery, que estará disponível com a versão 4.0 do WCF. Isso traz grandes benefícios aos serviços que mudam constantemente de endereço, e principalmente, para os clientes que consomem, já que não precisam alterar nenhuma configuração caso isso aconteça. Esta nova funcionalidade é adicionada ao WCF seguindo as mesmas características anteriores, ou seja, com pequenas configurações habilitamos este recurso, sem a necessidade de entender como o protocolo funciona nos bastidores, nos permitindo continuar com o foco no desenvolvimento da regra de negócio.

WSDiscovery.zip (14.63 kb)

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.