Esse artigo faz parte da revista Clube Delphi Edição 102. Clique aqui para ler todos os artigos desta edição

Win32

Suporte técnico on-line via chat

Faça você mesmo um aplicativo de Chat usando Delphi

Neste artigo veremos

·         Como utilizar a suíte de componentes Indy;

·         Como criar um protocolo de comunicação;

·         Como utilizar o protocolo criado em um aplicativo de troca de mensagens instantâneas.

Qual a finalidade?

·         Desenvolvimento de um servidor de mensagens no estilo MSN.

Quais situações utilizam esses recursos?

·         Em sistemas que necessitam trocar informações através de uma rede TCP/IP.

 

Resumo do DevMan

         Manter uma forma de comunicação barata e eficiente com o cliente é essencial. E se esse meio de comunicação puder ser personalizado para sua empresa, melhor ainda. Aumentar a comunicação do cliente com o suporte técnico e ter essa preocupação com o bom atendimento, sem dúvida é o diferencial de qualquer empresa. Hoje o mercado tem usado freqüentemente o Messenger da Microsoft para manter um bom relacionamento com seus clientes, porém muitas empresa temem o uso indiscriminado da ferramenta, além é claro do risco de vírus na rede.

         Neste artigo um serviço de troca de mensagens instantâneas será desenvolvido e personalizado de acordo com as nossas necessidades. Criaremos um pequeno protocolo para troca de mensagens e um servidor para distribuir o fluxo de comandos.

 

Acredito que a uma boa parte de nós, programadores que precisam manter contato com seus clientes, utilizam o MSN para prestar suporte técnico. Porém existem alguns clientes que não permitem o uso de aplicativos de mensagem instantânea com medo de que sua rede seja infectada por vírus ou até mesmo por achar que os funcionários ficarão batendo papo o dia todo. Como resolver esse impasse? É aí que o Delphi vem em nosso socorro. Por que não criar um serviço de chat, estilo MSN, só que personalizado para sua empresa? O cliente com esse chat instalado não veria outra coisa a não ser a disponibilidade do suporte e você poderia ver todos os clientes que estão on-line. Com certeza ao mostrar esse aplicativo a esse cliente ele iria se sentir seguro, pois o serviço de comunicação estará com o logo da sua empresa e apenas o setor de suporte estará disponível. Vamos por a mão na massa e agregar valor aos nossos negócios!

Uma visão geral

Nosso DevChat, nome que daremos a nossa aplicação, será composto de 3 partes: um server e dois clients. O server será responsável por manter os clients cadastrados e seu estado. Também caberá a ele direcionar a comunicação entre os clients. Isso significa que para um client enviar uma mensagem de texto para outro, essa deverá ser enviada ao server e então será encaminhada para o destinatário (Figura 1) e para isso o client deve se conectar ao Server por meio da Internet.

 

Figura 1. Interação entre clients e server

Os outros dois clients são muito parecidos. Um será destinado aos clientes e a ele só será permitido enxergar a equipe de suporte. O outro será utilizado pelo suporte e nele estarão sendo exibidos todos os clientes.

Mas o que esperar do nosso DevChat ? Quero implementar junto com vocês as seguintes funcionalidades:

·         Troca de mensagens, formato texto simples: Quem nunca sentiu dificuldade em ler certos diálogos no MSN onde a cada palavra que a pessoa digita do outro lado forma um gif animado pra você? Nosso chat será uma ferramenta de suporte da sua empresa, por isso, isso será cortado.

·         Notificação de mudança de estado: Será possível identificar quem está on-line ou não.

Acredito que com esses requisitos estamos cobrindo o que há de mais básico em termos de comunicação textual on-line.

Construindo o servidor

Como já mencionado, o server deve manter os clients cadastrados.  Para isso vou utilizar um banco de dados Firebird. Na Figura 2 vemos as tabelas que o compõem. Além disso, também é sua responsabilidade validar quem está tentando se conectar. Para que o exemplo faça sentido, crie um banco de dados com o nome que desejar, aqui criamos com o nome DevChat.fdb, e nele crie as tabelas, chaves primárias e estrangeiras conforme o esquema da Figura 2. Não abordarei a criação do banco nesse artigo por não fazer parte do tema.

 

Nota: Não cobrirei aqui a forma que esses clients serão cadastrados, por isso vou inserir alguns registros no banco de dados de forma manual. Seria importante disponibilizar, por exemplo, uma página onde seus clientes pudessem baixar o client e registrar um usuário e senha.

 

Figura 2. Diagrama do banco de dados DevChat.fdb

A comunicação realizada em um aplicativo de Chat se dá através de uma rede TCP/IP, ou seja, para criarmos nosso servidor ele deve ser capaz de utilizar TCP/IP para enviar e receber mensagens. Como fazer isso com Delphi? Quando você instala o Delphi um dos pacotes de componentes que é adicionado é o Indy. Ele é composto por um conjunto de componentes específicos para trabalhar com redes e oferece os mais variados recursos. Neste artigo vamos utilizar a versão disponibilizada junto com o RAD Studio 2007. Portanto crie um novo projeto no RAD Studio e salve-o como DevChatServer.dproj e deixe-o como na Figura 3.

Figura 3. Exemplo de tela do DevChat-Server

Adicione também um Data Module ao projeto e conecte-se ao banco de dados, como na Figura 4. Os componentes qryEmpresa, qryUsuario, qrySuporte simplesmente acessam suas respectivas tabelas e juntamente com os controles de update (upUsuario e upSuporte) permitem que sejam atualizadas informações.

 

Nota: Para conexão com o banco de dados, estamos usando os componentes na paleta Interbase, tais como IBDataBase, IBQuery, IBTransation e IBUpdateSQL. Porém, nada impede que façamos tais conexões utilizando outros componentes de acesso a dados como o dbExpress, por exemplo.


Figura 4. Data Module para conexão ao banco

Ao formulário principal adicione um componente idTCPServer que está localizado na guia Indy Servers. É este componente que pode tornar nosso aplicativo um servidor. Nele configuramos qual porta o DevChat irá utilizar para escutar os clients que desejam se conectar. Para ativar o servidor vamos colocar no evento OnClick do botão Iniciar o código da Listagem 1.

Listagem 1. Iniciando o servidor

procedure TfrmPrincipal.btnIniciarClick(Sender: TObject);

begin

  if btIniciar.Caption = 'Iniciar' then

  begin

    tcpServer.DefaultPort := strtoInt(editPorta.Text);

    tcpServer.Active := true;

    dtm.qryUsuario.Close;

    dtm.qryUsuario.SQL.Clear;

    dtm.qryUsuario.SQL.Text := 'UPDATE USUARIO SET ONLINE = 0, STATUS = 0';

    dtm.qryUsuario.ExecSQL;

    dtm.qryUsuario.Transaction.CommitRetaining;

    dtm.qryUsuario.Close;

    dtm.qrySuporte.Close;

    dtm.qrySuporte.SQL.Clear;

    dtm.qrySuporte.SQL.Text := 'UPDATE SUPORTE SET ONLINE = 0, STATUS = 0';

    dtm.qrySuporte.ExecSQL;

    dtm.qrySuporte.Transaction.CommitRetaining;

    dtm.qrySuporte.Close;

    btIniciar.Caption := 'Parar';

  end

  else

  begin

    tcpServer.Active := false;

    btIniciar.Caption := 'Iniciar'

  end;

  Total := 0;

end;

Se executarmos o programa e clicarmos sobre o botão Iniciar é provável que o firewall do seu sistema operacional seja acionado. Isso acontece porque estamos abrindo uma porta de conexão. No caso do Windows XP, basta confirmar e tudo estará funcionando. Caso o exista algum firewall no seu sistema operacional que não seja o do próprio Windows, talvez seja necessário dar permissão para o sistema que acabamos de desenvolver.

Na Listagem 1 estamos passando, através da propriedade DefaultPort, para o componente tcpServer qual porta será utilizada para manter uma comunicação e então o ativamos. Após isso, ajustamos todos os usuários a terem seu status como off-line apenas fazendo um Update nas tabelas Usuario e Suporte. Isso é necessário, já que em teoria nenhum usuário ou suporte técnico poderia estar on-line com o servidor off-line.

Como dito anteriormente, o servidor ficará escutado as requisições das outras duas aplicações e fará o redirecionamento das mensagens de um para outro. Por isso, precisaremos prever que tipos de mensagem e como elas serão interpretadas no lado servidor.

O que precisamos fazer agora é programar o servidor para que ele seja capaz de interpretar o que recebe e conseqüentemente distribuir as mensagens e comandos recebidos. Usaremos o evento OnExecute do componente TIdTCPServer. Clique duas vezes sobre esse conforme visto na Listagem 2.

Nessa Listagem 2, recebemos o valor passado pelo client através do parâmetro do evento. Em seguida fazemos um copy na variável Texto para que possamos saber qual comando ou mensagem foi passado. Caso a mensagem seja listaSuporte chamamos o método GetUsuariosSuporte. Você precisará criar essa função conforme a Listagem 3. Veja que a tarefa é simples, apenas conectamos ao banco de dados, executamos uma instrução SQL para retornar todos os membros do suporte técnico e carregamos em um TStringList que é passado como retorno.

Listagem 2. Processando o comando de lista de suporte

procedure TServerF.tcpServerExecute(AContext: TIdContext);

var

  comando: string;

  Texto: string;

  ListaSuporte: TStringList;

begin

  Texto:= AContext.Connection.IOHandler.ReadLn;

  comando := Copy(Texto, 1, Pos('||', Texto)-1);

  if comando = 'listaSuporte' then

  begin

    ListaSuporte := GetUsuariosSuporte;

    AContext.Connection.IOHandler.WriteRFCStrings(ListaSuporte);

  end;

end;

Listagem 3. Obtendo a lista de suporte

function TServerF.GetUsuariosSuporte: TStringList;

var

 ListaSuporte: TStringList;

begin

  dtm.qrySuporte.Close;

  dtm.qrySuporte.SQL.Clear;

  dtm.qrySuporte.SQL.Add('SELECT * FROM SUPORTE');

  dtm.qrySuporte.Open;

  ListaSuporte := TStringList.Create;

  while not dtm.qrySuporte.Eof do

  begin

    ListaSuporte.Append('listaSuporte||' +

      dtm.qrySuporteID_SUPORTE.asString + '||' +

      dtm.qrySuporteAPELIDO.AsString + '||'+

      dtm.qrySuporteONLINE.AsString + '||'+

      dtm.qrySuporteSTATUS.AsString+ '||');

    dtm.qrySuporte.Next;

  end;

  dtm.qrySuporte.Close;

  result := ListaSuporte;

end;

Mais tarde veremos que a lista retornada será utilizada para armazenar esses integrantes do suporte na versão client e para isso vamos utilizar o que o Delphi nos oferece de melhor, um ClientDataSet.

Mas antes disso veremos outro ponto importantíssimo em nossa solução. Para que as três aplicações conversem entre si, vamos estabelecer um protocolo. Definiremos algumas regras e comandos que farão com que os três aplicativos enviem e recebam mensagens e comandos entre si.

No servidor é preciso agora verificar se o usuário que está tentando se conectar é válido. Toda vez que algum usuário tenta se conectar utilizando o host e porta específicos, o evento onConnect do TcpServer é disparado e é nele que vamos verificar as informações que estão chegando. Programe o evento OnConnect mencionado anteriormente conforme a Listagem 4.

Listagem 4. Validando a conexão

procedure TServerF.tcpServerConnect(AContext: TIdContext);

var

  cliente: TUsuario;

  loginInfo: TLoginInfo;

  IdUsuario: integer;

  texto, comando: string;

begin

  texto := AContext.Connection.IOHandler.ReadLn;

  comando := Copy(texto,1,pos('||',texto)-1);

  delete(texto,1,pos('||',texto)+1);

  loginInfo := TLoginInfo.Create;

  loginInfo.IdEmpresa := strToInt(Copy(texto,1,pos('||',texto)-1));

  delete(texto,1,pos('||',texto)+1);

  loginInfo.Nick := Copy(texto,1,pos('||',texto)-1);

  delete(texto,1,pos('||',texto)+1);

  loginInfo.Senha := Copy(texto,1,pos('||',texto)-1);

  idUsuario := Login(loginInfo.IdEmpresa, loginInfo.Nick, loginInfo.Senha);

  if idUsuario > 0  then

  begin

    cliente := TUsuario.Create;

    cliente := LoadUsuario(idUsuario);

    cliente.IP := AContext.Connection.Socket.Binding.PeerIP;

    cliente.Host := GStack.HostByAddress(cliente.IP);

    cliente.Online := oSim;

    cliente.Status := sDisponivel;

    AContext.Data := cliente;

    AContext.Connection.IOHandler.WriteLn('sucessoLogin||'

      + IntToStr(cliente.IdUsuario)+ '||');

   Total := Total + 1;

  end

  else

  begin

    AContext.Connection.IOHandler.WriteLn('sucessoLogin||'

      + IntToStr(idUsuario)+'||');

    Acontext.Connection.Disconnect;

  end;

  loginInfo.Free;

end;

 

Vamos entender a Listagem 4. Observe que o evento tcpServerConnect carrega consigo um parâmetro do tipo TIdContext, o AContext. Para cada client que se conecta, ou tenta se conectar, um novo TIdContext é criado o representando. A variável AContext contém informações importantes, como por exemplo a conexão atual, e através dela podemos obter os comandos que o client está enviando.  Isso é feito na linha a seguir:

 

texto := AContext.Connection.IOHandler.ReadLn;

 

Perceba que em dado momento, recebemos o ID do usuário na variável idUsuario. Essa variável recebe a identificação do usuário através da função Login.

 

  idUsuario := Login(loginInfo.IdEmpresa, loginInfo.Nick, loginInfo.Senha);

 

Veja que desmembramos o conteúdo da variável texto em um objeto da classe TLoginInfo e passamos o resultado para o método Login, que por sua vez retorna o Id do usuário, 0 se o login não for válido ou ainda -1 se o usuário já estiver conectado. Caso o usuário seja válido, um objeto do tipo TUsuario é instanciado e passado para a propriedade Data do objeto AContext atual, então enviamos o retorno disso de volta para o client, que digo novamente é representado pelo objeto AContext. Atualizamos a quantidade de usuários conectados no server.

 

Nota: A definição da classe TLoginInfo e TUsuario é feita na Unit ClientesU e por questões de espaço não é exibida aqui mas está disponível para download.

 

Crie uma nova função no Server para que possamos fazer esse trabalho. Veja seu código na Listagem 5. Na Listagem 5 vemos como que um usuário é validado. Criamos uma function chamada Login que é chamada de dentro do evento OnConnect retornando o ID do usuário. A validação é ainda mais simples do que vimos até agora. Apenas pegamos a Empresa, Apelido e Senha do usuário e efetuamos consultas ao banco de dados.

Listagem 5. Verificando o usuário junto ao banco de dados

function TServerF.Login(IdEmpresa: integer; Nick, Senha: string): Integer;

begin

  dtm.qryUsuario.Close;

  dtm.qryUsuario.SQL.Clear;

  dtm.qryUsuario.SQL.Add('SELECT * FROM USUARIO');

  dtm.qryUsuario.SQL.Add('WHERE ID_EMPRESA = :ID_EMPRESA');

  dtm.qryUsuario.SQL.Add('AND APELIDO = :NICK');

  dtm.qryUsuario.SQL.Add('AND SENHA = :SENHA');

  dtm.qryUsuario.ParamByName('NICK').AsString := Nick;

  dtm.qryUsuario.ParamByName('ID_EMPRESA').AsInteger := IdEmpresa;

  dtm.qryUsuario.ParamByName('SENHA').AsString := Senha;

  dtm.qryUsuario.Open;

  if dtm.qryUsuario.IsEmpty then

    result := 0

  else

  begin

    if dtm.qryUsuarioONLINE.AsInteger = 1 then

      result := -1

    else

    begin

      result := dtm.qryUsuarioID_USUARIO.AsInteger;

      dtm.qryUsuario.Edit;

      dtm.qryUsuarioONLINE.AsInteger := 1;

      dtm.qryUsuarioSTATUS.AsInteger := 1;

      dtm.qryUsuario.Post;

      dtm.qryUsuario.Transaction.CommitRetaining;

    end;

  end;

  dtm.qryUsuario.Close;

end;

Definindo um protocolo

Como o client precisa se comunicar com o servidor é necessário que criemos um protocolo de comunicação que seja obedecido e conhecido pelas partes envolvidas. Um protocolo de comunicação é um conjunto de comandos e valores que são formatados em um padrão pré-determinado. Nosso protocolo será simples, baseado em texto simples e cobrirá as funcionalidades citadas anteriormente. Veja que nas Tabelas 1 e 2 especificamos os o comandos válidos que serão trocados entre aplicação cliente e servidora, respectivamente.

Funcionalidade

Protocolo

Login

login||IdEmpresa||<Nick>||senha||

Obter lista dos usuários de suporte

listaSuporte||IdUsuario||

Enviar mensagem para alguém do suporte

mensagemParaSuporte||IdUsuarioOrigem||IdSuporte||mensagem||NumeroLinha

Estou escrevendo

Escrevendo||IdUsuarioOrigem||IdUsuarioDestino||

Parei de escrever

pareiEscrever||IdUsuarioOrigem||IdUsuarioDestino||

Mudança de estado

mudancaEstado||IdUsuario||Estado||OnLine||

Obter a lista dos clientes cadastrados e seus status

listaCliente||<IdSuporte>||

Login do client de suporte

loginSuporte||<Nick>||senha||

Mensagem para algum cliente

mensagemParaCliente||IdSuporteOrigem||IdCliente||mensagem||NumeroLinha

Tabela 1. Comandos do protocolo

 

Funcionalidade

Protocolo

Retornar a lista de suporte

Um stringlist onde cada item obedece:

listaSuporte||idSuporte||Nick||OnLine||estado||

Sucesso/Falha de conexão/login

sucessoLogin||IdCliente||

Tabela 2. Comandos de retorno do server

Basicamente nosso protocolo é composto por um comando e seus parâmetros, todos em um texto. Para que possamos distinguir o que é cada valor, criamos um separador para nos ajudar. Nosso separador é composto por dois pipe-lines, ||, colocados em seqüência. Para que possamos ver o protocolo em funcionamento precisamos implementar tanto o client quanto o server em paralelo. Já vimos um exemplo de uso do protocolo na Listagem 2. Veja:

 

  if comando = 'listaSuporte' then

  begin

    ListaSuporte := GetUsuariosSuporte;

    AContext.Connection.IOHandler.WriteRFCStrings(ListaSuporte);

  end;

 

Aqui estamos verificando se o comando recebido é a palavra listaSuporte e caso seja, carregamos os usuários como já vimos anteriormente.

Criando a aplicação cliente

Teremos dois clients. Um será utilizado por nossos clientes e outro pela equipe de suporte que terá funcionalidades especiais como veremos mais adiante. Vamos começar pelo dos clientes. Adicione à solução um novo projeto, do tipo Win32, chamando-a de DevChatClient.dproj. No formulário principal vamos adicionar um TIdTCPClient da guia Indy Clients e configurar suas propriedades Port e Host para que sejam as mesmas utilizadas pelo servidor de mensagens. Em Host colocamos o IP da máquina onde está o servidor de mensagens, que neste caso é 127.0.0.1 ou localhost. Adicione agora um painel e nele insira três TEdit e um TButton, como na Figura 5.

No OnClick do botão tentamos conectar ao servidor de mensagens, como é mostrado na Listagem 6.

Figura 5. Tela inicial do Client

Listagem 6. Conectando no servidor

procedure TClientF.btConectarClick(Sender: TObject);

begin

  if (Trim(EditEmpresa.Text) <> '') and

    (Trim(EditLogin.Text) <> '') and

    (Trim(EditSenha.Text) <> '') then

  begin

    tcClient.Connect;

    tcClient.Socket.WriteLn('login||'+ EditEmpresa.Text +

      '||' + EditLogin.Text + '||'+ EditSenha.Text+'||');

  end

  else

  begin

    ShowMessage('Preencha todas as informações para Login');

    EditEmpresa.SetFocus;

  end;

end;

Perceba que o código não é em nada complexo. Estamos verificando se os campos Empresa, Login e Senha estão preenchidos e em caso positivo chamamos o método Connect do componente TCPClient. Esse método se encarregará de conectar-se à aplicação server que deverá estar em execução. Após isso, enviamos a mensagem login||NomeDaEmpresa||Login||Senha|| (Veja a utilização do nosso protocolo novamente). Essas informações fazem parte do protocolo que criamos, ou seja, nada mais são do que um simples texto que será recebido na aplicação Server e conseqüentemente validado.

Depois de conectado ao servidor, nossa aplicação deverá listar os membros da equipe de suporte e verificar o status de cada um. Lembre-se qie para isso programamos no Server uma função capaz de nos retornar tal informação. O retorno da função é um TStringList, ou seja, uma lista de strings. Como já foi dito, usaremos um ClientDataset para preencher com esse membros.  

Adicione então um ClientDataSet no formulário principal do Client e adicione os campos que representam um usuário do suporte: IdSuporte, Nick, OnLine e Status. Adicione também um DataSource e ligue-o ao ClientDataSet. Agora adicione um DBGrid, pois é nele que exibiremos a lista de suporte.

Para que o client processe o retorno é necessário criar duas classes como na Listagem 7, para que além de tratar as mensagens recebidas a resposta do client seja mantida.

Listagem 7. Classes que mantém a responsividade

  TReadingThread = class(TThread)

    protected

      FConn: TIdTCPConnection;

      procedure Execute; override;

    public

      constructor Create(AConn: TIdTCPConnection); reintroduce;

  end;

 

  TLog = class(TIdSync)

    protected

      FMsg: string;

      procedure DoSynchronize; override;

    public

      constructor Create(const AMsg: string);

      class procedure AddMsg(const AMsg: string);

  end;

Basicamente essas duas classes trabalham juntas na leitura de qualquer mensagem enviada pelo servidor. Elas asseguram que a thread principal da VCL não será utilizada quando as mensagens chegam, dessa forma a interface gráfica fica livre para que o usuário a utilize. A classe TReadingThread lê a mensagem primeiro e então utiliza a classe TLog para processá-la. Antes de podermos utilizar as classes TLog e TReadingThread, vamos implementá-las como na Listagem 7.

 

ClubeDelphi PLUS!

Acesse agora o mesmo o portal do assinante ClubeDelphi e assista a uma vídeo aula de Guinther Paulo que mostra como trabalhar com thread.  

//www.devmedia.com.br/articles/viewcomp.asp?comp=1733

 

Nota DevMan!

O que são Thread?

         Um programa normal é chamado de single thread, ou seja, processo simples ou processo único. Isso significa que só há processamento em um determinado ponto do programa como a emissão de um relatório, cadastramento de um cliente ou mesmo manipulação de qualquer tipo de informação.

         Uma thread é um processo que acontece com início, meio e fim, e também acontece em um determinado momento dentro de um sistema. Porém, quando temos mais de um processo dentro do mesmo programa chamamos isso de multithread. Multithread porque temos mais de um processo acontecendo ao mesmo tempo, como emissão de um relatório e cadastramento de um cliente ao mesmo tempo.

         As threads são usadas normalmente para agilizar processos ou criar vários processos iguais, porém trabalhando em momentos diferentes. Por exemplo: podemos solicitar a emissão de um relatório grande que levará 2 horas para ser emitido pelo programa devido a massa de dados que ele irá manipular. Se desenvolvermos um relatório como esse em nosso sistema e o usuário utilizá-lo, certamente serão 2 horas sem poder utilizar o programa caso seja single thread.

         A partir de momento que o isolamos em uma thread, temos condições de liberar o programa para que ele execute novas tarefas sem precisar aguardar o término da primeira.

         Um exemplo bastante simples de se entender é quando estamos utilizando programas especializados em downloads de arquivos, como o antigo Kazaa ou o mais recente LimeWire para download de músicas. Imagine se cada música que você ordenasse o programa a baixar você tivesse que aguardar a última música ser totalmente baixada? Levaria muito tempo. Portanto, esses programas costumam ser multithread, pois executam mais de um processo ao mesmo tempo de forma independente.

 

Crie essas duas classes na Unit do formulário principal da versão client. Basta digitar suas declarações abaixo da palavra reservada Type e em seguida pressionar Ctrl + Shift + C para que o Delphi nos crie o escopo principal de cada método. Um método DoSynchronize fará o papel de sincronizar as mensagens entre as aplicações independente do que ocorrendo no restante do aplicativo.

Precisamos também adicionar na procedure Synchronize o tratamento do retorno da lista. Parte do código dessa procedure pode ser visto na Listagem 8. Evidentemente a listagem não foi totalmente impressa no artigo devido ao seu tamanho. É nesse procedimento que prevemos e tratamos todas as mensagens enviadas e recebidas do servidor. Por exemplo: quando o nosso suporte técnico enviar uma mensagem ao cliente, essa mensagem será recebida com a string mensagemParaCliente. Encontre no trecho de código da Listagem 8 esse pedaço do código e veja o que fazemos.

Listagem 8. Procedure Synchronize no Client

procedure TLog.DoSynchronize;

var

  comando: string;

  RetornoLogin: integer;

  CodigoSuporte: integer;

  NickSuporte: string;

  OnLineSuporte: integer;

  StatusSuporte: integer;

  Mensagem: string;

  NumeroLinha: integer;

begin

  comando := Copy(FMsg,1,pos('||',FMsg)-1);

  if comando = 'sucessoLogin' then

  begin

    delete(FMsg,1,pos('||',FMsg)+1);

    RetornoLogin:= strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    case RetornoLogin of

      -1 : Form1.JaEstavaConectado;

       0 : Form1.LoginDeuErrado;

       else

       begin

         Form1.IdCliente := RetornoLogin;

         Form1.LoginDeuCerto;

       end;

    end;

    exit;

  end;

  if comando = 'listaSuporte' then

  begin

    delete(FMsg,1,pos('||',FMsg)+1);

    CodigoSuporte := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    delete(FMsg,1,pos('||',FMsg)+1);

    NickSuporte := Copy(FMsg,1,pos('||',FMsg)-1);

    delete(FMsg,1,pos('||',FMsg)+1);

    OnLineSuporte := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    delete(FMsg,1,pos('||',FMsg)+1);

    StatusSuporte := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    Form1.AtualizaListaSuporte(CodigoSuporte, NickSuporte, OnLineSuporte, StatusSuporte);

  end;

  if comando = 'mensagemParaCliente' then

  begin

    delete(FMsg, 1, Pos('||', FMsg)+1);

    CodigoSuporte := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    delete(FMsg,1,pos('||',FMsg)+1) ;

    delete(FMsg,1,pos('||',FMsg)+1) ;

    Mensagem := Copy(FMsg,1,pos('||',FMsg)-1);

    delete(FMsg,1,pos('||',FMsg)+1);

    NumeroLinha := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    Form1.IniciaChatCom(CodigoSuporte,Mensagem, NumeroLinha);

  end;

[...]

end;

 

Basicamente, apagamos os pipe-lines de nosso protocolo, pegamos o código do suporte, a mensagem e o número dela e fazemos o start da conversa chamando o método IniciaChatCom.

 

Nota: A implementação da função IniciaChatCom pode ser vista no arquivo de download disponível no site de edição no portal DevMedia.

 

Toda a procedure DoSynchronize funciona dessa forma. O mais interessante é que podemos implementar diversas funcionalidades somente alterando nosso protocolo e prevendo as mensagens em ambas aplicações, Server e Client.

A classe TReadingThread possui apenas dois métodos: Create e Execute. No Create apenas inicializamos a variável FConn e herdamos o método Create de seu ancestral. Já o método Execute, esse é encarregado de escutar a aplicação e detectar o exato momento de uma conexão. Caso uma conexão seja interceptada, então uma mensagem é gerada e enviada para procedure AddMsg da classe TLog. Isso é necessário para que possamos ter controle de tudo que é feito no sistema. O código de implementação dos métodos das classes TReadingThread e TLog podem ser vistos na Listagem 9.

Note que o método AddMsg cria uma instância da classe e sincroniza. O construtor Create apenas atualiza a variável FMsg e herda o método Create de seu ancestor. Por fim o método DoSynchronize, que é responsável por fazer o sincronismo das mensagens funcionar perfeitamente. Recebmos o comando e avaliamos. Em caso de sucesso, apagamos os caracteres “||” (pipe-line) e testamos o retorno que pode ser 0 ou 1.

Quero destacar que a classe TLog é o núcleo do client, visto que ela é quem irá processar as mensagens recebidas e irá tomar as devidas providências para que a solicitação/resposta seja feita de acordo.

Para utilizar essas classes vamos adicionar uma variável na seção Var do formulário que referencie a classe TReadingThread, como a seguir:

 

var

  Form1: TForm1;

  rt: TReadingThread = nil;

 

E então no evento onConnected do TIdTCPClient instanciamos essa thread:

 

  rt := TReadingThread.Create(tcClient);

 

E ao encerrar a conexão é preciso também encerrar a Thread de leitura, isso é feito no evento onDisconnected, como é mostrado a seguir:

 

  if rt <> nil then

  begin

    rt.Terminate;

    rt.WaitFor;

    FreeAndNil(rt);

  end;

 

Observe na Listagem 8 que na procedure TLog. DoSynchronize estamos lidando com o retorno do servidor e se a conexão for bem sucedida chamamos o método LoginDeuCerto da classe TForm1, que é o formulário. Agora ao vermos o que esse método faz, vemos que o client envia outra mensagem para o servidor, solicitando a lista de suporte. A mensagem e enviada usando o método WriteLn:

 

  PainelLogin.Visible := false;

  Caption := 'DevChat - '+ EditLogin.Text;

  tcClient.Socket.WriteLn('listaSuporte||'+ IntToStr(IdCliente));

 

Listagem 9. Implementando TReadingThread e TLog

constructor TReadingThread.Create(AConn: TIdTCPConnection);

begin

  FConn := AConn;

  inherited Create(False);

end;

procedure TReadingThread.Execute;

begin

  while not Terminated and FConn.Connected do

  begin

    TLog.AddMsg(FConn.IOHandler.Readln);

  end;

end;

{ TLog }

class procedure TLog.AddMsg(const AMsg: string);

begin

  with Create(AMsg) do

    try

       Synchronize;

    finally

       Free;

    end;

end;

constructor TLog.Create(const AMsg: string);

begin

  FMsg := AMsg;

  inherited Create;

end;

 

Quem efetivamente faz a atualização da lista de membros do suporte técnico é a função AtualizaListaSuporte apenas verifica se o código do usuário de suporte passado como parâmetro já existe no ClientDataset, se não existir ele é adicionado. Veja na Listagem 10.

Listagem 10. Código da função AtualizaListaSuporte

procedure TForm1.AtualizaListaSuporte(CodigoSuporte:

  integer; NickSuporte: string; OnLineSuporte,

  StatusSuporte: integer);

begin

  if cdsSuporte.Locate('idSuporte',CodigoSuporte,[]) then

     cdsSuporte.Edit

  else

    cdsSuporte.Insert;

  cdsSuporteidSuporte.AsInteger := CodigoSuporte;

  cdsSuporteNick.AsString := NickSuporte;

  cdsSuporteOnline.AsInteger := OnLineSuporte;

  cdsSuportestatus.AsInteger := StatusSuporte;

  cdsSuporte.Post;

end;

Montando a aplicação do suporte

As duas aplicações cliente são bastante semelhantes, diferindo apenas em alguns detalhes e funcionamentos. Por isso podemos clonar a aplicação criada no tópico anterior e fazer pequenas modificações. Adicione um novo projeto Win32 ao grupo e deixe o formulário principal semelhante o formulário do client destinado aos clientes, com a exceção que não teremos nele o TEdit referente o código da empresa. Crie também as classes TReadingThread e TLog, como foi feito no client dos clientes.

A primeira coisa que devemos pensar é que precisamos carregar a lista de clientes em nossa aplicação. Ao se conectar é preciso obter a lista dos clientes cadastrados em nosso banco de dados, para isso vamos utilizar o comando listaCliente do nosso protocolo de comunicação. O server ao receber esse comando executa uma instrução SQL no BD que retorna todos os clientes cadastrados no serviço de mensagens, incluindo nas informações o status de cada um e devolve isso ao client do suporte. Esse por sua vez trata a mensagem no método DoSynchronize da classe TLog, como é visto na Listagem 11.

Listagem 11. Trecho de DoSynchronize para tratamento da lista de clientes

[…]

  if comando = 'listaCliente' then

  begin

    delete(FMsg,1,pos('||',FMsg)+1);

    CodigoCliente := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    delete(FMsg,1,pos('||',FMsg)+1);

    NickCliente := Copy(FMsg,1,pos('||',FMsg)-1);

    delete(FMsg,1,pos('||',FMsg)+1);

    OnLineCliente := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    delete(FMsg,1,pos('||',FMsg)+1);

    StatusCliente := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    DevChatSuporteF.AtualizaListaCliente(CodigoCliente, NickCliente, OnLineCliente, StatusCliente);

  end;

[...]

 

Veja que é bastante semelhante ao que vimos no aplicativo destinado aos clientes. A diferença principal é que estamos chamando o método AtualizaListaCliente que por sua vez se utiliza de um ClientDataSet para armazenar os clientes retornados, seguindo o padrão empregado no client dos clientes.

 

Nota: Para que o client de suporte se conecte é utilizado o comando loginSuporte do protocolo que realiza as conferências necessárias para saber se o usuário de suporte é válido ou não.

 

Se executarmos a aplicação nesse momento, rodando primeiramente o servidor e posteriormente os dois clientes, veremos que em cada uma teremos a lista de usuários/suporte necessários. Porém como faremos para notificar a mudança de estado, já que mesmo estando conectados os usuários representados nas listas ainda estão off-line?

Vamos utilizar o comando mudancaEstado. Nele informamos o Id do usuário, não importando se é cliente ou suporte, seguidos do seu status e se está on-line. O server por sua vez, ao receber essa mensagem, realiza uma operação de update no banco de dados, logicamente para registrar a mudança de estado e então encaminha para todos  a mensagem de que o determinado usuário mudou seu status. Veja Listagem 12.

Listagem 12. Trecho de DoSynchronize com mudança de status

[...]

if comando = 'mudancaEstado' then

 begin

    delete(Texto,1,pos('||',Texto)+1);

    CodigoUsuario := strToInt(Copy(Texto,1,pos('||',Texto)-1));

    delete(Texto,1,pos('||',Texto)+1);

    StatusUsuario := strToInt(Copy(Texto,1,pos('||',Texto)-1));

    delete(Texto,1,pos('||',Texto)+1);

    OnLineUsuario := strToInt(Copy(Texto,1,pos('||',Texto)-1));

    NotificarMudancaEstado(AContext,codigoUsuario,TStatus(StatusUsuario),TOnLine(OnlineUsuario));

 end;

[…]

 

Nota: Como foi visto no início, os clientes e suporte ficam armazenados em tabelas distintas, então para evitar conflitos de Id, ambas as tabelas poderiam utilizar o mesmo generator ou a tabela de clientes poderia iniciar a contagem de Ids a partir do número 1000.

 

Nota DevMan!

O que são Generators’s?

         O Generator um recurso existente no banco de dados Firebird capaz de gerar número seqüenciais. Quem chegou a utilizar os bancos de dados Paradox e DBase conhece esse recurso como auto-numeração.  Havia um tipo de campo nesses bancos de dados que fazia a geração automática de números seqüenciais que normalmente são usado para evitar duplicação de registros da base.

         Com a entrada do Firebird e Interbase ao mercado, esse conceito morreu dando lugar aos Generator’s. No Firebird 2.0 o Generator foi substituído pelo recurso Sequence, que tem a mesma finalidade.

 

A procedure DoSynchronize consegue saber o estado de um usuário por que fica o tempo todo escutando as mensagens que são transmitidas e recebidas entre Server e Client. Em outras palavras, cada client ao receber o comando mudancaEstado, atualiza o estado do usuário, como é mostrado na Listagem 13 e na Figura 6.

Listagem 13. Atualizando o status do usuário ao receber comando

procedure TDevChatSuporteF.AtualizaEstado(CodigoCliente, Estado,

  Online: integer);

begin

  if CodigoCliente = IdSuporte then exit;

  cdsClientes.First;

  if cdsClientes.Locate('idCliente',CodigoCliente,[]) then

  begin

    cdsClientes.Edit;

    cdsClientesOnline.AsInteger := OnLine;

    cdsClientesStatus.AsInteger := Estado;

    cdsClientes.Post;

  end

  else

  begin

    tcClient.Socket.WriteLn('listaCliente||'+ IntToStr(IdSuporte));

  end;

end;


Figura 6. Mudança de status em execução


Desconectando do servidor

No evento onClose dos clients disparamos o método   tcClient.Disconnect que por sua vez dispara no server o evento onDisconect, indicando que um client foi desconectado, dessa forma nesse evento podemos mudar o estado desse cliente para off-line e notificamos os outros que ainda estão conectados, como é visto a seguir:

 

Total := Total - 1;

SetStatusUsuario(TUsuario(AContext.Data).IdUsuario,TStatus(0),

  TOnLine(0),TUsuario(AContext.Data).Suporte);

if Total > 0 then

 NotificarMudancaEstado(AContext,TUsuario(AContext.Data).IdUsuario,

  TStatus(0),TOnLine(0));

 

Trocando mensagens

Após termos o controle de quem está ou não está conectado é preciso realizar a troca de mensagens entre clients. Antes de tudo é preciso entender como o chat se dará. O cliente dará um clique duplo sobre o usuário de suporte que está on-line e então a janela de chat aparece. Caso o cliente dê um clique duplo em um usuário de suporte com uma conversa já iniciada, a janela onde essa conversa está sendo mantida é então exibida e não será criada uma nova. Tendo essas premissas, vemos que é preciso então criar uma forma de controlar quais janelas que já estão abertas.

Para tal começaremos com o client dos clientes. Adicione a esse projeto um novo formulário, salve-o como ChatU.pas e deixe-o como na Figura 7. E no evento onClick do botão Enviar  disparamos o comando de envio de mensagens (Listagem 14).


Figura 7. Formulário de conversação

Listagem 14. Código do evento OnClick do botão Enviar

procedure TChatF.btEnviarClick(Sender: TObject);

var

  I: integer;

begin

  for I := 0 to memoTexto.Lines.Count - 1 do

    Form1.tcClient.Socket.WriteLn('mensagemParaSuporte||'+   

    IntToStr(Form1.IdCliente)+'||'+IntToStr(Self.Tag)+

    '||'+(memoTexto.Lines[I])+'||'+IntToStr(I)+'||');

  memoChat.Lines.Append(DateTimeToStr(now) + ' - Você diz: ' +

    MemoTexto.Text);

  memoTexto.Clear;

  memoTexto.SetFocus;

end;

Perceba que é simples. Apenas enviamos a mensagem para o componente TCPClient usando o método WriteLn. Em seguida atualizamos o componente Memo com informações de data e hora da mensagem.

Crie uma propriedade pública, de nome NomeSuporte e  no seu método Set mude o Caption da janela como segue:

 

  FNomeSuporte := Value;

  Caption := 'Conversando com : ' + Value;

 

Vamos adicionar um campo privado no formulário principal do client, um campo de nome ListaJanelas que será do tipo TList. No evento onCreate do formulário instanciamos esse campo. Essa lista irá armazenar as conversas que estão em andamento.

 

ListaJanelas := TList.Create;

 

Para verificar se já existe uma conversa em andamento vamos realizar um truque aqui. Utilizaremos a propriedade Tag do TForm para armazenar o id do suporte com quem se está conversando e vamos procurar por esse id na lista de janelas. Se o id não constar criamos um novo formulário, caso contrário retornamos a instância encontrada. Tudo isso pode ser visto na Listagem 15. Esse método é então utilizado no evento OnDblClick do DBGrid de usuários de suporte disponíveis.

Listagem 15. Controlando quais conversas já estão iniciadas

function TForm1.CriaFormChat(ParaIdSuporte: integer): TChatF;

var

 x: integer;

 encontrada: boolean;

 Form : TChatF;

begin

  encontrada := false;

  for x := 0 to ListaJanelas.Count - 1 do

  begin

    if TForm(ListaJanelas.Items[x]).Tag = ParaIdSuporte then

    begin

      encontrada := true;

      result := TChatF(ListaJanelas.Items[x]);

      break;

    end;

  end;

  if not encontrada then

  begin

    Form := TChatF.Create(Self);

    Form.Tag := ParaIdSuporte;

    ListaJanelas.Add(Form);

    result := Form;

  end;

end;

O método IniciaChatCom localiza o registro do usuário de suporte com quem estamos tentando falar e verifica se ele já possui uma conversa em aberto conosco, se possuir a janela de conversação é exibida. Quando o parâmetro Mensagem possui algum valor ele é então exibido na janela de conversação. Vale lembrar aqui que a mensagem que será exibida é enviada primeiramente ao Server e que ele é que encaminha para o destinatário.

O comando é desmembrado para suas respectivas partes e então o método EnviaMensagemParaSuporte é utilizado. Esse método tem a tarefa de percorrer a lista de clients conectados e identificar quem deve receber a mensagem e então passá-la. Na Listagem 16 vemos isso. Essa lista de clients é retornada pelo método LockList de TIdContext.

Listagem 16. Enviando a mensagem para o Client correto

procedure TServerF.EnviaMensagemParaSuporte(const AContext:

  TIdContext; idUsuarioOrigem, idSuporte: integer;

  Mensagem: string; Linha: integer);

var

 List: TList;

 I: integer;

 HackContext : TContextHack;

begin

  List := TContextHack(AContext).FContextList.LockList;

  try

    for I := 0 to List.Count - 1 do

    begin

      HackContext := TContextHack(List[i]);

      if TUsuario(HackContext.Data).IdUsuario = idSuporte then

      begin

         HackContext.Connection.IOHandler.WriteLn('mensagemParaSuporte||'+

           IntToStr(idUsuarioOrigem)+'||'+IntToStr(idSuporte)+

           '||'+Mensagem+'||'+IntToStr(Linha)+'||');

         break;

      end;

    end;

  finally

    TContextHack(AContext).FContextList.UnlockList;

  end;

end;

Recebendo uma mensagem

Podemos dizer agora que nossa solução está quase pronta. Precisamos ver o último passo, receber e exibir as mensagens. Ao receber um comando MensagemParaCliente é preciso incluir no método TLog.DoSynchronize o respectivo tratamento. Observe na Listagem 17 que utilizamos o método IniciaChatCom passando todos os parâmetros,  o que fará com que a mensagem recebida seja exibida corretamente. E novamente não há grande complexidade no código. Novamente desmembramos a mensagem recebida e extraímos dela o comando recebido. Por fim chamamos a função IniciaChatCom.

Listagem 17. Recebendo uma mensagem

  if comando = 'mensagemParaCliente' then

  begin

    delete(FMsg,1,pos('||',FMsg)+1);

    CodigoSuporte := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    delete(FMsg,1,pos('||',FMsg)+1) ;

    delete(FMsg,1,pos('||',FMsg)+1) ;

    Mensagem := Copy(FMsg,1,pos('||',FMsg)-1);

    delete(FMsg,1,pos('||',FMsg)+1);

    NumeroLinha := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    Form1.IniciaChatCom(CodigoSuporte,Mensagem, NumeroLinha);

  end;

Avisando que você está digitando

Um recurso interessante é avisar para ambas as parte, de que a pessoa na outra ponta está digitamdo algo, assim como o MSN. Para implementar isso, vamos fazer uso do comando Escrevendo ou PareiEscrever definidos anteriormente no protocolo de comunicação.

Vamos adicionar no formulário de chat um TStatusBar e no evento OnChange do TMemo de digitação nós verificamos se existe texto ainda no TMemo, se existir verificamos se já não enviamos o comando Escrevendo. Se o comando ainda não foi enviado então o enviamos. Caso o TMemo não tenha texto algum, enviamos o comando PareiEscrever, confira tudo isso na Listagem 18. Quando o client recebe esses comandos ele apenas limpa ou adiciona o texto informando isso no TStatusBar (Listagem 19).

Listagem 18. Avisando da digitação

procedure TChatF.memoTextoChange(Sender: TObject);

begin

  if Trim(memoTexto.Text) = '' then

  begin

    FDigitando := false;

    Form1.tcClient.Socket.WriteLn('pareiEscrever||'+

      IntToStr(Form1.IdCliente)+'||'+

      IntToStr(Self.Tag)+'||');

    exit;

  end;

  if not FDigitando then

  begin

    FDigitando := true;

    Form1.tcClient.Socket.WriteLn('Escrevendo||'+

      IntToStr(Form1.IdCliente)+'||'+

      IntToStr(Self.Tag)+'||');

    exit;

  end;

end;

Listagem 19. Tratando comando Escrevendo/pareiEscrever

procedure TLog.DoSynchronize;

var

{...}

begin

{...}

  if comando = 'Escrevendo' then

  begin

    delete(FMsg,1,pos('||',FMsg)+1);

    CodigoSuporte := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    Form1.SuporteEscrevendo(CodigoSuporte,true);

  end;

  if comando = 'pareiEscrever' then

  begin

    delete(FMsg,1,pos('||',FMsg)+1);

    CodigoSuporte := strToInt(Copy(FMsg,1,pos('||',FMsg)-1));

    Form1.SuporteEscrevendo(CodigoSuporte,false);

  end;

end;

procedure TForm1.SuporteEscrevendo(idSuporte: integer; Escrevendo: boolean);

begin

  cdsSuporte.Locate('IdSuporte',idSuporte,[]);

  with CriaFormChat(idSuporte) do

  begin

    if Escrevendo then

      BarraMensagem.SimpleText := cdsSuporteNick.AsString + ' está escrevendo...'

    else

      BarraMensagem.SimpleText := '';

  end;

end;

Por fim, será necessário fazer a implementação do que acabamos de ver, na versão client de uso do suporte técnico. Ele é muito similiar ao Client destinado para o clientes. Existem algumas diferenças como, por exemplo, a de ao invés de listar os usuários de suporte, os clientes é que são listados.

Na seção de download dessa edição, será possível e analisar o código-fonte completo dessas aplicações.

Conclusão

Este artigo foi apenas uma introdução no desenvolvimento de um Messenger. A partir do que foi mostrado, novos recursos podem ser explorados como envio de arquivos, avisos de chamada de atenção. No server um log e histórico poderia ser montado além de ter a possibilidade de convertê-lo para um serviço do Windows. Fazer tudo isso exigiria várias edições, porém estou à disposição quando alguma dúvida surgir. Muito obrigado.