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

24 comentários:

  1. Fico bastante feliz que tenha gostado. Aproveite e forte abraço.

    ResponderExcluir
  2. falew pelo artigo me ajudou muito...

    e ira ajudar muitas pessoas....

    obrigado pela contribuição...

    ResponderExcluir
  3. Essa é minha intenção, contribuir com o desenvolvimento tecnológico no Brasil.

    ResponderExcluir
  4. nelson onde baixo este seu exemplo, nao vi nenhum link em sua página

    ResponderExcluir
    Respostas
    1. Realmente não coloquei um link com o exemplo. Me manda um e-mail(jnelson3@ig.com.br) que envio para você. Os próximos irão com link.

      Excluir
  5. Caro Nelson, parabéns pelo artigo! Existe um link para baixar o exemplo? Obrigado!

    ResponderExcluir
  6. Realmente não coloquei um link com o exemplo. Me manda um e-mail(jnelson3@ig.com.br) que envio para você. Os próximos irão com link.

    ResponderExcluir
  7. Este comentário foi removido pelo autor.

    ResponderExcluir
  8. Primeiramente quero agradecer muito, acho difícil achar bons artigos sobre delphi em português hoje em dia.
    Fiz tudo conforme você explicou, e tudo funcionou bem.
    Tenho só duas ressalvas que não diminuem em nada o que você fez e não afetam o funcionamento do cerne.
    1. Não achei a procedure DesconectarTodos;
    2. Acho que na próxima vez se você usar um css próprio para o código vai ficar muito melhor. Abaixo segue um modelo
    http://www.mundoblogger.com.br/2010/10/caixa-exibir-codigos-no-post.html

    Sei que delphi é uma linguagem muito boa, se usada corretamente, tenho refletido muito se houvesse um documento para direcionar a embarcadero no nosso mercado, como por exemplo foco em ensino acadêmico(acho que java está dominando o mercado muito por isso), uso intensivo de design patterns, foco em oo, melhor direcionamento no mundo web (não adianta ficar fazendo pra mil plataformas), melhoria nas pesquisas do índice TIOBE... resumindo acho que eles tem condições de fazer a linguagem voltar o problema é enxergar o óbvio.

    ResponderExcluir
    Respostas
    1. João, obrigado pelo seu comentário. Realmente esqueci de colocar a procedure DesconectarTodos; =/ Mas tenho o fonte disponível caso queira.

      Quanto há um CSS proprio. Eu concordo o problema é como fazer isso, não tenho estudado muito a formatação do blog, se tiver um exemplo ou tutorial favor passar, será de grande valia pra mim.

      E sobre o Delphi o futuro, as liguagens, a Embarcadero está se posicionando no mercado bem, é preciso avançar mais, mas o q temos hj é uma ferramenta atual. Acredito q a questão dos componentes pode melhorar, ela deve fazer semelhante ao q a Apple fez com os apps. Creio q ela deve baixar o valor do produto q criar um canal entre o desenvolvedor e os componentes q vc pode publicar grátis ou cobrar.

      Eu desenvolvi um FrameWork chamado de "J3" q sua principal virtude é incorporar as 3 plataformas moblie, desktop e web, cada uma com suas virtudes. Entra em contato se tiver interesse. Sem mais abraços. =)

      Excluir
    2. Existe uma solução rápida e simples para formatar código, acesse o link:
      http://codeformatter.blogspot.com.br/

      1. Copie o código do delphi para o campo "Paste Here Your Source Code"
      2. Clique no botão "Format Source Code"
      3. Cole o código gerado no seu post

      Ele mantém o código com a identação e o espaçamento igual de caracteres

      Agora se você quiser aprofundar nesse a assunto, eu indico dar uma olhada nesse link aqui
      http://alexgorbatchev.com/SyntaxHighlighter/

      Nesse método ele também se preocupa com o destaque da sintaxe da linguagem e seus comandos, Delphi é suportado, segue no link abaixo

      http://alexgorbatchev.com/SyntaxHighlighter/manual/brushes/delphi.html


      Quanto ao Chat, eu codifiquei tudo aqui, baseado no seu post e tudo funcionou bem, acho que só faltou o conteúdo da DesconectarTodos;
      Se você quiser passar o conteúdo dessa procedure aqui no comentário é melhor pois assim compartilhamos.


      Em relação ao Framework J3, eu procurei no seu blog mas só achei falando de um componente que melhora o FortesReport, não do framework em si.
      Se tiver algum material de leitura para eu me inteirar do assunto

      Excluir
    3. Boas dicas irei adotar para os próximos.

      Quando a procedure segue:
      procedure TFServidor.DesconectarTodos;
      var
      Cliente : PCLiente;
      begin
      while (FConexoes.Count > 0) do
      begin
      Cliente := PCliente(FConexoes.Items[0]);
      Cliente.Connection.Disconnect;
      end;
      end;

      E ao FrameWork não tem referencia, pois ainda estou escrevendo e fico esperando a maturidade ideal, mas creio q isso vai ficar difícil de acontecer quero, lançar para a comunidade e preciso de ideais para colaborar com o projeto. Em breve estarei lançando ele no mercado. Talvez no Google Code. Abraços.

      Excluir
  9. Este comentário foi removido pelo autor.

    ResponderExcluir
  10. Olá Nelson... eu novamente. Estou tentando desenvolver aqui o chat e estou criando a opção do usuário escolher em uma lista para quem ele quer mandar a mensagem, até ai tudo bem, mesmo as Threads aqui gerando um código igual(isso é estranho) quando varro a lista FConexoes eu gero um Guid que identifica o chat, o contador dela está certo. Então eu gostaria de atualizar a lista dos clientes quando o contador de conexões for alterado. Até ai tudo bem, isso você mesmo passou.

    Minha Dúvida: Estou querendo enviar uma mensagem com a lista atualizada de clientes para meus clientes ativos a partir do servidor. Ou seja sem que a mensagem venha do cliente.Para quando o contador de conexões alterar, o servidor envia a mensagem de quem está ativo sem o cliente requisitar, você tem uma ideia sobre isso? Log, Melhorias isso ai acredito que vai correr bem, mas essa parte está um gargalo. Obrigado.

    ResponderExcluir
  11. Maravilha! parabéns! rodei o mundo virtual atras de uma solução igual a sua e não encontrei... com esse código resolvo algo muito maior do que um simples chat. Obrigado por vc existir... rs

    ResponderExcluir
  12. Amigo comigo não deu certo. Quando coloco esse codigo esta dando erro:

    Type
    TCliente = Record
    IP : string[15];
    HostName : string[40];
    ThreadID: Cardinal;
    Connection: TidTCPConnection;
    end;
    PCLiente = ^TCliente;

    ResponderExcluir
  13. Onde vai isso:
    { 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;

    ResponderExcluir
  14. Esse código vai aonde?

    Type
    TCliente = Record
    IP : string[15];
    HostName : string[40];
    ThreadID: Cardinal;
    Connection: TidTCPConnection;
    end;
    PCLiente = ^TCliente;

    Grato

    ResponderExcluir
  15. Boa tarde. Parabéns.
    O que seria "TIdSync" está dando erro e não sei como resolver, pode ajudar?

    ResponderExcluir
  16. Boa tarde
    Parabéns pelo artigo.
    Implementei seu exemplo e rodou perfeitamente.
    Coloquei o servidor nas nuvens usando a porta 1212, mas em alguns computadores de cliente (windows 7) ele não conecta retornando o erro "Connection closed gracefully", já desativei o fireall e antivirus e mesmo assim não conecta.

    Isso já aconteceu com você? e como posso resolver esse problema?

    Obrigado.

    ResponderExcluir
  17. Bom dia show, mas tem como mandar o codigo fonte professorfribel@gmail.com

    ResponderExcluir
  18. Top! Obrigado. Ajudou muiito, Vlw pra montar um projetinho..

    ResponderExcluir