Ferramentas de Usuário

Ferramentas de Site


dev_net:csharp:chat_usando_sockets_e_threads

Criar um Chat usando Sockets e Threads

Deixo aqui uma pequena brincadeira desenvolvida em C#, um chat, que serve para mostrar como é fácil trabalhar com sockets e threads. 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#.

Introdução

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.

Este artigo deve ser acompanhado vendo o código fonte do chat, que pode ser obtido aqui. O zip deve ser descomptado e aberto o ficheiro <localização_unzip>TcpDeviceSimulatoryListenerTcpDeviceSimulatoryListener.sln que possui ambas as aplicações o cliente e o servidor.

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.

Receber Mensagens

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:

  • 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++;
                }
  • Receber a mensagem através do cliente tcp:
int readBytes = tcp.GetStream().Read(msg,0,msg.Length);
  • 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();
    • É 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

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 = "";
}
  • 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];
    	}
  • Enviar através do cliente tcp o array de bytes:
    tcp.GetStream().Write(dataToSend,0,dataToSend.Length);

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();
}
  • O primeiro passo é criar um endereço IP do servidor a partir da string que o utilizador especificou:
    IPAddress address = IPAddress.Parse(ipAddress);
  • 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.

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();.
  • Efecuta-se a ligação ao servidor:
    tcp.Connect(new IPEndPoint(Dns.Resolve(ipAddress).AddressList[0],4001));
  • Inicia-se a thread de espera de mensagem, usando o delegate [color=blue]ThreadProcReceive[/color]:
	receiveThread = new Thread(new ThreadStart(ThreadProcReceive));
	receiveThread.Name = "Client's Receive Thread";
	receiveThread.ApartmentState = ApartmentState.MTA;
	receiveThread.Start();

Conclusão

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

  1. Executar o servidor numa máquina.
  2. Pressionar "Start Listener".
  3. Executar o cliente noutra máquina.
  4. Introduzir o IP do servidor e pressionar "Connect"
  5. Escrever as mensagens na caixa de texto e pressionar "Send"

Consultem a documentação para saberem mais e para exclarecer dúvidas.

Tópico de discussão no fórum: Chat
dev_net/csharp/chat_usando_sockets_e_threads.txt · Última modificação em: 2018/05/14 21:37 (edição externa)