domingo, 3 de julho de 2011

Chat no Delphi

Olá prezados leitores,
Hoje vamos implementar um chat ou programa de bate papo. Devido ao enorme sucesso do artigo passado: (http://www.activedelphi.com.br/modules.php?op=modload&name=News&file=article&sid=249&mode=mode=thread&order=0&thold=0) resolvi lançar uma versão atual, com uma utilização prática. Escolhi um chat, pois percebi que a grande maioria dos leitores foram atrás do site buscando uma solução para fazer seus próprios MSNs da vida.
Bem, a teoria sobre Cliente e Servidor você pode ver no artigo citado acima, combinado? O único pro é que tem que ser cadastrado no site para visualizar o artigo. Quero apenar evitar que esse artigo se torne longo demais e cansativo...
Ligando os artigos
Muita coisa mudou... antes era Delphi 7 agora é XE antes eram componentes TServerSocket e TCLientSocket, mudaram os componentes mudaram os eventos, agora vamos utilizar TidTCPServer e TidTCPClient. Isso mesmo os componentes INDY.
A idéia base é a mesma os conceitos são os mesmo, o que vai mudar são os eventos que vamos utilizar, ficou um pouco mais complicado porem com mais funcionalidades, talvez você precise ler duas ou três vezes alguns trechos, é normal, principalmente se é primeira vez que estiver usando esses componentes.
O TidCPServer alem de ser um componente mais robusto tem um evento OnExecute que vai “informar” qual cliente fez algo. Já o TidTCPClient não nos da muitas pistas nem dispara evento quando algo chega. Então vamos criar um Thread simples para ficar lendo e vamos usar uma outra classe para processar as mensagens recebidas. Vamos agora dar uma pequena revisada para facilitar nossa compreensão e tirar algumas dúvidas:
Como foi explicado no artigo passado, mas é bom da um reforço aqui:
1) Todo chat é pelo menos um cliente e um servidor
2) Todo cliente se liga em um servidor de cada vez
3) Para cada TCPClient me conecto em um Servidor(TidTCPServer)
4) Um Servidor por sua vez pode ter vários clientes conectados.
5) Para um cliente enviar informações para outro cliente a menssagem vai necessariamente passar pelo servidor.
Acho que essas nossas principais “Leis” então, entendido isso vamos em frente....

PRIMEIRO O SERVIDOR
Vamos primeiro ao servidor pois requer um pouco mais de cuidado e atenção. Seu layout de tela é muito simples ele não vai ter interação com o usuário praticamente, salvo um botão para ligar e desligar o cliente. E um memo que vai exibir todo o trafego no servidor. Nossa tela então ficou assim:
Observe que vou mudar o nome de alguns componentes bem como do Form. Observe também que foi incluído o Componente TidTCPServer.

Codificando o servidor
Vamos criar uma pequena estrutura para guardar algumas informações dos nossos clientes, como IP, Host, HoraConexao, e a Conexão dele. Segue abaixo minha estrutura, pode ser uma classe também, vai mudar um pouco as coisas, mas pode ser feito. Mas a minha ficou assim:
Type
TCliente = Record
IP : string[15];
HostName : string[40];
ThreadID: Cardinal;
Connection: TidTCPConnection;
end;
PCLiente = ^TCliente;
Atenção: Vc deve declarar no USES IdTCPConnection para a classe TidTCPConnection possa ser entendida pelo compilador.

Minha idéia é guardar algumas informações do cliente quanto conectar no caso ai está apenas o IP através do IP iremos pegar o Host, vamos ver mais a frente o ID do Thread não precisamos saber muito disso agora, e o mais importante é o Connection que é por onde vamos nos comunicar com os clientes.
Agora vamos criar uma Lista de conexões isso é familiar para você, se tiver lido o artigo SOCKETS que foi citado no inicio deste artigo. =) Veja como ficou:



A única configuração que temos que fazer no idTCPServer é sua propriedade DefaultPort, que como o nome já disse é a porta padrão, lembra do socket? IP e porta? É isso ai... escolha uma porta que mais lhe agrada no meu caso usei a 1212.


Meu numero da sorte =) Escolha um numero qualquer para sua porta o importante é que a mesma porta do servidor seja a mesma do cliente e que não esteja sendo usando por outra aplicação.
CODIFICANDO OS EVENTOS
Form
OnCreate
procedure TFServidor.FormCreate(Sender: TObject);begin FConexoes := TList.Create;end;
OnDestroy
procedure TFServidor.FormDestroy(Sender: TObject);begin DesconectarTodos; FConexoes.Free;end;
Botões
Ativar: onClick
procedure TFServidor.Button1Click(Sender: TObject);begin IdTCPServer1.Active := True;end;
Desativar: onClick
procedure TFServidor.Button2Click(Sender: TObject);begin if IdTCPServer1.Active then IdTCPServer1.Active := False;end;
TidTCPServer
OnConnect
procedure TFServidor.IdTCPServer1Connect(AContext: TIdContext);var ClienteNovo : PCLiente;begin GetMem(ClienteNovo,Sizeof(TCliente)); ClienteNovo.IP := AContext.Connection.Socket.Binding.PeerIP; ClienteNovo.HostName := GStack.HostByAddress(ClienteNovo.IP); ClienteNovo.Connection := AContext.Connection; AContext.Data := TObject(ClienteNovo); FConexoes.Add(ClienteNovo); // Adicionando na listaend;
Aqui para utilizar o GStack é preciso adicionar no uses IdStack.
OnDisconnect
procedure TFServidor.IdTCPServer1Disconnect(AContext: TIdContext);Var Cliente : PCliente;begin Cliente := PCliente(AContext.Data); FConexoes.Remove(Cliente); AContext.Data := nil; FreeMem(Cliente,SizeOf(TCliente));end;
OnExecute
procedure TFServidor.IdTCPServer1Execute(AContext: TIdContext);var Cliente : PCliente; Msg: string; i :integer;begin Cliente := PCliente(AContext.Data); Msg := '<' + Cliente.IP + '>:'; Msg := Msg + Cliente.Connection.IOHandler.ReadLn; memolog.Lines.Add(Msg); BroadcastMsg(Msg);end;
A idéia aqui é redirecionar o que chegou para todos os outros clientes conectados, ou seja que estão na nossa lista FConexoes; Observer que tem no final uma procedure chamda de Broadcast(); Ela vai fazer o trabalho de percorrer nossa lista e enviar a variável Msg para todos os clientes. Segue o código:
procedure TFServidor.BroadcastMsg(_msg: string);var i : integer; Cliente : PCLiente;begin for I := 0 to FConexoes.Count-1 do begin Cliente := PCliente(FConexoes.Items[i]); Cliente.Connection.IOHandler.WriteLn(_msg); end;end;
Servidor está pronto =) Escolha a porta a seu gosto é importante rodar talvez o windows peça para você desbloquear sempre que mudar de porta, certifique-se que a porta escolhida não está sendo utilizada por outro programa.
Codificando o Cliente
Bem a interface de todo chat é praticamente a mesma, segue a nossa:


É um TMemo em cima um TEdit em baixo e um botão enviar, deixei os nomes padrões para você identificar facilmente cada um. O importante ai é ver que circulei na imagem para destacar que já deixei configurado a dupla IP e Porta =) Para quem não sabe o IP 127.0.0.1 é o ip da nossa própria maquina independente do ip real dele, pode ate ser que nem ip tenha, o ip 127.0.0.1 foi criado para testes mesmo é o mesmo que localhost.
Agora vamos codificar nosso cliente... ta perto do fim já =) Devemos iniciar declarando essas duas clases
type
TOnReceiverThread = class (TThread)protected FConexao : TIdTCPConnection; procedure Execute; override;
public constructor Create(_Conexao: TIdTCPConnection); reintroduce;
end;
TOnProcessa = class(TIdSync)protected FMsg: string; procedure DoSynchronize; override;
public constructor Create(const _Msg:string); class procedure AddMsg(const _Msg:string);
end;
Não se esqueça do IdTCPConnection no USES;
A primeira classe é um Thread se você não sabe o que um Thread recomendo que leia algo sobre isso urgente. Mas entenda que é como disparar um processo paralelo em nossa aplicação. Então ele vai ficar executando o comando IOHandler.ReadLn durante todo tempo O FConexao é nosso componente IdTCPClient;
A segunda classe é que vai processar, estou usando um artifício bastente interessante dentro da minha classe crio uma class procedure e dentro dela disparo a criação da classe e na criação da classe preencho a variável FMsg, já e na procedure DoSynchronize faço a ação de pegar o que está no FMsg e adcionar o memo. Segue o código:
{ TOnReceiverThread }
constructor TOnReceiverThread.Create(_Conexao: TIdTCPConnection);begin FConexao := _Conexao; inherited Create(False);end;
procedure TOnReceiverThread.Execute;begin while not Terminated and FConexao.Connected do begin TOnProcessa.AddMsg(FConexao.IOHandler.ReadLn); end; inherited;end;
{ TOnProcessa }
class procedure TOnProcessa.AddMsg(const _Msg: string);begin with Create(_Msg) do try Synchronize; finally Free; end;end;
constructor TOnProcessa.Create(const _Msg: string);begin FMsg := _Msg; inherited Create;end;
procedure TOnProcessa.DoSynchronize;begin inherited; Form2.Memo1.Lines.Add(FMsg);end;
Declare uma variável com seu thread o meu ficou assim
var FCliente: TFCliente; Receiver : TOnReceiverThread; // Aqui meu thread
Acredito que essa seja a parte mais complicada, por isso analise com calma se for preciso releia e observe as ligações meu thread que chamei de RECEIVER vai ser criando no evento OnConnected que veremos a seguir, depois ele vai agir sozinho e dentro no Execute ele chama a class procedure do TOnProcessa;
Agora os eventos....Primeiro o botão enviar:
procedure TFCliente.BitBtn1Click(Sender: TObject);begin IdTCPClient1.IOHandler.WriteLn(edit1.Text);end;

Ao clicar envia para o servidor o que estiver no Edit.
onCreate do Form
procedure TFCliente.FormCreate(Sender: TObject);begin IdTCPClient1.Connect;end;
onConnected e OnDisconnected
procedure TFCliente.IdTCPClient1Connected(Sender: TObject);begin Receiver := TOnReceiverThread.Create(IdTCPClient1);end;
procedure TFCliente.IdTCPClient1Disconnected(Sender: TObject);begin if Receiver <> nil then begin Receiver.Terminate; Receiver.WaitFor; FreeAndNil(Receiver); end;end;
Naturamente o cliente deve ser iniciado após o servidor estar ativo. Caso contrario ele vai de caro da um erro e informar um numero (um código de erro) esse código é padrão para toda aplicação com sockets por exemplo 10061 = Não foi possível conectar. Provavelmente servidor não está rodando, se tiver pode ser a porta diferente da sua ou ainda, está rodando mas não está ativo. Esses são os 3 problemas mais comuns. O outro é colocar um IP não valido para cliente se conectar, para testar se o ip é valido para conectar é o so dar o vamos comando PING no prompt se houver resposta positiva, verifique as portas.
Na internet você acha fácil um lista com esses erros. Em uma busca muito rápida encontrei esse link: http://claheadshot.forumeiro.com/t626-lista-de-erros-de-socket-e-suas-causas
Se o link estiver quebrado não desespere é realmente muito fácil encontrar basta buscar “lista de erros sockets” há muito mais material.
CONSIDERAÇÕES FINAIS
Há muita coisa para implementar, =) Nick name, logs, enviar arquivos, trocar o memo por um report mais interativo que suporte imagem e formatação de textos como negrito, quem souber de um bom componente me avise. Já no lado servidor, podemos colocar regras para banir ips, ir salvando o log em arquivo txt, alem de implementar um protocolo que permita mensagens privadas e muitas outras coisas... Mande sua idéia, vou criar outros artigos apartir deste no futuro, aqui foi o ponta pé inicial =) Acredito que tudo ficou claro na codificação, se necessário envio os fontes. Basta mandar um e-mail para jnelson3@ig.com solicitando.
Deste já agradeço por ter lido, espero que tenha gostado um abraço e ate o próximo