• Revista PROGRAMAR: Já está disponível a edição #53 da revista programar. Faz já o download aqui!

M6

[C#] Chat

10 mensagens neste tópico

Tendo como missão dinamizar o cantinho do C#, deixo aqui uma pequena brincadeira desenvolvida nesta linguagem, um chat, que serve para mostrar como é fácil trabalhar com sockets e threads em C#. Este artigo pode ser levado mais a sério se pensarmos que este sistema pode ser usado na comunicação entre dois sistemas efectuando, por exemplo, trocas de mensagens.

Antes de continuar tenho de referir que este artigo é baseado numa aplicação que não foi desenvolvida por mim, pelo que os créditos desta aplicação podem ser encontrados aqui. A aplicação serve apenas de base para mostrar como se trabalha com sockets e threads em C#.


Antes de falar sobre sockets, comunicação TCP e afins, urge uma explicação teórica de como este tipo de aplicações, cliente-servidor, costuma funcionar.

Normalmente existe um servidor que se encontra à escuta num determinado porto. Quando o servidor recebe um pedido de ligação de um cliente, o servidor lança uma thread que passará a efectuar a comunicação com esse cliente, ficando assim o servidor livre para responder a outras solicitações de outros clientes. No final da comunicação, tanto o cliente como a thread do lado do servidor terminam. Esta é apenas uma forma, talvez a mais comum, do modo de funcionamento deste tipo de sistemas.

Neste caso, dado que este chat é muito simples e efectua apenas uma ligação ponto-a-ponto, não existe necessidade do servidor lançar uma thread a cada pedido que recebe.

Dito isto, passemos ao chat propriamente dito. Devem acompanhar este artigo vendo o código fonte do chat, que pode ser obtido aqui. Descompactem o zip e abram o ficheiro <localização_unzip>\TcpDeviceSimulatoryListener\TcpDeviceSimulatoryListener.sln que possui ambas as aplicações o cliente e o servidor.

Abram o Form1.cs do TcpDeviceSimulatoryListener, que será o servidor e vejam que esta janela possui apenas o botão de "Start Listener", que servirá para iniciar o listener, ou seja, o servidor que ficará à escuta. A acção deste botão não é muito interessante pois faz pouco mais do que lançar o Form2.cs. Abram o Form2.cs e, este sim com mais interesse, para ver como funciona o servidor.

Inicialização

Reparem no construtor:

public Form2(String hostPort, Form1 form1) {
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//
dataReadyToSend = new AutoResetEvent(false);
refToForm1 = form1;
// Resolve the local host.
IPHostEntry localHost = Dns.Resolve(Dns.GetHostName());
// Create a local end point for listening.
IPEndPoint localEndPoint = new IPEndPoint(localHost.AddressList[0], 4001);
// Instantiate the TCP Listener.
tcpListener = new TcpListener(localEndPoint);
tcpListener.Start();
tcp = tcpListener.AcceptTcpClient();
ethernetThreadStart = new ThreadStart(this.ThreadProcPollOnEthernet);
pollDevicesEthernetThread = new Thread(ethernetThreadStart);
pollDevicesEthernetThread.Name = "Listener's Receive Thread";
pollDevicesEthernetThread.ApartmentState = System.Threading.ApartmentState.MTA;
pollDevicesEthernetThread.Start();
}

O que este construtor faz é:

1. Resolver o localhost, ou seja, descobrir qual é o IP da máquina local que funcionará como servidor:

IPHostEntry localHost = Dns.Resolve(Dns.GetHostName());

2. Criar o end point onde o o servidor irá ficar à escuta, ou seja, definir o endereço e o porto que o servidor irá usar para escutar comunicações dos clientes:

IPEndPoint localEndPoint = new IPEndPoint(localHost.AddressList[0], 4001);

3. Instanciar o listner TCP com o end point, ou seja, criar uma ligação TCP que passará a escutar no endereço e porto definidos anteriormente:

tcpListener = new TcpListener(localEndPoint); // Cria o listner TCP no end point definido anteriormente
tcpListener.Start(); // Inicia o listner
tcp = tcpListener.AcceptTcpClient(); // Instância da ligação TCP do listner definido

É de referir que o tcp é um cliente TCP, como é visível no código na secção de definição dos atributos da classe Form2, e é obtido através do listner usando TcpListener.AcceptTcpClient().

4. De seguida efectua-se lança-se a o cliente TCP numa thread de forma a que a thread onde corre o processo principal não fique bloqueada. Se não se usasse esta thread, então a janela ficaria congelada pois estaria apenas a responder à comunicação TCP, lançando o TCP numa thread separada, o utilizador pode interagir com a janela da aplicação, para escrever as suas mensagens no chat, enquantorecebe as mensagens do outro utilizador em simultâneo.

Para tal, delega-se no método ThreadProcPollOnEthernet a comunicação tcp que depois é usado na thread:

ethernetThreadStart = new ThreadStart(this.ThreadProcPollOnEthernet); // Definir que o método ThreadProcPollOnEthernet é um delegate
pollDevicesEthernetThread = new Thread(ethernetThreadStart); // Cria uma thread onde o método ThreadProcPollOnEthernet, delegado, será o "corpo" da thread
pollDevicesEthernetThread.Name = "Listener's Receive Thread";
pollDevicesEthernetThread.ApartmentState = System.Threading.ApartmentState.MTA;
pollDevicesEthernetThread.Start(); // Inicia a thread, ou seja, o método ThreadProcPollOnEthernet passa a correr num processo separado.

Receber Mensagens

5. O método ThreadProcPollOnEthernet é definido da seguinte forma:

private void ThreadProcPollOnEthernet()
{
for (;  {
	Thread.Sleep(100);			
	byte[] msg = new Byte[Constants.maxNoOfBytes];
               byte count1 = 0x01;
               for (int i = 0; i < msg.Length; i++)
               {
                   msg[i] = count1++;
               }
	try
	{
                   if (formClosing == true)
                   {
                       return;
                   }

	    int readBytes = tcp.GetStream().Read(msg,0,msg.Length);

		if (readBytes == 8)
		{
			StringBuilder shutMessage = new StringBuilder(8);
			for (int count = 0; count < 8; count++)
			{
				char ch = (char)msg[count];
				shutMessage = shutMessage.Append(ch);
			}
			string shut = "shutdown";
			string receivedMessage = shutMessage.ToString();
			if (receivedMessage.Equals(shut))
			{
				MessageBox.Show(this,"Shutdown Request has arrived from the \nconnected party.\nYou cannot send message anymore.\nPlease close the window.","Shut Down Request",MessageBoxButtons.OK,MessageBoxIcon.Information);
				buttonSend.Enabled = false;
				return;
			}
		}

		StringBuilder str = new StringBuilder(Constants.maxNoOfBytes);
		for (int count = 0; count < readBytes ; count++)
		{
			char ch = (char)msg[count];
			str = str.Append(ch);
			str = str.Append(" ");
		}
		textBox1.Text = str.ToString();                   
	}
	catch (IOException)
	{
		return;
	}
}
}

Descrito de forma simples, o que este método faz é, num ciclo infinito com intervalos de 100 milisegundos de espera:

5.1. Preparar um array de bytes com um tamanho máximo (o número máximo de caracteres que a mensagem pode ter):

	byte[] msg = new Byte[Constants.maxNoOfBytes];
               byte count1 = 0x01;
               for (int i = 0; i < msg.Length; i++)
               {
                   msg[i] = count1++;
               }

5.2. Receber a mensagem através do cliente tcp:

int readBytes = tcp.GetStream().Read(msg,0,msg.Length);

5.3. Compor a mensagem através da conversão dos bytes para uma string de caracteres e mostrá-la na textBox1:

                 
StringBuilder str = new StringBuilder(Constants.maxNoOfBytes);
for (int count = 0; count < readBytes ; count++)
{
char ch = (char)msg[count];
str = str.Append(ch);
str = str.Append(" ");
}
textBox1.Text = str.ToString();

5.2.1. É de referir que, no código original, existe uma verificação de mensagem antes do código mostrado acima que verifica se a mensagem enviado é a palavra "shutdown". Se a mensagem enviado é a palavra "shutdown", então inicia-se o processo de shutdown do chat, onde as ligações serão fechadas e não será possível enviar nem receber mais mensagens:

int readBytes = tcp.GetStream().Read(msg,0,msg.Length);

if (readBytes == 8)
{
StringBuilder shutMessage = new StringBuilder(8);
for (int count = 0; count < 8; count++)
{
	char ch = (char)msg[count];
	shutMessage = shutMessage.Append(ch);
}
string shut = "shutdown";
string receivedMessage = shutMessage.ToString();
if (receivedMessage.Equals(shut))
{
	MessageBox.Show(this,"Shutdown Request has arrived from the \nconnected party.\nYou cannot send message anymore.\nPlease close the window.","Shut Down Request",MessageBoxButtons.OK,MessageBoxIcon.Information);
	buttonSend.Enabled = false;
	return;
}
}

Enviar Mensagem

6. O envio de mensagens, que ocorre quando o utilizador pressiona o botão "Send", é tão simples quando a recepção, apenas se efectuam as operações por ordem inversa:

private void buttonSend_Click(object sender, System.EventArgs e)
{
if (textBox2.Text.Length != 0)
{
	char[] charArray = textBox2.Text.ToCharArray(0,textBox2.Text.Length);
	dataToSend = new byte[textBox2.Text.Length];
	for (int charCount = 0;
		charCount < textBox2.Text.Length;
		charCount++)
	{
		dataToSend[charCount] = (byte)charArray[charCount];
	}
}
else
{
	dataToSend = new byte[]{(byte)'e',(byte)'m',(byte)'p',(byte)'t',(byte)'y'};
}
tcp.GetStream().Write(dataToSend,0,dataToSend.Length);
textBox2.Text = "";
}

6.1. Converter a mensagem a enviar, que está numa string de characteres, para um array de bytes:

char[] charArray = textBox2.Text.ToCharArray(0,textBox2.Text.Length);
dataToSend = new byte[textBox2.Text.Length];
for (int charCount = 0;
	charCount < textBox2.Text.Length;
	charCount++)
{
	dataToSend[charCount] = (byte)charArray[charCount];
}

6.1. Enviar através do cliente tcp o array de bytes:

tcp.GetStream().Write(dataToSend,0,dataToSend.Length);

E isto termina a parte do servidor.

Comunicação Cliente

Quanto ao cliente, a primeira acção a fazer é efectuar uma ligação entre o cliente e o servidor, depois usa-se a mesma técnica explicada no servidor para enviar e receber as mensagens. O código abaixo efectua a comunicação entre o cliente e o servidor:

private void buttonConnect_Click(object sender, System.EventArgs e)
{
textBoxIPAddress.Enabled = false;
IPAddress address = IPAddress.Parse(ipAddress);

tcp = new TcpClient((new IPEndPoint(Dns.Resolve(Dns.GetHostName()).AddressList[0],4002)));
LingerOption lingerOption = new LingerOption(false, 1);
tcp.LingerState = lingerOption;
tcp.Connect(new IPEndPoint(Dns.Resolve(ipAddress).AddressList[0],4001));
buttonSend.Enabled = true;
((Button)sender).Enabled = false;
receiveThread = new Thread(new ThreadStart(ThreadProcReceive));
receiveThread.Name = "Client's Receive Thread";
receiveThread.ApartmentState = ApartmentState.MTA;
receiveThread.Start();
}

6.1. O primeiro passo é criar um endereço IP do servidor a partir da string que o utilizador especificou:

IPAddress address = IPAddress.Parse(ipAddress);

6.2. De seguida cria-se uma ligação TCP:

tcp = new TcpClient((new IPEndPoint(Dns.Resolve(Dns.GetHostName()).AddressList[0],4002)));
LingerOption lingerOption = new LingerOption(false, 1);
tcp.LingerState = lingerOption;

O LingerState define o tempo de espera aquando do fecho da ligação.

Nota: não me parece necessário criar o TcpClient usando um IPEndPoint. No entanto não verifiquei se é suficiente construir a instância do cliente TCP usando unicamente

tcp = new TcpClient();

.

6.3. Efecuta-se a ligação ao servidor:

tcp.Connect(new IPEndPoint(Dns.Resolve(ipAddress).AddressList[0],4001));

6.4. Inicia-se a thread de espera de mensagem, usando o delegate ThreadProcReceive:

receiveThread = new Thread(new ThreadStart(ThreadProcReceive));
receiveThread.Name = "Client's Receive Thread";
receiveThread.ApartmentState = ApartmentState.MTA;
receiveThread.Start();

E pronto. Após compilar a solução, podem usar este chat em duas máquinas na mesma rede da seguinte forma:

- Executar o servidor numa máquina.

- Pressionar "Start Listener".

- Executar o cliente noutra máquina.

- Introduzir o IP do servidor e pressionar "Connect"

- Escrever as mensagens na caixa de texto e pressionar "Send"

Consultem a documentação para saberem mais e para exclarecer dúvidas. Caso não vejam as vossas dúvidas exclarecidas, coloquem-nas aqui.

Espero que tenham gostado do artigo. :)


[Artigo no Wiki]

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Bem, tenho uma dúvida. Estando 2 Pcs fora da lan (2 pcs caseiros normais) dá para os ligar a partir do ip público?? (pode ser uma pergunta estúpida mas comecei este ano e estou a fazer um trabalho parecido com este mas como ainda tenho algumas duvidas dai a minha pergunta)

Se não dá, o que se tem de mudar?(senão for abusar... :x)

Desde já os meus Comprimentos e obrigado por esta excelente dica...

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Muito obrigado pela resposta rápida mas mais um pergunta...(não sendo muito chato)

Como me aconselham a fazer para puder ligar-se mais que um cliente a conversa?

Muito obrigado pela ajuda desde já se eu soube-se tinha-me registado aqui logo no principio do ano lectivo... :)

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Podes ter um listner à escuta e por cada pedido, lanças uma thread que se liga ao cliente que fez o pedido.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Olá tenho uma duvida, estou fazendo um chat onde o usuário pode ter  opção de mandar sua mensagem para todos os usuário ou privada para apenas um outro usuário, alguém pode me ajudar explicando ou me dando um exemplo como posso fazer esse controle..

qualquer coisa para contato meu email é:

cygnus.tks@hotmail.com

Obrigado

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Podes fazer isso enviando a mensagem directamente para a máquina do utilizador ou controlando a mensagem.

Por exemplo, a mensagem pode levar a indicação de que só deve ser mostrada caso o utilizador seja XXX ou se encontre na máquina ABC.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

pretendia que me envia-ses este codigo em zip

mas era o servidor e o cliente

aqui fira o email pra onde poderas enviar

(email removido pelo staff)

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

adf, isto é um fórum sobre programação, não é um serviço de aplicações à la carte.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Crie uma conta ou ligue-se para comentar

Só membros podem comentar

Criar nova conta

Registe para ter uma conta na nossa comunidade. É fácil!


Registar nova conta

Entra

Já tem conta? Inicie sessão aqui.


Entrar Agora