quinta-feira, 3 de abril de 2008

[.NET] Comunicador simples com .NET remoting


Os componentes das APIs .Net Remoting e Java RMI (Remote Method Invocation) provêm funcionalidades para desenvolvimento de sistemas distribuídos, permitindo a chamada remota de procedimentos (i.e. de forma semelhante à RPC, no entanto, sobre o paradigma OO). Neste contexo, os métodos invocados podem ser de processos distindos na mesma estação ou em estações diversas (i.e. máquinas virtuais distintas).

Tal tecnologia também é fundamental para a integração de sistemas, sendo um alternativa aos webservices em contextos onde a performance é crítica. De forma geral, a grande vantagem sobre um implementação a nível de sockets é que se dispensa o desenvolvimento de todo um protocolo de comunicação.
Para o desenvolvimento de uma aplicação cliente-servidor sobre estas APIs, deve-se definir a interface de serviços a serem oferecidos pelo objeto servidor. Os serviços especificados pela interface remota deverão ser implementados através de classes concretas. Com a interface estabelecida e o serviço implementado, é finalmente possível criar as aplicações cliente e servidor, sendo necessário apenas o registro do serviço para que as APIs citadas encapsulem toda a serialização e transferências das mensagens.

Neste artigo apresenta-se uma aplicação "chat" simples sobre a API .Net Remoting da plataforma .NET 2.0, onde diversos clientes contectam-se a um servidor para enviar mensagens aos outros participantes. Desta forma, a solução será dividada em 3 projetos:
  • assembly "MessagerCore" (DLL): contendo toda a lógica da aplicação.

  • aplicação console "MessagerServer": com responsabilidade de ser o mediador de mensagens e servidor da aplicação .Net Remoting

  • aplicação Windows Forms "MessagerClient": com responsabilidade de interface dos usuários e cliente .Net Remoting
Neste aspecto, inicia-se pelo desenvolvimento do assembly "MessagerCore" (DLL), através da definição das interfaces do serviço. Foram definidas as interfaces IChatter (representa o usuário cliente do chat) e IChatServer (representa o mediador de mensagens). Na interface IChatter, o método MessageArrived(string message) dispara o evento IncomingMessage para tratamento no cliente.

Interface IChatter
namespace net.jorgealbuquerque.Messager.Core
{
public interface IChatter
{
// Recupera o nome do usuário do chat
string Name { get; }
// Dispara o evento IncomingMessage para tratamento no cliente
void MessageArrived(string message);
}
}

Na interface IChatServer, os métodos RegisterChatter e UnRegisterChatter registram, respectivamente, o ingresso e abandono do cliente no chat. Por sua vez, o método AddMessage(IChatter sender, string message) representa o envio da mensagem.

Interface IChatServer
namespace net.jorgealbuquerque.Messager.Core
{
public interface IChatServer
{
// Ingressa no chat
void RegisterChatter(IChatter chatter);
// Abandona o chat
void UnRegisterChatter(IChatter chatter);
// Envia mensagem
void AddMessage(IChatter sender, string message);
}
}
Uma vez definidas as interfaces do serviço remoto, pode-se construir suas classes concretas. Nestes contexto, a classe Chatter (realização de IChatter) poderia ser implementada na assembly cliente e a classe ChatServer (realização de IChatServer) poderia ser implementada no servidor. No entanto, foi realizada a opção de manter toda a lógica no assembly "MessagerCore", de forma a prover apenas lógica de apresentação nas aplicações cliente e servidor. Tal escolha quebra um pouco as o design pattern observer, mas separa eficientemetne as camadas de negócios e apresentação. Observe, no entanto, que não existe nenhuma dependência entre os namespaces do cliente e servidor.

A classe Chatter estende MarshalByRefObject, o que permite a sua serialização e manipulação remota pela API .NET remoting. Note ainda que o método MessageArrived(string message) ativa o evento IncomingMessage.

Classe Chatter
using System;
using System.Runtime.Remoting;
using net.jorgealbuquerque.Messager.Core;
namespace net.jorgealbuquerque.Messager.Client
{
public class Chatter : MarshalByRefObject, IChatter
{
public Chatter(string name)
{
_Name = name;
}
public event IncomingMessageEventHandler IncomingMessage;
public delegate void IncomingMessageEventHandler(string Message);
private string _Name;
public string Name
{
get { return _Name; }
}
public void MessageArrived(string message)
{
if (this.IncomingMessage != null)
this.IncomingMessage(message);
}
}
}


De forma semelhante, o servidor também estende MarshalByRefObject. Neste contexto, o método UpdateObsevers(string message) realiza a atualização de todos os clientes, segundo o design pattern observer, por meio do evento IncomingMessage.


Classe ChatServer
using System;
using System.Runtime.Remoting;
using net.jorgealbuquerque.Messager.Core;
using System.Collections.Generic;
namespace net.jorgealbuquerque.Messager.Server
{
public class ChatServer : MarshalByRefObject, IChatServer
{
private List _Chatters = new List();
public override object InitializeLifetimeService()
{
return null;
}
private void UpdateObsevers(string message)
{
foreach (IChatter observer in _Chatters)
observer.MessageArrived(message);
//Console.WriteLine(message);
}
public void AddMessage(IChatter chatter, string message)
{
if (string.IsNullOrEmpty(message))
return;
UpdateObsevers(string.Format("{0}) {1} disse: {2}",
DateTime.Now.ToLongTimeString(),
chatter.Name, message));
}
public void RegisterChatter(IChatter chatter)
{
_Chatters.Add(chatter);
UpdateObsevers(
string.Format("{0}) Usuário {1} entrou.",
DateTime.Now.ToLongTimeString(), chatter.Name));
}
public void UnRegisterChatter(IChatter chatter)
{
_Chatters.Remove(chatter);
UpdateObsevers(
string.Format("{0}) Usuário {1} saiu.",
DateTime.Now.ToLongTimeString(), chatter.Name));
}
}
}
Finalmente, a lógica de registro e conexão dos servidor e cliente foram encapsulados nas classes de serviço abaixo. A classe ServerChannelService apresenta registro para intanciação remota do ChatServer como Singleton (i.e. apenas um mediator para todos os clientes). Por sua vez, a classe ClienteChannelService mostra a chamada remota propriamente dita da mesma classe pela construção Activator.GetObject().

Classe ServerChannelService
using System;
using System.Runtime.Remoting;
using net.jorgealbuquerque.Messager.Core;
namespace net.jorgealbuquerque.Messager.Server
{
public class ServerChannelService
{
public ServerChannelService(int port)
{
new RemotingTcpServicesHelper().ServerRegisterChannel("ChatServer", port);
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(ChatServer), "ChatServer", WellKnownObjectMode.Singleton);
}
}
}


Classe ClientChannelService
using System;
using net.jorgealbuquerque.Messager.Core;
namespace net.jorgealbuquerque.Messager.Client
{
public class ClientChannelService
{
IChatServer _ChatServer = null;
public ClientChannelService(string url, int port)
{
new RemotingTcpServicesHelper().ClientRegisterChannel("ChatServer", port);
string uri = string.Format("tcp://{0}:{1}/ChatServer", url, port);
_ChatServer = (IChatServer)Activator.GetObject(typeof(IChatServer), uri);
}
public IChatServer ChatServer
{
get { return _ChatServer; }
}
}
}

Ambos os serviços consomem a classe RemotingTcpServicesHelper, responsável pelo registro do canal de comunicação TCP entre servidor e cliente. No caso, esta implementação foi definida de forma a alterar o nível de segurança da instanciação de objetos remotos para "Full" (o padrão do framework 1.1 em diante é "low", o que impossibilita a troca de objetos serializados).

Classe RemotingTcpServicesHelper
using System;
using System.Collections.Generic;
using System.Runtime.Remoting.Channels;
using System.Runtime.Serialization.Formatters;
using System.Collections;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting;
using net.jorgealbuquerque.Messager.Server;
namespace net.jorgealbuquerque.Messager.Core
{
public class RemotingTcpServicesHelper
{
public void ClientRegisterChannel(string serviceName, int port)
{
if (ChannelServices.GetChannel(serviceName) != null)
return;
IDictionary props = new Hashtable();
props["name"] = serviceName;
props["port"] = 0;
props["bindTo"] = "127.0.0.1";
props["typeFilterLevel"] = TypeFilterLevel.Full;
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
BinaryClientFormatterSinkProvider clientprovider = new BinaryClientFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
TcpChannel channel = new TcpChannel(props, clientprovider, serverProvider);
ChannelServices.RegisterChannel(channel, false);
}
public void ServerRegisterChannel(string serviceName, int port)
{
if (ChannelServices.GetChannel(serviceName) != null)
return;
IDictionary props = new Hashtable();
props["name"] = serviceName;
props["port"] = port;
props["bindTo"] = "127.0.0.1";
props["typeFilterLevel"] = TypeFilterLevel.Full;
BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
serverProvider.TypeFilterLevel = TypeFilterLevel.Full;
TcpChannel channel = new TcpChannel(props, null, serverProvider);
ChannelServices.RegisterChannel(channel, false);
}
}
}

Finalmente, encerra-se o desenvolvimento do assembly "MessagerCore", passando-se a construção da aplicação servidora pelo projeto console "MessagerServer":

MessagerServer
using System;
using System.Collections.Generic;
using net.jorgealbuquerque.Messager.Server;
namespace net.jorgealbuquerque.Messager.MessagerServer
{
class Program
{
static void Main(string[] args)
{
int port = 8080;
ServerChannelService service = new ServerChannelService(port);
Console.WriteLine("O servidor de mensagens está ativo na porta {0}", port);
Console.WriteLine("Pressine enter para parar o servidor...");
Console.ReadLine();
}
}
}

Observa-se a total abstração da lógica de negócios na camada de apresentação do servidor. Da mesma forma, apresenta-se o código fonte do único formulário (frmMain) para a aplicação cliente Windows Forms.

frmMain.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using net.jorgealbuquerque.Messager.Core;
using net.jorgealbuquerque.Messager.Client;
using System.Threading;
namespace net.jorgealbuquerque.Messager.MessagerClient
{
public partial class frmMain : Form
{
private Chatter _Chatter;
private IChatServer _ChatServer;
public frmMain()
{
InitializeComponent();
}
private void InitServer()
{
_Chatter = new Chatter(txtName.Text);
ClientChannelService service = new ClientChannelService(
txtServerHost.Text,
Int32.Parse(txtServerPort.Text));
_ChatServer = service.ChatServer;
_Chatter.IncomingMessage += new
Chatter.IncomingMessageEventHandler(Chatter_IncomingMessage);
}
private void frmMain_FormClosing(object sender, FormClosingEventArgs e)
{
try
{
if (_ChatServer != null)
_ChatServer.UnRegisterChatter(_Chatter);
}
catch
{
this.Dispose();
}
}
private void SendMessageToServer()
{
string texto = txtMessage.Text;
txtMessage.Text = string.Empty;
if (_ChatServer == null _Chatter == null string.IsNullOrEmpty(texto))
return;
_ChatServer.AddMessage(_Chatter, texto);
}
private void Chatter_IncomingMessage(string Message)
{
txtChat.Text = txtChat.Text + Message + "\r\n";
}
private void Send()
{
try
{
new Thread(new ThreadStart(this.SendMessageToServer)).Start();
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void Register()
{
if (string.IsNullOrEmpty(txtName.Text))
{
MessageBox.Show("Nome inválido!", "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
try
{
InitServer();
_ChatServer.RegisterChatter(_Chatter);
pnlLogin.Visible = false;
pnlMain.Visible = true;
this.Text = string.Format("Usuário: {0}", txtName.Text);
}
catch (System.Net.Sockets.SocketException exSocket)
{
MessageBox.Show("Servidor não encontrado: " + exSocket.ErrorCode, "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Erro", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void btnSend_Click(object sender, EventArgs e)
{
Send();
}
private void txtMessage_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == Convert.ToChar(13))
Send();
}
private void btnRegister_Click(object sender, EventArgs e)
{
Register();
}
private void txtName_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == Convert.ToChar(13))
Register();
}
}
}

O formulário foi divido em 2 panels (pnlLogin e pnlMain), responsáveis pelo login no servidor e pela interface principal do chat. Finalmente, apresenta-se o construtor da aplicação, onde é definido a flag CheckForIllegalCrossThreadCalls = false. Tal opção permite a chamada dos componentes do formulário por múltiplas threads.

Program.cs
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace net.jorgealbuquerque.Messager.MessagerClient
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
frmMain.CheckForIllegalCrossThreadCalls = false;
Application.Run(new frmMain());
}
}
}

2 comentários:

Rodrigo Macedo disse...

Meus parabéns pelo artigo. Muito interessante o seu exemplo usando o .NET Remoting
com uma aplicação de bate-papo.

Obrigado por compartilhar seus conhecimentos, excelente blog.

Abraços,
Rodrigo Macedo.

Eder disse...

Parabéns pelo artigo.
Agora cuidaddo com o bindTo,
Ele limita a criação de clientes na própia máquina.

Estou desenvolvendo uma aplicação ponto-a-ponto com .NET Remoting.

Tem idéia de como fazer broadcasting com isto?

Abração.