C # - Criando um Chat - Parte 2 - O Servidor


Neste artigo eu vou mostrar como criar um Chat procurando ser bem objetivo e mantendo as coisas o simples possível.

Na verdade uma aplicação de Chat (bate-papo) consiste em duas aplicações:

Para poder acompanhar e entender tudo que vamos fazer é necessário que se tenha conhecimento sobre os seguintes tópicos:

Na primeira parte eu criei a aplicação cliente e agora vamos criar a aplicação que atuará como servidor.

O aplicativo Chat Servidor será um pouco mais complexo do que a aplicação cliente, porque ele precisa manter informações sobre todos os clientes conectados, aguardar as mensagens de cada um e enviar mensagens de entrada para todos.

Chat - Criando a aplicação Servidor

Inicie o Visual C# 2010 Express Edition e crie um novo projeto do tipo Windows Appliation com o nome ChatServidor:

Vamos alterar o nome do formulário form1.cs criado por padrão no projeto para frmServidor.cs e a seguir definir o leiaute conforme mostra a figura abaixo no formulário:

Controles usados no formulário:
  • btnAtender - Realiza a conexão com o servidor;
  • txtIP - Informa o endereço IP de atendimento
  • txtLog - Exibe todas as mensagens;

Vamos agora ao código usado no servidor:

1- Namespaces usados:

using System.Windows.Forms;
using System.Net;

2- Logo após a declaraçãod a classe frmServidor vamos declarar um delegate que sera usado para atualziar o TextBox - txtLog da outra thread.

private delegate void AtualizaStatusCallback(string strMensagem);

3- Código do evento Click do botão Iniciar Atendimento:

         private void btnAtender_Click(object sender, System.EventArgs e)
        {
            // Analisa o endereço IP do servidor informado no textbox
            IPAddress enderecoIP = IPAddress.Parse(txtIP.Text);

            // Cria uma nova instância do objeto ChatServidor
            ChatServidor mainServidor = new ChatServidor(enderecoIP);

            // Vincula o tratamento de evento StatusChanged a mainServer_StatusChanged
            ChatServidor.StatusChanged += new StatusChangedEventHandler(mainServer_StatusChanged);

            // Inicia o atendimento das conexões
            mainServidor.IniciaAtendimento();

            // Mostra que nos iniciamos o atendimento para conexões
            txtLog.AppendText("Monitorando as conexões...\r\n");
        }

Estamos instanciando objetos, incluindo um objeto ChatServidor. Vamos escrever a classe ChatServidor logo a seguir, e você vai ver que ela lida com todas as conexões de entrada. Por sua vez, faremos uso de uma outra classe chamado Conexao.

A próxima coisa que faremos será criar um manipulador de eventos para o evento StatusChanged, que é um evento personalizado que vamos escrever. Ele vai nos informar quando um cliente se conecta, uma nova mensagem foi recebida, um cliente foi desconectado, etc

Finalmente, o método IniciaAtendimento diz ao objeto ChatServidor para iniciar a ouvir as conexões de entrada.

Vamos usar também um manipulador de eventos que ja usamos antes, e o outro é o método AtualizaStatus() que é chamado quando uma atualização precisa ser feita para o formulário.

Isso é necessário pois usamos Invoke() e o delegado que criamos anteriormente para fazer uma chamada na thread cruzada (ChatServidor esta em uma thread diferente):

Abaixo vemos o manipulador de eventos e o método AtualizaStatus() :

   
      public void mainServidor_StatusChanged(object sender, StatusChangedEventArgs e)
        {
            // Chama o método que atualiza o formulário
            this.Invoke(new AtualizaStatusCallback(this.AtualizaStatus), new object[] { e.EventMessage });
        }

        private void AtualizaStatus(string strMensagem)
        {
            // Atualiza o logo com mensagens
            txtLog.AppendText(strMensagem + "\r\n");
        }

Vamos agora criar uma classe chamada ChatServidor.cs no projeto;

No menu Project-> Add Class , selecione o template Class e informe o no me ChatServidor.cs e clique em Add;

Vamos iniciar definindo os namespaces usados nessa classe:

using System;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Collections;

A seguir vamos definir uma classe para o tratamento para o evento StatusChanged que monitora a mudança de estado relacionado as mensagens. A classe StatusChangedEventArgs :

// Trata os argumentos para o evento StatusChanged
    public class StatusChangedEventArgs : EventArgs
    {
        // Estamos interessados na mensagem descrevendo o evento
        private string EventMsg;

        // Propriedade para retornar e definir um mensagem do evento
        public string EventMessage
        {
            get { return EventMsg;}
            set { EventMsg = value;}
        }

        // Construtor para definir a mensagem do evento
        public StatusChangedEventArgs(string strEventMsg)
        {
            EventMsg = strEventMsg;
        }
    }

Precisamos definir o delegate StatusChangedEventHandler para tratar os parãmetros passados ao evento:

// Este delegate é necessário para especificar os parametros que estamos pasando com o nosso evento
public delegate void StatusChangedEventHandler(object sender, StatusChangedEventArgs e);

Na classe ChatServidor vamos tratar as mensagens e o usuários definindo os seguintes métodos:

temos também o evento OnStatusChanged que é usado para disparar o evento de mudança de estado.

O código completo da classe comentado segue abaixo:

class ChatServidor
    {
        // Esta hash table armazena os usuários e as conexões (acessado/consultado por usuário)
        public static Hashtable htUsuarios = new Hashtable(30); // 30 usuarios é o limite definido
        // Esta hash table armazena os usuários e as conexões (acessada/consultada por conexão)
        public static Hashtable htConexoes = new Hashtable(30); // 30 usuários é o limite definido
        // armazena o endereço IP passado
        private IPAddress enderecoIP;
        private TcpClient tcpCliente;
        // O evento e o seu argumento irá notificar o formulário quando um usuário se conecta, desconecta, envia uma mensagem,etc
        public static event StatusChangedEventHandler StatusChanged;
        private static StatusChangedEventArgs e;

        // O construtor define o endereço IP para aquele retornado pela instanciação do objeto
        public ChatServidor(IPAddress endereco)
        {
            enderecoIP = endereco;
        }

        // A thread que ira tratar o escutador de conexões
        private Thread thrListener;

        // O objeto TCP object que escuta as conexões
        private TcpListener tlsCliente;

        // Ira dizer ao laço while para manter a monitoração das conexões
        bool ServRodando = false;

        // Inclui o usuário nas tabelas hash
        public static void IncluiUsuario(TcpClient tcpUsuario, string strUsername)
        {
            // Primeiro inclui o nome e conexão associada para ambas as hash tables
            ChatServidor.htUsuarios.Add(strUsername, tcpUsuario);
            ChatServidor.htConexoes.Add(tcpUsuario, strUsername);

            // Informa a nova conexão para todos os usuário e para o formulário do servidor
            EnviaMensagemAdmin(htConexoes[tcpUsuario] + " entrou..");
        }

        // Remove o usuário das tabelas (hash tables)
        public static void RemoveUsuario(TcpClient tcpUsuario)
        {
            // Se o usuário existir
            if (htConexoes[tcpUsuario] != null)
            {
                // Primeiro mostra a informação e informa os outros usuários sobre a conexão
                EnviaMensagemAdmin(htConexoes[tcpUsuario] + " saiu...");

                // Removeo usuário da hash table
                ChatServidor.htUsuarios.Remove(ChatServidor.htConexoes[tcpUsuario]);
                ChatServidor.htConexoes.Remove(tcpUsuario);
            }
        }

        // Este evento é chamado quando queremos disparar o evento StatusChanged
        public static void OnStatusChanged(StatusChangedEventArgs e)
        {
            StatusChangedEventHandler statusHandler = StatusChanged;
            if (statusHandler != null)
            {
                // invoca o  delegate
                statusHandler(null, e);
            }
        }

        // Envia mensagens administratias
        public static void EnviaMensagemAdmin(string Mensagem)
        {
            StreamWriter swSenderSender;

            // Exibe primeiro na aplicação
            e = new StatusChangedEventArgs("Administrador: " + Mensagem);
            OnStatusChanged(e);

            // Cria um array de clientes TCPs do tamanho do numero de clientes existentes
            TcpClient[] tcpClientes = new TcpClient[ChatServidor.htUsuarios.Count];
            // Copia os objetos TcpClient no array
            ChatServidor.htUsuarios.Values.CopyTo(tcpClientes, 0);
            // Percorre a lista de clientes TCP
            for (int i = 0; i < tcpClientes.Length; i++)
            {
                // Tenta enviar uma mensagem para cada cliente
                try
                {
                    // Se a mensagem estiver em branco ou a conexão for nula sai...
                    if (Mensagem.Trim() == "" || tcpClientes[i] == null)
                    {
                        continue;
                    }
                    // Envia a mensagem para o usuário atual no laço
                    swSenderSender = new StreamWriter(tcpClientes[i].GetStream());
                    swSenderSender.WriteLine("Administrador: " + Mensagem);
                    swSenderSender.Flush();
                    swSenderSender = null;
                }
                catch // Se houver um problema , o usuário não existe , então remove-o
                {
                    RemoveUsuario(tcpClientes[i]);
                }
            }
        }

        // Envia mensagens de um usuário para todos os outros
        public static void EnviaMensagem(string Origem, string Mensagem)
        {
            StreamWriter swSenderSender;

            // Primeiro exibe a mensagem na aplicação
            e = new StatusChangedEventArgs(Origem + " disse : " + Mensagem);
            OnStatusChanged(e);

            // Cria um array de clientes TCPs do tamanho do numero de clientes existentes
            TcpClient[] tcpClientes = new TcpClient[ChatServidor.htUsuarios.Count];
            // Copia os objetos TcpClient no array
            ChatServidor.htUsuarios.Values.CopyTo(tcpClientes, 0);
            // Percorre a lista de clientes TCP
            for (int i = 0; i < tcpClientes.Length; i++)
            {
                // Tenta enviar uma mensagem para cada cliente
                try
                {
                    // Se a mensagem estiver em branco ou a conexão for nula sai...
                    if (Mensagem.Trim() == "" || tcpClientes[i] == null)
                    {
                        continue;
                    }
                    // Envia a mensagem para o usuário atual no laço
                    swSenderSender = new StreamWriter(tcpClientes[i].GetStream());
                    swSenderSender.WriteLine(Origem + " disse: " + Mensagem);
                    swSenderSender.Flush();
                    swSenderSender = null;
                }
                catch // Se houver um problema , o usuário não existe , então remove-o
                {
                    RemoveUsuario(tcpClientes[i]);
                }
            }
        }

        public void IniciaAtendimento()
        {
            try
            {

                // Pega o IP do primeiro dispostivo da rede
                IPAddress ipaLocal = enderecoIP;

                // Cria um objeto TCP listener usando o IP do servidor e porta definidas
                tlsCliente = new TcpListener(ipaLocal, 2502);

                // Inicia o TCP listener e escuta as conexões
                tlsCliente.Start();

                // O laço While verifica se o servidor esta rodando antes de checar as conexões
                ServRodando = true;

                // Inicia uma nova tread que hospeda o listener
                thrListener = new Thread(MantemAtendimento);
                thrListener.Start();
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private void MantemAtendimento()
        {
            // Enquanto o servidor estiver rodando
            while (ServRodando == true)
            {
                // Aceita uma conexão pendente
                tcpCliente = tlsCliente.AcceptTcpClient();
                // Cria uma nova instância da conexão
                Conexao newConnection = new Conexao(tcpCliente);
            }
        }
    }

A classe Conexao trata as conexões dos usuários e possui os métodos:

O código da classe é visto a seguir:

// Ocorre quando um novo cliente é aceito
        private void AceitaCliente()
        {
            srReceptor = new System.IO.StreamReader(tcpCliente.GetStream());
            swEnviador = new System.IO.StreamWriter(tcpCliente.GetStream());

            // Lê a informação da conta do cliente
            usuarioAtual = srReceptor.ReadLine();

            // temos uma resposta do cliente
            if (usuarioAtual != "")
            {
                // Armazena o nome do usuário na hash table
                if (ChatServidor.htUsuarios.Contains(usuarioAtual) == true)
                {
                    // 0 => significa não conectado
                    swEnviador.WriteLine("0|Este nome de usuário já existe.");
                    swEnviador.Flush();
                    FechaConexao();
                    return;
                }
                else if (usuarioAtual == "Administrator")
                {
                    // 0 => não conectado
                    swEnviador.WriteLine("0|Este nome de usuário é reservado.");
                    swEnviador.Flush();
                    FechaConexao();
                    return;
                }
                else
                {
                    // 1 => conectou com sucesso
                    swEnviador.WriteLine("1");
                    swEnviador.Flush();

                    // Inclui o usuário na hash table e inicia a escuta de suas mensagens
                    ChatServidor.IncluiUsuario(tcpCliente, usuarioAtual);
                }
            }
            else
            {
                FechaConexao();
                return;
            }
            //
            try
            {
                // Continua aguardando por uma mensagem do usuário
                while ((strResposta = srReceptor.ReadLine()) != "")
                {
                    // Se for inválido remove-o
                    if (strResposta == null)
                    {
                        ChatServidor.RemoveUsuario(tcpCliente);
                    }
                    else
                    {
                        // envia a mensagem para todos os outros usuários
                        ChatServidor.EnviaMensagem(usuarioAtual, strResposta);
                    }
                }
            }
            catch
            {
                // Se houve um problema com este usuário desconecta-o
                ChatServidor.RemoveUsuario(tcpCliente);
            }
        }

Para podermos avaliar a nossa aplicação primeiro devemos executar o servidor e iniciar o seu atendimento deixando pronto para escutar e tratar as requisições dos clientes. A seguir executaremos duas instâncias da aplicação ChatCliente para simular a conexão de dois clientes simultâneos. Como estou executando o projeto em minha máquina local vou usar o ip 127.0.0.1. A porta definida é 2502. A seguir vemos os resultados para esta simulação:

Podemos observar a conexão e a troca de mensagens entre os usuários Macoratti e Convidado.

Dessa forma lancei os fundamentos de uma aplicação Chat Multithread. Ela pode ser melhorada em diversos aspectos tornando-a mais flexível, configurável e robusta.

Pegue o projeto completo aqui: ChatServidor.zip

"Porque o salário do pecado é a morte, mas o dom gratuito de Deus é a vida eterna, por Cristo Jesus Nosso Senhor."
Romanos 6-23

Referências:


José Carlos Macoratti