Desenvolvimento - WCF
WCF - Comunicação P2P
Quando ouvimos dizer sobre programas P2P, sempre nos vem a mente aqueles softwares que compartilham diversos conteúdos (ilegal ou não, não é o caso a ser discutido aqui), e que geralmente estão em formato de músicas, vídeos, livros, etc. A ideia é cada ponto publicar o que deseja disponibilizar, para que outros pontos que estão dentro da mesma rede, possam baixar o que lhes interessam.
por Israel AéceQuando ouvimos dizer sobre programas P2P, sempre nos vem a mente aqueles softwares que compartilham diversos conteúdos (ilegal ou não, não é o caso a ser discutido aqui), e que geralmente estão em formato de músicas, vídeos, livros, etc. A ideia é cada ponto publicar o que deseja disponibilizar, para que outros pontos que estão dentro da mesma rede, possam baixar o que lhes interessam.
Mas publicar conteúdo não é a única finalidade deste tipo de comunicação. Podemos usufruir deste tipo de rede para colaboração, que é algo cada vez mais frequente nas empresas, e além disso, a possibilidade de distribuir processamento entre os vários pontos, para que todos consigam cooperar na execução de uma tarefa maior.
Cada um dos pontos que fazem parte de uma rede P2P, são conhecidos como nó (node) ou peer, e a rede como mesh (mesh network). Mas uma rede P2P pode ser definida de duas formas, onde a primeira é considerada pura, ou seja, não existirá um servidor - centralizador - envolvido, e cada ponto atuará como cliente e servidor. Já a outra forma, conhecida como híbrida, teremos um servidor fazendo parte da rede, mas a finalidade dele é apenas de rastrear ou de gerenciar a comunicação entre os pontos, mas não é responsável por armazenar qualquer tipo informação.
Para implementarmos uma rede P2P, a Microsoft disponibilizou dentro do .NET Framework um namespace chamado System.Net.PeerToPeer, que contém todos os tipos necessários para a construção de uma rede deste tipo. Mas como o WCF é o pilar de comunicação dentro do .NET Framework, a Microsoft resolveu permitir a exposição de serviços através do modelo P2P, podendo reutilizar o conhecimento que temos em serviços tradicionais para expor tarefas/dados através deste modelo. Isso evitará conhecermos detalhes de mais baixo nível, que é algo que teríamos que fazer se fossemos utilizar as classes contidas no namespace System.Net.PeerToPeer.
Apesar de utilizar o WCF como meio de criação de aplicações que estarão conectadas via P2P, temos algumas mudanças consideráveis em relação ao contrato e ao consumo destes serviços, justamente pela natureza dele. Para explicar melhor, vamos explorar o contrato, que é o primeiro passo a ser realizado para a construção do serviço. Abaixo temos a interface quepossui um método chamado EnviarMensagem que possui duas strings como parâmetro, que representam o remetente e a mensagem, respectivamente. O principal detalhe a ser notar aqui, é a propriedade CallbackContract do atributo ServiceContractAttribute. Geralmente utilizamos uma interface de callback separada, mas como as mensagens enviadas e recebidas terão o mesmo formato, o contrato de callback deverá ser o mesmo que o contrato que descreve o serviço. Isso é referido como contrato simétrico.
[ServiceContract(CallbackContract = typeof(IContrato))]
public interface IContrato
{
[OperationContract(IsOneWay = true)]
void EnviarMensagem(string de, string mensagem);
}
Quando desenvolvemos um serviço WCF "tradicional", o próximo passo é a criação da aplicação que servirá como hospedeira da classe que representará o serviço, e que deverá utilizar a classe ServiceHost (ou uma de suas derivadas) para gerenciar a execução do serviço. Mas como falamos acima, uma das alternativas de redes P2P é não possuir um servidor, ou seja, a criação de uma aplicação para hospedar o serviço, neste caso,não existirá.
Com essa grande diferença, tudo o que precisaremos fazer é a configuração do lado do cliente. E como sabemos, todo serviço WCF deve possuir três características essenciais, conhecidas como Address, Binding e Contract. Aqui o binding será o NetPeerTcpBinding, que foi criado justamente para abstrair toda a comunicação feita sobre o protocolo P2P, e que internamente, recorrerá aos tipos fornecidos pelo namespace System.Net.PeerToPeer, conforme falamos acima.
Seguindo em frente, devemos definir o endereço do serviço, que na verdade será o nome da rede (mesh) que todos os clientes farão parte. Mantendo a mesma padronização imposta pelo WCF, aqui o endereço deverá ser prefixado com net.p2p, a na sequência o nome do serviço. Um pouco mais abaixo, temos a configuração do binding, definindo a porta em que as mensagens serão processadas. Definindo a porta 0 (zero), fará com que o WCF escolha uma porta que não esteja sendo utilizada no momento.
Por fim, e não menos importante, temos o resolver. Como o próprio nome diz, o resolver é o responsável por manter e resolver os IDs daqueles que estão conectados na rede. Quando o canal é aberto, o WCF utiliza o resolver para resolver o ID daqueles que estão conectados à rede, e assim criar uma rede com pontos interconectados. O atributo mode define qual modelo de resolver será utilizado, e você pode escolher uma em três opções, que estão definidas através do enumerador PeerResolverMode, que estão listados abaixo:
-
Pnrp: Utiliza o padrão PNRP (Peer Name Resolution Protocol), que é um protocolo desenvolvido pela Microsoft que habilita a publicação e resolução dinâmica de nomes.
-
Custom: Com esta opção, você pode criar um serviço para a resolução de nomes e IDs daqueles que estão envolvidos na rede. Um exemplo mais detalhado será mostrado abaixo.
-
Auto: Quando configurado com a opção Auto e houver um resolver customizado definido, ele será utilizado; caso contrário, tentará fazer uso do protocoloPnrp.
Abaixo temos toda a configuração do arquivo App.config da aplicação. Note que neste primeiro momento, estamos utilizando o resolver definido como Pnrp:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint address="net.p2p://servicos/chat"
binding="netPeerTcpBinding"
bindingConfiguration="bindingConfig"
contract="AplicacaoDeChat.IContrato"
name="ChatEndpoint"/>
</client>
<bindings>
<netPeerTcpBinding>
<binding name="bindingConfig"
port="0">
<security mode="None" />
<resolver mode="Pnrp" />
</binding>
</netPeerTcpBinding>
</bindings>
</system.serviceModel>
</configuration>
Utilizar o PNRP parece ser a melhor alternativa, mas as vezes você está em um ambiente em que ele não é suportado, ou por algum motivo, você quer controlar como esse procedimento é realizado. Para isso que a opção Custom foi criada. Isso nos permitirá construir um serviço WCF, e lá customizar toda a regra de resolução de nomes/IDs.
Para essa customização ser feita, o primeiro passo é criar um serviço que fará essa tarefa. Felizmente o WCF já traz uma classe pronta, que corresponde ao serviço de resolução. Essa classe é chamada de CustomPeerResolverService, e implementa uma interface que define o seu contrato: IPeerResolverContract. Apesar de haver vários membros, essa interface fornece alguns principais métodos, que são autoexplicativos: Register, Resolve e Unregister. Caso essa implementação não te atenda, tudo o que você precisa fazer é criar a sua própria implementação, utilizando esta mesma interface.
Como ele será um serviço WCF puro, precisamos criar uma aplicação que sirva como hospedeira para ele, recorrendo ao ServiceHost. Note que logo abaixo temos o arquivo de configuração correspondente, que está expondo o serviço através do protocolo TCP:
CustomPeerResolverService cprs = new CustomPeerResolverService();
using (ServiceHost host = new ServiceHost(cprs, new Uri[] { }))
{
cprs.Open();
host.Open();
Console.WriteLine("[ Serviço de Resolução Ativo ]");
Console.ReadLine();
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="System.ServiceModel.PeerResolvers.CustomPeerResolverService">
<endpoint address="net.tcp://localhost:8282/prs"
binding="netTcpBinding"
bindingConfiguration="bindingConfig"
contract="System.ServiceModel.PeerResolvers.IPeerResolverContract" />
</service>
</services>
<bindings>
<netTcpBinding>
<binding name="bindingConfig">
<security mode="None"/>
</binding>
</netTcpBinding>
</bindings>
</system.serviceModel>
</configuration>
Quando este serviço estiver ativo, podemos definir o resolver customizado nas aplicações, e como já sabemos, devemos definir o atributo mode como Custom, e logo no interior do elemento resolver, especificarmos as configurações de acesso ao serviço de resolução, tais como o endereço, o binding e sua respectiva configuração. O código abaixo ilustra a mesma configuração que vimos acima, mas agora utilizando um resolver customizado ao invés do PNRP:
<bindings>
<netPeerTcpBinding>
<binding name="bindingConfig"
port="0">
<security mode="None" />
<resolver mode="Custom">
<custom address="net.tcp://localhost:8282/prs"
binding="netTcpBinding"
bindingConfiguration="peerBindingConfig" />
</resolver>
</binding>
</netPeerTcpBinding>
<netTcpBinding>
<binding name="peerBindingConfig">
<security mode="None" />
</binding>
</netTcpBinding>
</bindings>
Depois do resolver definindo (não importa qual modelo seja), chega o momento que criarmos a aplicação que fará uso deste tipo diferenciado de serviço. Como sabemos, não há um serviço rodando que podemos efetuar a referência. Tudo o que precisaremos fazer aqui é o "consumo direto", criando manualmente as classes para efetuar essa comunicação.
Para o consumo, é necessário a criação de um canal de comunicação, e o responsável por isso é a classe ChannelFactory<TChannel>. Mas neste caso, o canal deverá ser duplex (bidirecional), o que nos obriga a utilizar uma factory especializada para esta situação, que é a classeDuplexChannelFactory<TChannel>. O construtor desta classe deve receber a instância da classe InstanceContext, que possui acesso à classe do lado do cliente que implementa a interface (contrato) de callback. Além dela, ainda é necessário apontar o nome do endpoint que está no arquivo de configuração, para que a factory possa criar os canais utilizando todas as configurações previamente estabelecidas.
Como estamos construindo um chat, estamos implementando o contrato de callback no próprio formulário. O método EnviarMensagem é o callback, que será disparado quando uma mensagem chegar, enquanto o método Enviar_Click é o tratador do evento Click do botão, que será disparado quando a aplicação desejar enviar uma mensagem. Abaixo temos o formulário já implementado, com algumas linhas de código omitidas por questões de espaço:
public partial class Conversacao : Form, IContrato
{
private IContratoChannel channel;
private string nomeDoUsuario;
public Conversacao(string nomeDoUsuario)
{
InitializeComponent();
this.nomeDoUsuario = nomeDoUsuario;
this.channel = new DuplexChannelFactory<IContratoChannel>(
new InstanceContext(this), "ChatEndpoint").CreateChannel();
this.channel.Open();
}
public void EnviarMensagem(string de, string mensagem)
{
this.Mensagens.Text +=
string.Format("{0}:{2}{1}{2}{2}", de, mensagem, Environment.NewLine);
}
private void Enviar_Click(object sender, EventArgs e)
{
this.channel.EnviarMensagem(this.nomeDoUsuario, this.Mensagem.Text);
}
}
Distribuindo as Mensagens
Aparentemente tudo funciona da forma correta, mas é importante ter cuidado com o modo em que as mensagens são enviadas para a rede. O P2P utiliza broadcast, ou seja, se uma aplicação mandar uma mensagem para a rede, todos os pontos que estiverem conectados, receberão a mesma. Na imagem acima temos a sensação de que somente as duas pessoas estão conversando, mas se uma outra instância desta mesma aplicação entrar no ar, ela também começará a receber as mensagens.
O WCF possui algumas formas para controlar a distribuição das mensagens entre os pontos que compõem a rede, onde a primeira é através de uma contagem e a segunda utilizando um filtro. No primeiro caso, podemos modificar o nosso contrato, criando ao invés de strings que representam as informações do mensagem, uma classe para incorporar todos os parâmetros necessários, e além disso, devemos incluir um novo campo para contabilizar o número de encaminhamentos que a mensagem deverá fazer.
Como a API que estamos utilizando abstrai grande parte da comunicação entre os pontos, podemos apenas incluir no contrato da mensagem um campo decorado com o atributo PeerHopCountAttribute, que será decrementado a cada ponto que ela for entregue, e quando atingir o número 0, por mais que existam ainda pontos, eles não receberão a respectiva mensagem. Abaixo temos a classe Mensagem com os campos que compõem a estrutura do negócio e um campo chamado Hops, que será utilizando apenas quando estiver sendo exposto através do binding NetPeerTcpBinding.
[MessageContract]
public class Mensagem
{
[PeerHopCount]
public int Hops;
[MessageBodyMember]
public string De;
[MessageBodyMember]
public string Para;
[MessageBodyMember]
public string Texto;
}
É importante dizer que o contrato que criamos acima (IContrato), também foi mudado para receber esta nova classe ao invés do parâmetros separados.
Para finalizar, podemos também fazer o uso de filtros, que permite criar alguma espécie de regra para entregar ou não a mensagem. Note que coloquei na classe Mensagem um campo chamado Para que não havia na implementação inicial. A finalidade deste campo é analisar se o ponto que está recebendo a mensagem é mesmo o usuário à qual se destina. Caso seja, entregaremos a mensagem e iremos parar a propagação para os pontos seguintes; caso contrário, não entregaremos e iremos optar por propagar a mensagem adiante.
A criação de um filtro consiste em implementar a classe abstrata PeerMessagePropagationFilter, que nos obriga a sobrescrever o método ShouldMessagePropagate. Esse método possui um parâmetro que corresponde a mensagem que está chegando e um segundo parâmetro do tipo PeerMessageOrigination, que determina se a origem dela é local ou remota. Esse parâmetro é útil para determinar se a mensagem partiu da própria aplicação, e que no nosso caso, deveremos entregar para ela própria para conseguir exibir o conteúdo na tela.
Esse método deve retornar uma das opções definidas no enumerador PeerMessagePropagation, que determinará se a mensagem deverá ser entregue apenas localmente, remotamente, ambos ou não deve fazer nada. Para exemplificar, abaixo temos a classe UserNameChatFilter, que analisará o conteúdo da classe Message que vem como parâmetro e determinará se ele deve ou não ser entregue para a aplicação local, olhando para a origem da mensagem e para o atributo Para do corpo da mensagem.
internal class UserNameChatFilter : PeerMessagePropagationFilter
{
private string username;
public UserNameChatFilter(string username)
{
this.username = username;
}
public override PeerMessagePropagation ShouldMessagePropagate(
Message message, PeerMessageOrigination origination)
{
if (origination == PeerMessageOrigination.Local)
{
return PeerMessagePropagation.LocalAndRemote;
}
else
{
Mensagem temp =
(Mensagem)TypedMessageConverter.Create(typeof(Mensagem), null).FromMessage(message);
string to = temp.Para;
return to == username ? PeerMessagePropagation.Local : PeerMessagePropagation.Remote;
}
}
}
Depois da classe criada, temos que adicioná-la à execução, e para isso vamos recorrer ao método GetProperty<T> da classe genérica DuplexChannelFactory<TChannel>. Esse método genérico é utilizado para extrair da channel stack, objetos que são específicos ao modelo de comunicação. Neste caso, vamos tentar recuperar o objeto PeerNode, que corresponde ao ponto atual. Essa classe possui uma propriedade chamada MessagePropagationFilter, que pode receber uma instância de alguma classe que implemente um filtro customizado (PeerMessagePropagationFilter), assim como fizemos acima. O código abaixo ilustra como acoplar o filtro à execução:
this.channel.GetProperty<PeerNode>().MessagePropagationFilter =
new UserNameChatFilter(nomeDoUsuario);
Com isso, ao enviar uma requisição para um usuário específico, o filtro entrará em ação, avaliando se deve receber a mensagem ou propagar para o próximo ponto existente na rede. Apesar de conseguirmos atingir o objetivo, talvez esse modelo não seja a melhor saída em algumas situações, inclusive aqui. Como na maioria das vezes queremos conversar com alguém específico, o melhor seria manter um contato direto, ao invés de inundar a rede em busca desta pessoa.
Conclusão: Apesar de mudar um pouco a forma de como se cria serviços WCF, vimos neste artigo que temos mais uma possibilidade de criar aplicações conectadas, que agora também nos permite usufruir de uma rede P2P utilizando um modelo se comunicação ligeiramente diferente do qual estamos habituados a trabalhar.