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:
|
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: